Gift Auction: Follow up (#6569)

This commit is contained in:
Alexander Zinchuk 2026-01-13 01:14:09 +01:00
parent 048d531321
commit d14da19bd8
4 changed files with 101 additions and 55 deletions

View File

@ -58,12 +58,15 @@
top: 0.5rem; top: 0.5rem;
left: 0.5rem; left: 0.5rem;
padding: 0.125rem 0.375rem; display: flex;
align-items: center;
height: 1rem;
padding: 0 0.375rem;
border-radius: 1rem; border-radius: 1rem;
font-size: 0.6875rem; font-size: 0.6875rem;
font-weight: var(--font-weight-medium); font-weight: var(--font-weight-medium);
line-height: 0.75rem;
backdrop-filter: blur(0.5rem); backdrop-filter: blur(0.5rem);
} }

View File

@ -88,6 +88,7 @@
} }
.winningBadge { .winningBadge {
margin-top: 0.125rem;
padding: 0.0625rem 0.5rem; padding: 0.0625rem 0.5rem;
border-radius: 0.875rem; border-radius: 0.875rem;

View File

@ -100,14 +100,6 @@ const GiftAuctionBidModal = ({
const sliderMaxValue = Math.ceil(currentMinBid / BID_ROUNDING_STEP) * BID_ROUNDING_STEP + MAX_BID_AMOUNT_STEP; 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(() => { useEffect(() => {
setSelectedBidAmount(currentMinBid); setSelectedBidAmount(currentMinBid);
}, [currentMinBid]); }, [currentMinBid]);
@ -137,9 +129,13 @@ const GiftAuctionBidModal = ({
loadActiveGiftAuction({ giftId: renderingAuctionState.gift.id }); loadActiveGiftAuction({ giftId: renderingAuctionState.gift.id });
}); });
const handleRequestCustomValue = useLastCallback(() => {
openCustomBidModal();
});
const handleBadgeClick = useLastCallback(() => { const handleBadgeClick = useLastCallback(() => {
if (isAtMaxValue) { if (isAtMaxValue) {
openCustomBidModal(); handleRequestCustomValue();
} }
}); });
@ -354,12 +350,14 @@ const GiftAuctionBidModal = ({
<StarSlider <StarSlider
className={styles.slider} className={styles.slider}
defaultValue={currentMinBid} defaultValue={currentMinBid}
minValue={giftMinBid} minValue={baseMinBid}
minAllowedValue={currentMinBid} minAllowedValue={currentMinBid}
minAllowedProgress={MIN_SLIDER_PROGRESS}
maxValue={sliderMaxValue} maxValue={sliderMaxValue}
floatingBadgeDescription={sliderSecondaryText} floatingBadgeDescription={sliderSecondaryText}
onChange={handleAmountChange} onChange={handleAmountChange}
onBadgeClick={handleBadgeClick} onBadgeClick={handleBadgeClick}
onCustomValueClick={handleRequestCustomValue}
shouldUseDynamicColor shouldUseDynamicColor
shouldAllowCustomValue shouldAllowCustomValue
/> />

View File

@ -23,12 +23,14 @@ type OwnProps = {
defaultValue: number; defaultValue: number;
minValue?: number; minValue?: number;
minAllowedValue?: number; minAllowedValue?: number;
minAllowedProgress?: number;
className?: string; className?: string;
floatingBadgeDescription?: TeactNode; floatingBadgeDescription?: TeactNode;
shouldUseDynamicColor?: boolean; shouldUseDynamicColor?: boolean;
shouldAllowCustomValue?: boolean; shouldAllowCustomValue?: boolean;
onChange: (value: number) => void; onChange: (value: number) => void;
onBadgeClick?: NoneToVoidFunction; onBadgeClick?: NoneToVoidFunction;
onCustomValueClick?: NoneToVoidFunction;
}; };
const DEFAULT_POINTS = [50, 100, 500, 1000, 2000, 5000, 10000]; const DEFAULT_POINTS = [50, 100, 500, 1000, 2000, 5000, 10000];
@ -65,62 +67,63 @@ const SLIDER_COLORS = [
'#40A920', // Green '#40A920', // Green
'#E29A09', // Yellow '#E29A09', // Yellow
'#ED771E', // Orange '#ED771E', // Orange
'#E14542', // Red '#E14741', // Red
'#596473', // Silver (100% only) '#5B6676', // Silver
]; ];
function getColorForProgress(progress: number): string { function getColorForProgress(progress: number): string {
if (progress >= 1) return SLIDER_COLORS[SLIDER_COLORS.length - 1]; const index = Math.floor(progress * SLIDER_COLORS.length);
return SLIDER_COLORS[Math.min(index, 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 StarSlider = ({ const StarSlider = ({
maxValue, maxValue,
defaultValue, defaultValue,
minValue, minValue: minValueProp,
minAllowedValue, minAllowedValue,
minAllowedProgress,
className, className,
floatingBadgeDescription, floatingBadgeDescription,
shouldUseDynamicColor, shouldUseDynamicColor,
shouldAllowCustomValue, shouldAllowCustomValue,
onChange, onChange,
onBadgeClick, onBadgeClick,
onCustomValueClick,
}: OwnProps) => { }: OwnProps) => {
const containerRef = useRef<HTMLDivElement>(); const containerRef = useRef<HTMLDivElement>();
const floatingBadgeContentRef = useRef<HTMLDivElement>(); const floatingBadgeContentRef = useRef<HTMLDivElement>();
const lang = useLang(); 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 points = useMemo(() => {
const result = []; return buildPoints(actualMinValue, maxValue);
}, [maxValue, actualMinValue]);
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]);
const [value, setValue] = useState(0); const [value, setValue] = useState(0);
const [containerWidth, setContainerWidth] = useState(0); const [containerWidth, setContainerWidth] = useState(0);
@ -129,14 +132,14 @@ const StarSlider = ({
const startXRef = useRef<number | undefined>(); const startXRef = useRef<number | undefined>();
const prevBadgeWidth = usePrevious(badgeWidth); const prevBadgeWidth = usePrevious(badgeWidth);
const badgeText = lang.number(getValue(points, value, min)); const badgeText = lang.number(getValue(points, value, actualMinValue));
const minAllowedProgress = minAllowedValue !== undefined const minSliderProgress = minAllowedValue !== undefined
? getProgress(points, minAllowedValue, min) : 0; ? getProgress(points, minAllowedValue, actualMinValue) : 0;
useEffect(() => { useEffect(() => {
setValue(getProgress(points, defaultValue, min)); setValue(getProgress(points, defaultValue, actualMinValue));
}, [defaultValue, points, min]); }, [defaultValue, points, actualMinValue]);
useEffect(() => { useEffect(() => {
if (!floatingBadgeContentRef.current) return; if (!floatingBadgeContentRef.current) return;
@ -183,10 +186,10 @@ const StarSlider = ({
const handleChange = useLastCallback((event: React.ChangeEvent<HTMLInputElement>) => { const handleChange = useLastCallback((event: React.ChangeEvent<HTMLInputElement>) => {
const rawValue = Number(event.currentTarget.value); const rawValue = Number(event.currentTarget.value);
const clampedValue = Math.max(rawValue, minAllowedProgress); const clampedValue = Math.max(rawValue, minSliderProgress);
setValue(clampedValue); setValue(clampedValue);
const resultValue = getValue(points, clampedValue, min); const resultValue = getValue(points, clampedValue, actualMinValue);
onChange(resultValue); onChange(resultValue);
}); });
@ -203,8 +206,20 @@ const StarSlider = ({
} }
}); });
const handlePointerUp = useLastCallback(() => { const handlePointerUp = useLastCallback((e: React.PointerEvent<HTMLInputElement>) => {
startXRef.current = undefined; 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); setIsDragging(false);
}); });
@ -291,7 +306,7 @@ const StarSlider = ({
type="range" type="range"
min={0} min={0}
max={points.length} max={points.length}
defaultValue={getProgress(points, defaultValue, min)} defaultValue={getProgress(points, defaultValue, actualMinValue)}
step="any" step="any"
onChange={handleChange} onChange={handleChange}
onPointerDown={handlePointerDown} 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) { function getProgress(points: number[], value: number, minValue: number) {
const pointIndex = points.findIndex((point) => value <= point); const pointIndex = points.findIndex((point) => value <= point);
const prevPoint = points[pointIndex - 1] || minValue; const prevPoint = points[pointIndex - 1] || minValue;