fix: history only saves on drag end + throttle drag updates
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 <noreply@anthropic.com>
This commit is contained in:
@@ -32,6 +32,7 @@ export default function MainConverter() {
|
|||||||
const [favorites, setFavorites] = useState<string[]>([]);
|
const [favorites, setFavorites] = useState<string[]>([]);
|
||||||
const [copiedUnit, setCopiedUnit] = useState<string | null>(null);
|
const [copiedUnit, setCopiedUnit] = useState<string | null>(null);
|
||||||
const [showVisualComparison, setShowVisualComparison] = useState(false);
|
const [showVisualComparison, setShowVisualComparison] = useState(false);
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
|
||||||
const measures = getAllMeasures();
|
const measures = getAllMeasures();
|
||||||
const units = getUnitsForMeasure(selectedMeasure);
|
const units = getUnitsForMeasure(selectedMeasure);
|
||||||
@@ -92,8 +93,10 @@ export default function MainConverter() {
|
|||||||
setFavorites(getFavorites());
|
setFavorites(getFavorites());
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Save to history when conversion happens
|
// Save to history when conversion happens (but not during dragging)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (isDragging) return; // Don't save to history while dragging
|
||||||
|
|
||||||
const numValue = parseNumberInput(inputValue);
|
const numValue = parseNumberInput(inputValue);
|
||||||
if (numValue !== null && selectedUnit && conversions.length > 0) {
|
if (numValue !== null && selectedUnit && conversions.length > 0) {
|
||||||
// Save first conversion to history
|
// Save first conversion to history
|
||||||
@@ -108,7 +111,7 @@ export default function MainConverter() {
|
|||||||
window.dispatchEvent(new Event('historyUpdated'));
|
window.dispatchEvent(new Event('historyUpdated'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [inputValue, selectedUnit, conversions, selectedMeasure]);
|
}, [inputValue, selectedUnit, conversions, selectedMeasure, isDragging]);
|
||||||
|
|
||||||
// Handle search selection
|
// Handle search selection
|
||||||
const handleSearchSelect = useCallback((unit: string, measure: Measure) => {
|
const handleSearchSelect = useCallback((unit: string, measure: Measure) => {
|
||||||
@@ -124,8 +127,9 @@ export default function MainConverter() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Handle value change from draggable bars
|
// 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
|
// When dragging a bar, switch to that unit as the source
|
||||||
|
setIsDragging(dragging);
|
||||||
setInputValue(value.toString());
|
setInputValue(value.toString());
|
||||||
setSelectedUnit(unit);
|
setSelectedUnit(unit);
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { formatNumber, cn } from '@/lib/utils';
|
|||||||
interface VisualComparisonProps {
|
interface VisualComparisonProps {
|
||||||
conversions: ConversionResult[];
|
conversions: ConversionResult[];
|
||||||
color: string;
|
color: string;
|
||||||
onValueChange?: (value: number, unit: string) => void;
|
onValueChange?: (value: number, unit: string, dragging: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function VisualComparison({
|
export default function VisualComparison({
|
||||||
@@ -19,6 +19,7 @@ export default function VisualComparison({
|
|||||||
const dragStartX = useRef<number>(0);
|
const dragStartX = useRef<number>(0);
|
||||||
const dragStartWidth = useRef<number>(0);
|
const dragStartWidth = useRef<number>(0);
|
||||||
const activeBarRef = useRef<HTMLDivElement | null>(null);
|
const activeBarRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const lastUpdateTime = useRef<number>(0);
|
||||||
// Calculate percentages for visual bars using logarithmic scale
|
// Calculate percentages for visual bars using logarithmic scale
|
||||||
const withPercentages = useMemo(() => {
|
const withPercentages = useMemo(() => {
|
||||||
if (conversions.length === 0) return [];
|
if (conversions.length === 0) return [];
|
||||||
@@ -93,6 +94,11 @@ export default function VisualComparison({
|
|||||||
const handleMouseMove = useCallback((e: MouseEvent) => {
|
const handleMouseMove = useCallback((e: MouseEvent) => {
|
||||||
if (!draggingUnit || !activeBarRef.current || !onValueChange) return;
|
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 barWidth = activeBarRef.current.offsetWidth;
|
||||||
const deltaX = e.clientX - dragStartX.current;
|
const deltaX = e.clientX - dragStartX.current;
|
||||||
const deltaPercentage = (deltaX / barWidth) * 100;
|
const deltaPercentage = (deltaX / barWidth) * 100;
|
||||||
@@ -112,13 +118,20 @@ export default function VisualComparison({
|
|||||||
// Calculate new value from percentage
|
// Calculate new value from percentage
|
||||||
const newValue = calculateValueFromPercentage(newPercentage, minValue, maxValue);
|
const newValue = calculateValueFromPercentage(newPercentage, minValue, maxValue);
|
||||||
|
|
||||||
onValueChange(newValue, draggingUnit);
|
onValueChange(newValue, draggingUnit, true); // true = currently dragging
|
||||||
}, [draggingUnit, conversions, onValueChange, calculateValueFromPercentage]);
|
}, [draggingUnit, conversions, onValueChange, calculateValueFromPercentage]);
|
||||||
|
|
||||||
const handleMouseUp = useCallback(() => {
|
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);
|
setDraggingUnit(null);
|
||||||
activeBarRef.current = null;
|
activeBarRef.current = null;
|
||||||
}, []);
|
}, [draggingUnit, conversions, onValueChange]);
|
||||||
|
|
||||||
// Touch drag handlers
|
// Touch drag handlers
|
||||||
const handleTouchStart = useCallback((e: React.TouchEvent, unit: string, currentPercentage: number, barElement: HTMLDivElement) => {
|
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) => {
|
const handleTouchMove = useCallback((e: TouchEvent) => {
|
||||||
if (!draggingUnit || !activeBarRef.current || !onValueChange) return;
|
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
|
e.preventDefault(); // Prevent scrolling while dragging
|
||||||
const touch = e.touches[0];
|
const touch = e.touches[0];
|
||||||
const barWidth = activeBarRef.current.offsetWidth;
|
const barWidth = activeBarRef.current.offsetWidth;
|
||||||
@@ -152,13 +170,20 @@ export default function VisualComparison({
|
|||||||
|
|
||||||
const newValue = calculateValueFromPercentage(newPercentage, minValue, maxValue);
|
const newValue = calculateValueFromPercentage(newPercentage, minValue, maxValue);
|
||||||
|
|
||||||
onValueChange(newValue, draggingUnit);
|
onValueChange(newValue, draggingUnit, true); // true = currently dragging
|
||||||
}, [draggingUnit, conversions, onValueChange, calculateValueFromPercentage]);
|
}, [draggingUnit, conversions, onValueChange, calculateValueFromPercentage]);
|
||||||
|
|
||||||
const handleTouchEnd = useCallback(() => {
|
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);
|
setDraggingUnit(null);
|
||||||
activeBarRef.current = null;
|
activeBarRef.current = null;
|
||||||
}, []);
|
}, [draggingUnit, conversions, onValueChange]);
|
||||||
|
|
||||||
// Add/remove global event listeners for drag
|
// Add/remove global event listeners for drag
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user