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;
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);
}

View File

@ -88,6 +88,7 @@
}
.winningBadge {
margin-top: 0.125rem;
padding: 0.0625rem 0.5rem;
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 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 = ({
<StarSlider
className={styles.slider}
defaultValue={currentMinBid}
minValue={giftMinBid}
minValue={baseMinBid}
minAllowedValue={currentMinBid}
minAllowedProgress={MIN_SLIDER_PROGRESS}
maxValue={sliderMaxValue}
floatingBadgeDescription={sliderSecondaryText}
onChange={handleAmountChange}
onBadgeClick={handleBadgeClick}
onCustomValueClick={handleRequestCustomValue}
shouldUseDynamicColor
shouldAllowCustomValue
/>

View File

@ -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<HTMLDivElement>();
const floatingBadgeContentRef = useRef<HTMLDivElement>();
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<number | undefined>();
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<HTMLInputElement>) => {
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<HTMLInputElement>) => {
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;