'use client'; import { useMemo, useState, useRef, useCallback, useEffect } from 'react'; import { type ConversionResult } from '@/lib/units/units'; import { formatNumber, cn } from '@/lib/utils'; interface VisualComparisonProps { conversions: ConversionResult[]; onValueChange?: (value: number, unit: string, dragging: boolean) => void; } export default function VisualComparison({ conversions, 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([]); const withPercentages = useMemo(() => { if (conversions.length === 0) return []; const scaleSource = baseConversionsRef.current.length > 0 ? baseConversionsRef.current : conversions; const values = scaleSource.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 })); } return conversions.map((c) => { const absValue = Math.abs(c.value); if (absValue === 0 || !isFinite(absValue)) return { ...c, percentage: 2 }; const logValue = Math.log10(absValue); const logMax = Math.log10(maxValue); const logMin = minValue > 0 ? Math.log10(minValue) : logMax - 6; const logRange = logMax - logMin; const percentage = logRange === 0 ? 100 : Math.max(3, Math.min(100, ((logValue - logMin) / logRange) * 100)); return { ...c, percentage }; }); }, [conversions]); const calculateValueFromPercentage = useCallback( (percentage: number, minValue: number, maxValue: number): number => { const logMax = Math.log10(maxValue); const logMin = minValue > 0 ? Math.log10(minValue) : logMax - 6; return Math.pow(10, logMin + (percentage / 100) * (logMax - logMin)); }, [] ); 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; baseConversionsRef.current = [...conversions]; }, [onValueChange, conversions] ); const handleMouseMove = useCallback( (e: MouseEvent) => { if (!draggingUnit || !activeBarRef.current || !onValueChange) return; const now = Date.now(); if (now - lastUpdateTime.current < 16) return; lastUpdateTime.current = now; const deltaPercentage = ((e.clientX - dragStartX.current) / activeBarRef.current.offsetWidth) * 100; const newPercentage = Math.max(3, Math.min(100, dragStartWidth.current + deltaPercentage)); setDraggedPercentage(newPercentage); const base = baseConversionsRef.current.length > 0 ? baseConversionsRef.current : conversions; const vals = base.map((c) => Math.abs(c.value)); const newValue = calculateValueFromPercentage(newPercentage, Math.min(...vals.filter((v) => v > 0)), Math.max(...vals)); onValueChange(newValue, draggingUnit, true); }, [draggingUnit, conversions, onValueChange, calculateValueFromPercentage] ); const handleMouseUp = useCallback(() => { if (draggingUnit && onValueChange) { const conversion = conversions.find((c) => c.unit === draggingUnit); if (conversion) onValueChange(conversion.value, draggingUnit, false); } setDraggingUnit(null); activeBarRef.current = null; }, [draggingUnit, conversions, onValueChange]); 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; baseConversionsRef.current = [...conversions]; }, [onValueChange, conversions] ); const handleTouchMove = useCallback( (e: TouchEvent) => { if (!draggingUnit || !activeBarRef.current || !onValueChange) return; const now = Date.now(); if (now - lastUpdateTime.current < 16) return; lastUpdateTime.current = now; e.preventDefault(); const touch = e.touches[0]; const deltaPercentage = ((touch.clientX - dragStartX.current) / activeBarRef.current.offsetWidth) * 100; const newPercentage = Math.max(3, Math.min(100, dragStartWidth.current + deltaPercentage)); setDraggedPercentage(newPercentage); const base = baseConversionsRef.current.length > 0 ? baseConversionsRef.current : conversions; const vals = base.map((c) => Math.abs(c.value)); const newValue = calculateValueFromPercentage(newPercentage, Math.min(...vals.filter((v) => v > 0)), Math.max(...vals)); onValueChange(newValue, draggingUnit, true); }, [draggingUnit, conversions, onValueChange, calculateValueFromPercentage] ); const handleTouchEnd = useCallback(() => { if (draggingUnit && onValueChange) { const conversion = conversions.find((c) => c.unit === draggingUnit); if (conversion) onValueChange(conversion.value, draggingUnit, false); } setDraggingUnit(null); activeBarRef.current = null; }, [draggingUnit, conversions, onValueChange]); 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]); useEffect(() => { if (!draggingUnit && draggedPercentage !== null) { 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; const displayPercentage = isDragging && draggedPercentage !== null ? draggedPercentage : item.percentage; return (
{item.unitInfo.plural} {formatNumber(item.value)} {item.unit}
{ 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); }} >
{isDraggable && !isDragging && (
drag
)}
); })}
); }