'use client'; import { useMemo, useState, useRef, useCallback, useEffect } from 'react'; import { type ConversionResult } from '@/lib/units'; import { formatNumber, cn } from '@/lib/utils'; interface VisualComparisonProps { conversions: ConversionResult[]; color: string; onValueChange?: (value: number, unit: string, dragging: boolean) => void; } export default function VisualComparison({ conversions, color, onValueChange, }: VisualComparisonProps) { const [draggingUnit, setDraggingUnit] = useState(null); const [draggedPercentage, setDraggedPercentage] = useState(null); const dragStartX = useRef(0); const dragStartWidth = useRef(0); const activeBarRef = useRef(null); const lastUpdateTime = useRef(0); const baseConversionsRef = useRef([]); // Calculate percentages for visual bars using logarithmic scale const withPercentages = useMemo(() => { if (conversions.length === 0) return []; // Get all values const values = conversions.map(c => Math.abs(c.value)); const maxValue = Math.max(...values); const minValue = Math.min(...values.filter(v => v > 0)); if (maxValue === 0 || !isFinite(maxValue)) { return conversions.map(c => ({ ...c, percentage: 0 })); } // Use logarithmic scale for better visualization return conversions.map(c => { const absValue = Math.abs(c.value); if (absValue === 0 || !isFinite(absValue)) { return { ...c, percentage: 2 }; // Show minimal bar } // Logarithmic scale const logValue = Math.log10(absValue); const logMax = Math.log10(maxValue); const logMin = minValue > 0 ? Math.log10(minValue) : logMax - 6; // 6 orders of magnitude range const logRange = logMax - logMin; let percentage: number; if (logRange === 0) { percentage = 100; } else { percentage = ((logValue - logMin) / logRange) * 100; // Ensure bars are visible - minimum 3%, maximum 100% percentage = Math.max(3, Math.min(100, percentage)); } return { ...c, percentage, }; }); }, [conversions]); // Calculate value from percentage (reverse logarithmic scale) const calculateValueFromPercentage = useCallback(( percentage: number, minValue: number, maxValue: number ): number => { const logMax = Math.log10(maxValue); const logMin = minValue > 0 ? Math.log10(minValue) : logMax - 6; const logRange = logMax - logMin; // Convert percentage back to log value const logValue = logMin + (percentage / 100) * logRange; // Convert log value back to actual value return Math.pow(10, logValue); }, []); // Mouse drag handlers const handleMouseDown = useCallback((e: React.MouseEvent, unit: string, currentPercentage: number, barElement: HTMLDivElement) => { if (!onValueChange) return; e.preventDefault(); setDraggingUnit(unit); setDraggedPercentage(currentPercentage); dragStartX.current = e.clientX; dragStartWidth.current = currentPercentage; activeBarRef.current = barElement; // Save the current conversions as reference baseConversionsRef.current = [...conversions]; }, [onValueChange, conversions]); 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; let newPercentage = dragStartWidth.current + deltaPercentage; newPercentage = Math.max(3, Math.min(100, newPercentage)); // Update visual percentage immediately setDraggedPercentage(newPercentage); // Use the base conversions (from when drag started) for scale calculation const baseConversions = baseConversionsRef.current.length > 0 ? baseConversionsRef.current : conversions; // Calculate min/max values for the scale from BASE conversions const values = baseConversions.map(c => Math.abs(c.value)); const maxValue = Math.max(...values); const minValue = Math.min(...values.filter(v => v > 0)); // Calculate new value from percentage const newValue = calculateValueFromPercentage(newPercentage, minValue, maxValue); 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); // Don't clear draggedPercentage yet - let it clear when conversions update activeBarRef.current = null; // baseConversionsRef cleared after conversions update }, [draggingUnit, conversions, onValueChange]); // Touch drag handlers const handleTouchStart = useCallback((e: React.TouchEvent, unit: string, currentPercentage: number, barElement: HTMLDivElement) => { if (!onValueChange) return; const touch = e.touches[0]; setDraggingUnit(unit); setDraggedPercentage(currentPercentage); dragStartX.current = touch.clientX; dragStartWidth.current = currentPercentage; activeBarRef.current = barElement; // Save the current conversions as reference baseConversionsRef.current = [...conversions]; }, [onValueChange, conversions]); 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; const deltaX = touch.clientX - dragStartX.current; const deltaPercentage = (deltaX / barWidth) * 100; let newPercentage = dragStartWidth.current + deltaPercentage; newPercentage = Math.max(3, Math.min(100, newPercentage)); // Update visual percentage immediately setDraggedPercentage(newPercentage); // Use the base conversions (from when drag started) for scale calculation const baseConversions = baseConversionsRef.current.length > 0 ? baseConversionsRef.current : conversions; const values = baseConversions.map(c => Math.abs(c.value)); const maxValue = Math.max(...values); const minValue = Math.min(...values.filter(v => v > 0)); const newValue = calculateValueFromPercentage(newPercentage, minValue, maxValue); 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); // Don't clear draggedPercentage yet - let it clear when conversions update activeBarRef.current = null; // baseConversionsRef cleared after conversions update }, [draggingUnit, conversions, onValueChange]); // Add/remove global event listeners for drag useEffect(() => { if (draggingUnit) { document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); document.addEventListener('touchmove', handleTouchMove, { passive: false }); document.addEventListener('touchend', handleTouchEnd); return () => { document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); document.removeEventListener('touchmove', handleTouchMove); document.removeEventListener('touchend', handleTouchEnd); }; } }, [draggingUnit, handleMouseMove, handleMouseUp, handleTouchMove, handleTouchEnd]); // Clear drag state when conversions update after drag ends useEffect(() => { if (!draggingUnit && draggedPercentage !== null) { // Drag has ended, conversions have updated, now clear visual state setDraggedPercentage(null); baseConversionsRef.current = []; } }, [conversions, draggingUnit, draggedPercentage]); if (conversions.length === 0) { return (
Enter a value to see conversions
); } return (
{withPercentages.map(item => { const isDragging = draggingUnit === item.unit; const isDraggable = !!onValueChange; // Use draggedPercentage if this bar is being dragged const displayPercentage = isDragging && draggedPercentage !== null ? draggedPercentage : item.percentage; return (
{item.unitInfo.plural} {formatNumber(item.value)} {item.unit}
{/* Progress bar */}
{ if (isDraggable && e.currentTarget instanceof HTMLDivElement) { handleMouseDown(e, item.unit, item.percentage, e.currentTarget); } }} onTouchStart={(e) => { if (isDraggable && e.currentTarget instanceof HTMLDivElement) { handleTouchStart(e, item.unit, item.percentage, e.currentTarget); } }} > {/* Colored fill */}
{/* Percentage label overlay */}
{Math.round(displayPercentage)}%
{/* Drag hint on hover */} {isDraggable && !isDragging && (
Drag to adjust
)}
); })}
); }