diff --git a/src/components/middle/message/WebPageStarGiftAuction.module.scss b/src/components/middle/message/WebPageStarGiftAuction.module.scss index 11c69ceed..8192bd19d 100644 --- a/src/components/middle/message/WebPageStarGiftAuction.module.scss +++ b/src/components/middle/message/WebPageStarGiftAuction.module.scss @@ -58,12 +58,15 @@ top: 0.5rem; left: 0.5rem; - padding: 0.125rem 0.375rem; + display: flex; + align-items: center; + + height: 1rem; + padding: 0 0.375rem; border-radius: 1rem; font-size: 0.6875rem; font-weight: var(--font-weight-medium); - line-height: 0.75rem; backdrop-filter: blur(0.5rem); } diff --git a/src/components/modals/gift/auction/GiftAuctionBidModal.module.scss b/src/components/modals/gift/auction/GiftAuctionBidModal.module.scss index 7c6f53a12..c13a65893 100644 --- a/src/components/modals/gift/auction/GiftAuctionBidModal.module.scss +++ b/src/components/modals/gift/auction/GiftAuctionBidModal.module.scss @@ -88,6 +88,7 @@ } .winningBadge { + margin-top: 0.125rem; padding: 0.0625rem 0.5rem; border-radius: 0.875rem; diff --git a/src/components/modals/gift/auction/GiftAuctionBidModal.tsx b/src/components/modals/gift/auction/GiftAuctionBidModal.tsx index 6dbb6f859..9d8d998df 100644 --- a/src/components/modals/gift/auction/GiftAuctionBidModal.tsx +++ b/src/components/modals/gift/auction/GiftAuctionBidModal.tsx @@ -100,14 +100,6 @@ const GiftAuctionBidModal = ({ const sliderMaxValue = Math.ceil(currentMinBid / BID_ROUNDING_STEP) * BID_ROUNDING_STEP + MAX_BID_AMOUNT_STEP; - const currentProgress = (currentMinBid - baseMinBid) / (sliderMaxValue - baseMinBid); - const adjustedMinBid = Math.floor( - (currentMinBid - MIN_SLIDER_PROGRESS * sliderMaxValue) / (1 - MIN_SLIDER_PROGRESS), - ); - const giftMinBid = currentProgress > MIN_SLIDER_PROGRESS - ? Math.max(1, adjustedMinBid) - : baseMinBid; - useEffect(() => { setSelectedBidAmount(currentMinBid); }, [currentMinBid]); @@ -137,9 +129,13 @@ const GiftAuctionBidModal = ({ loadActiveGiftAuction({ giftId: renderingAuctionState.gift.id }); }); + const handleRequestCustomValue = useLastCallback(() => { + openCustomBidModal(); + }); + const handleBadgeClick = useLastCallback(() => { if (isAtMaxValue) { - openCustomBidModal(); + handleRequestCustomValue(); } }); @@ -354,12 +350,14 @@ const GiftAuctionBidModal = ({ diff --git a/src/components/modals/paidReaction/StarSlider.tsx b/src/components/modals/paidReaction/StarSlider.tsx index ab84513b6..5c40dcfc8 100644 --- a/src/components/modals/paidReaction/StarSlider.tsx +++ b/src/components/modals/paidReaction/StarSlider.tsx @@ -23,12 +23,14 @@ type OwnProps = { defaultValue: number; minValue?: number; minAllowedValue?: number; + minAllowedProgress?: number; className?: string; floatingBadgeDescription?: TeactNode; shouldUseDynamicColor?: boolean; shouldAllowCustomValue?: boolean; onChange: (value: number) => void; onBadgeClick?: NoneToVoidFunction; + onCustomValueClick?: NoneToVoidFunction; }; const DEFAULT_POINTS = [50, 100, 500, 1000, 2000, 5000, 10000]; @@ -65,62 +67,63 @@ const SLIDER_COLORS = [ '#40A920', // Green '#E29A09', // Yellow '#ED771E', // Orange - '#E14542', // Red - '#596473', // Silver (100% only) + '#E14741', // Red + '#5B6676', // Silver ]; function getColorForProgress(progress: number): string { - if (progress >= 1) return SLIDER_COLORS[SLIDER_COLORS.length - 1]; - - const regularColorsCount = SLIDER_COLORS.length - 1; - const index = Math.floor(progress * regularColorsCount); - return SLIDER_COLORS[Math.min(index, regularColorsCount - 1)]; + const index = Math.floor(progress * SLIDER_COLORS.length); + return SLIDER_COLORS[Math.min(index, SLIDER_COLORS.length - 1)]; } const StarSlider = ({ maxValue, defaultValue, - minValue, + minValue: minValueProp, minAllowedValue, + minAllowedProgress, className, floatingBadgeDescription, shouldUseDynamicColor, shouldAllowCustomValue, onChange, onBadgeClick, + onCustomValueClick, }: OwnProps) => { const containerRef = useRef(); const floatingBadgeContentRef = useRef(); const lang = useLang(); - const min = minValue ?? 1; + const baseMinValue = minValueProp ?? 1; + + // Uses binary search - O(log n) + const actualMinValue = useMemo(() => { + if (!minAllowedProgress || !minAllowedValue || minAllowedValue <= baseMinValue) { + return baseMinValue; + } + + let low = baseMinValue; + let high = minAllowedValue; + + while (low < high) { + const mid = Math.floor((low + high) / 2); + const testPoints = buildPoints(mid, maxValue); + const testProgress = getProgress(testPoints, minAllowedValue, mid); + const normalizedProgress = testProgress / testPoints.length; + + if (normalizedProgress < minAllowedProgress) { + high = mid; + } else { + low = mid + 1; + } + } + + return Math.max(baseMinValue, low); + }, [baseMinValue, minAllowedValue, minAllowedProgress, maxValue]); const points = useMemo(() => { - const result = []; - - for (let i = 0; i < DEFAULT_POINTS.length; i++) { - if (DEFAULT_POINTS[i] <= min) continue; - - if (DEFAULT_POINTS[i] < maxValue) { - result.push(DEFAULT_POINTS[i]); - } - - if (DEFAULT_POINTS[i] >= maxValue) { - result.push(maxValue); - return result; - } - } - - const lastPoint = DEFAULT_POINTS[DEFAULT_POINTS.length - 1]; - let nextPoint = lastPoint + LARGE_STEP; - while (nextPoint < maxValue) { - result.push(nextPoint); - nextPoint += LARGE_STEP; - } - result.push(maxValue); - - return result; - }, [maxValue, min]); + return buildPoints(actualMinValue, maxValue); + }, [maxValue, actualMinValue]); const [value, setValue] = useState(0); const [containerWidth, setContainerWidth] = useState(0); @@ -129,14 +132,14 @@ const StarSlider = ({ const startXRef = useRef(); const prevBadgeWidth = usePrevious(badgeWidth); - const badgeText = lang.number(getValue(points, value, min)); + const badgeText = lang.number(getValue(points, value, actualMinValue)); - const minAllowedProgress = minAllowedValue !== undefined - ? getProgress(points, minAllowedValue, min) : 0; + const minSliderProgress = minAllowedValue !== undefined + ? getProgress(points, minAllowedValue, actualMinValue) : 0; useEffect(() => { - setValue(getProgress(points, defaultValue, min)); - }, [defaultValue, points, min]); + setValue(getProgress(points, defaultValue, actualMinValue)); + }, [defaultValue, points, actualMinValue]); useEffect(() => { if (!floatingBadgeContentRef.current) return; @@ -183,10 +186,10 @@ const StarSlider = ({ const handleChange = useLastCallback((event: React.ChangeEvent) => { const rawValue = Number(event.currentTarget.value); - const clampedValue = Math.max(rawValue, minAllowedProgress); + const clampedValue = Math.max(rawValue, minSliderProgress); setValue(clampedValue); - const resultValue = getValue(points, clampedValue, min); + const resultValue = getValue(points, clampedValue, actualMinValue); onChange(resultValue); }); @@ -203,8 +206,20 @@ const StarSlider = ({ } }); - const handlePointerUp = useLastCallback(() => { + const handlePointerUp = useLastCallback((e: React.PointerEvent) => { startXRef.current = undefined; + + if (!isDragging && onCustomValueClick && containerRef.current) { + const rect = containerRef.current.getBoundingClientRect(); + const clickZoneWidth = 1.875 * REM; + + const isInIconArea = e.clientX >= rect.right - clickZoneWidth && e.clientX <= rect.right; + + if (isInIconArea) { + onCustomValueClick(); + } + } + setIsDragging(false); }); @@ -291,7 +306,7 @@ const StarSlider = ({ type="range" min={0} max={points.length} - defaultValue={getProgress(points, defaultValue, min)} + defaultValue={getProgress(points, defaultValue, actualMinValue)} step="any" onChange={handleChange} onPointerDown={handlePointerDown} @@ -305,6 +320,35 @@ const StarSlider = ({ ); }; +function buildPoints(minValue: number, maxValue: number): number[] { + const result = []; + + for (let i = 0; i < DEFAULT_POINTS.length; i++) { + if (DEFAULT_POINTS[i] <= minValue) continue; + + if (DEFAULT_POINTS[i] < maxValue) { + result.push(DEFAULT_POINTS[i]); + } + + if (DEFAULT_POINTS[i] >= maxValue) { + result.push(maxValue); + return result; + } + } + + const lastPoint = DEFAULT_POINTS[DEFAULT_POINTS.length - 1]; + const stepsNeeded = Math.ceil((minValue - lastPoint) / LARGE_STEP); + let nextPoint = lastPoint + Math.max(1, stepsNeeded) * LARGE_STEP; + + while (nextPoint < maxValue) { + result.push(nextPoint); + nextPoint += LARGE_STEP; + } + result.push(maxValue); + + return result; +} + function getProgress(points: number[], value: number, minValue: number) { const pointIndex = points.findIndex((point) => value <= point); const prevPoint = points[pointIndex - 1] || minValue;