From f214ddf0bafa6febe9e1a0b0395d227c63a5b46a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Sat, 8 Nov 2025 10:54:05 +0100 Subject: [PATCH] fix: history only saves on drag end + throttle drag updates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed two critical issues with draggable bars: 1. History now only saves on drag end (not during dragging): - Added isDragging state to MainConverter - onValueChange callback now accepts dragging boolean parameter - History useEffect skips saving when isDragging is true - On mouseup/touchend, call onValueChange with dragging=false to trigger save - Prevents hundreds of history entries from a single drag 2. Throttled drag updates for better performance: - Added lastUpdateTime ref to track update frequency - Limited updates to 60fps (every 16ms) - Prevents React from being overwhelmed with rapid state updates - Smoother, more responsive dragging experience How it works: - During drag: onValueChange(value, unit, true) → isDragging=true → history skips - On drag end: onValueChange(value, unit, false) → isDragging=false → history saves - Drag move: throttled to max 60 updates per second This should make bars update smoothly during drag and history clean with only one entry per drag operation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- components/converter/MainConverter.tsx | 10 +++++-- components/converter/VisualComparison.tsx | 35 +++++++++++++++++++---- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/components/converter/MainConverter.tsx b/components/converter/MainConverter.tsx index be2ea9e..76fd56b 100644 --- a/components/converter/MainConverter.tsx +++ b/components/converter/MainConverter.tsx @@ -32,6 +32,7 @@ export default function MainConverter() { const [favorites, setFavorites] = useState([]); const [copiedUnit, setCopiedUnit] = useState(null); const [showVisualComparison, setShowVisualComparison] = useState(false); + const [isDragging, setIsDragging] = useState(false); const measures = getAllMeasures(); const units = getUnitsForMeasure(selectedMeasure); @@ -92,8 +93,10 @@ export default function MainConverter() { setFavorites(getFavorites()); }, []); - // Save to history when conversion happens + // Save to history when conversion happens (but not during dragging) useEffect(() => { + if (isDragging) return; // Don't save to history while dragging + const numValue = parseNumberInput(inputValue); if (numValue !== null && selectedUnit && conversions.length > 0) { // Save first conversion to history @@ -108,7 +111,7 @@ export default function MainConverter() { window.dispatchEvent(new Event('historyUpdated')); } } - }, [inputValue, selectedUnit, conversions, selectedMeasure]); + }, [inputValue, selectedUnit, conversions, selectedMeasure, isDragging]); // Handle search selection const handleSearchSelect = useCallback((unit: string, measure: Measure) => { @@ -124,8 +127,9 @@ export default function MainConverter() { }, []); // Handle value change from draggable bars - const handleValueChange = useCallback((value: number, unit: string) => { + const handleValueChange = useCallback((value: number, unit: string, dragging: boolean) => { // When dragging a bar, switch to that unit as the source + setIsDragging(dragging); setInputValue(value.toString()); setSelectedUnit(unit); }, []); diff --git a/components/converter/VisualComparison.tsx b/components/converter/VisualComparison.tsx index 6869be9..1d4e65e 100644 --- a/components/converter/VisualComparison.tsx +++ b/components/converter/VisualComparison.tsx @@ -7,7 +7,7 @@ import { formatNumber, cn } from '@/lib/utils'; interface VisualComparisonProps { conversions: ConversionResult[]; color: string; - onValueChange?: (value: number, unit: string) => void; + onValueChange?: (value: number, unit: string, dragging: boolean) => void; } export default function VisualComparison({ @@ -19,6 +19,7 @@ export default function VisualComparison({ const dragStartX = useRef(0); const dragStartWidth = useRef(0); const activeBarRef = useRef(null); + const lastUpdateTime = useRef(0); // Calculate percentages for visual bars using logarithmic scale const withPercentages = useMemo(() => { if (conversions.length === 0) return []; @@ -93,6 +94,11 @@ export default function VisualComparison({ const handleMouseMove = useCallback((e: MouseEvent) => { if (!draggingUnit || !activeBarRef.current || !onValueChange) return; + // Throttle updates to every 16ms (~60fps) + const now = Date.now(); + if (now - lastUpdateTime.current < 16) return; + lastUpdateTime.current = now; + const barWidth = activeBarRef.current.offsetWidth; const deltaX = e.clientX - dragStartX.current; const deltaPercentage = (deltaX / barWidth) * 100; @@ -112,13 +118,20 @@ export default function VisualComparison({ // Calculate new value from percentage const newValue = calculateValueFromPercentage(newPercentage, minValue, maxValue); - onValueChange(newValue, draggingUnit); + onValueChange(newValue, draggingUnit, true); // true = currently dragging }, [draggingUnit, conversions, onValueChange, calculateValueFromPercentage]); const handleMouseUp = useCallback(() => { + if (draggingUnit && onValueChange) { + // Find the current value for the dragged unit + const conversion = conversions.find(c => c.unit === draggingUnit); + if (conversion) { + onValueChange(conversion.value, draggingUnit, false); // false = drag ended + } + } setDraggingUnit(null); activeBarRef.current = null; - }, []); + }, [draggingUnit, conversions, onValueChange]); // Touch drag handlers const handleTouchStart = useCallback((e: React.TouchEvent, unit: string, currentPercentage: number, barElement: HTMLDivElement) => { @@ -134,6 +147,11 @@ export default function VisualComparison({ const handleTouchMove = useCallback((e: TouchEvent) => { if (!draggingUnit || !activeBarRef.current || !onValueChange) return; + // Throttle updates to every 16ms (~60fps) + const now = Date.now(); + if (now - lastUpdateTime.current < 16) return; + lastUpdateTime.current = now; + e.preventDefault(); // Prevent scrolling while dragging const touch = e.touches[0]; const barWidth = activeBarRef.current.offsetWidth; @@ -152,13 +170,20 @@ export default function VisualComparison({ const newValue = calculateValueFromPercentage(newPercentage, minValue, maxValue); - onValueChange(newValue, draggingUnit); + onValueChange(newValue, draggingUnit, true); // true = currently dragging }, [draggingUnit, conversions, onValueChange, calculateValueFromPercentage]); const handleTouchEnd = useCallback(() => { + if (draggingUnit && onValueChange) { + // Find the current value for the dragged unit + const conversion = conversions.find(c => c.unit === draggingUnit); + if (conversion) { + onValueChange(conversion.value, draggingUnit, false); // false = drag ended + } + } setDraggingUnit(null); activeBarRef.current = null; - }, []); + }, [draggingUnit, conversions, onValueChange]); // Add/remove global event listeners for drag useEffect(() => {