+
-
-
+
+
+
+
+
+
+
+
+ {/* Quick result */}
+ {parseNumberInput(inputValue) !== null && (
+
+
Result
+
+ {formatNumber(convertUnit(parseNumberInput(inputValue)!, selectedUnit, targetUnit))} {targetUnit}
+
+
+ )}
{/* Results */}
- Conversions
+
+ All Conversions
+
+
-
- {conversions.map((conversion) => {
+ {showVisualComparison ? (
+
+ ) : (
+
+ {conversions.map((conversion) => {
const isFavorite = favorites.includes(conversion.unit);
const isCopied = copiedUnit === conversion.unit;
@@ -235,7 +311,8 @@ export default function MainConverter() {
);
})}
-
+
+ )}
diff --git a/components/converter/VisualComparison.tsx b/components/converter/VisualComparison.tsx
new file mode 100644
index 0000000..024981a
--- /dev/null
+++ b/components/converter/VisualComparison.tsx
@@ -0,0 +1,60 @@
+'use client';
+
+import { useMemo } from 'react';
+import { type ConversionResult } from '@/lib/units';
+import { formatNumber, cn } from '@/lib/utils';
+
+interface VisualComparisonProps {
+ conversions: ConversionResult[];
+ color: string;
+}
+
+export default function VisualComparison({
+ conversions,
+ color,
+}: VisualComparisonProps) {
+ // Calculate percentages for visual bars
+ const withPercentages = useMemo(() => {
+ if (conversions.length === 0) return [];
+
+ const maxValue = Math.max(...conversions.map(c => Math.abs(c.value)));
+ if (maxValue === 0) return conversions.map(c => ({ ...c, percentage: 0 }));
+
+ return conversions.map(c => ({
+ ...c,
+ percentage: (Math.abs(c.value) / maxValue) * 100,
+ }));
+ }, [conversions]);
+
+ if (conversions.length === 0) {
+ return (
+
+ Enter a value to see conversions
+
+ );
+ }
+
+ return (
+
+ {withPercentages.map(item => (
+
+
+ {item.unitInfo.plural}
+
+ {formatNumber(item.value)} {item.unit}
+
+
+
+
+ ))}
+
+ );
+}
diff --git a/components/ui/CommandPalette.tsx b/components/ui/CommandPalette.tsx
new file mode 100644
index 0000000..0a0899f
--- /dev/null
+++ b/components/ui/CommandPalette.tsx
@@ -0,0 +1,223 @@
+'use client';
+
+import { useState, useEffect, useCallback, useRef } from 'react';
+import { Command, Hash, Clock, Star, Moon, Sun } from 'lucide-react';
+import { useTheme } from '@/components/providers/ThemeProvider';
+import {
+ getAllMeasures,
+ formatMeasureName,
+ getCategoryColor,
+ type Measure,
+} from '@/lib/units';
+import { getHistory, getFavorites } from '@/lib/storage';
+import { cn } from '@/lib/utils';
+
+interface CommandPaletteProps {
+ onSelectMeasure: (measure: Measure) => void;
+ onSelectUnit: (unit: string, measure: Measure) => void;
+}
+
+export default function CommandPalette({
+ onSelectMeasure,
+ onSelectUnit,
+}: CommandPaletteProps) {
+ const [isOpen, setIsOpen] = useState(false);
+ const [query, setQuery] = useState('');
+ const [selectedIndex, setSelectedIndex] = useState(0);
+ const { theme, setTheme } = useTheme();
+ const inputRef = useRef
(null);
+
+ // Commands
+ const commands = [
+ {
+ id: 'theme-light',
+ label: 'Switch to Light Mode',
+ icon: Sun,
+ action: () => setTheme('light'),
+ keywords: ['theme', 'light', 'mode'],
+ },
+ {
+ id: 'theme-dark',
+ label: 'Switch to Dark Mode',
+ icon: Moon,
+ action: () => setTheme('dark'),
+ keywords: ['theme', 'dark', 'mode'],
+ },
+ {
+ id: 'theme-system',
+ label: 'Use System Theme',
+ icon: Command,
+ action: () => setTheme('system'),
+ keywords: ['theme', 'system', 'auto'],
+ },
+ ];
+
+ // Add measure commands
+ const measures = getAllMeasures();
+ const measureCommands = measures.map(measure => ({
+ id: `measure-${measure}`,
+ label: `Convert ${formatMeasureName(measure)}`,
+ icon: Hash,
+ action: () => onSelectMeasure(measure),
+ keywords: ['convert', measure, formatMeasureName(measure).toLowerCase()],
+ color: getCategoryColor(measure),
+ }));
+
+ const allCommands = [...commands, ...measureCommands];
+
+ // Filter commands
+ const filteredCommands = query
+ ? allCommands.filter(cmd =>
+ cmd.keywords.some(kw => kw.toLowerCase().includes(query.toLowerCase())) ||
+ cmd.label.toLowerCase().includes(query.toLowerCase())
+ )
+ : allCommands;
+
+ // Keyboard shortcut to open
+ useEffect(() => {
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
+ e.preventDefault();
+ setIsOpen(prev => !prev);
+ }
+
+ if (e.key === 'Escape') {
+ setIsOpen(false);
+ }
+ };
+
+ document.addEventListener('keydown', handleKeyDown);
+ return () => document.removeEventListener('keydown', handleKeyDown);
+ }, []);
+
+ // Focus input when opened
+ useEffect(() => {
+ if (isOpen) {
+ inputRef.current?.focus();
+ setQuery('');
+ setSelectedIndex(0);
+ }
+ }, [isOpen]);
+
+ // Keyboard navigation
+ useEffect(() => {
+ if (!isOpen) return;
+
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'ArrowDown') {
+ e.preventDefault();
+ setSelectedIndex(prev =>
+ prev < filteredCommands.length - 1 ? prev + 1 : prev
+ );
+ } else if (e.key === 'ArrowUp') {
+ e.preventDefault();
+ setSelectedIndex(prev => (prev > 0 ? prev - 1 : prev));
+ } else if (e.key === 'Enter') {
+ e.preventDefault();
+ const command = filteredCommands[selectedIndex];
+ if (command) {
+ command.action();
+ setIsOpen(false);
+ }
+ }
+ };
+
+ document.addEventListener('keydown', handleKeyDown);
+ return () => document.removeEventListener('keydown', handleKeyDown);
+ }, [isOpen, selectedIndex, filteredCommands]);
+
+ // Reset selected index when query changes
+ useEffect(() => {
+ setSelectedIndex(0);
+ }, [query]);
+
+ if (!isOpen) return null;
+
+ return (
+ <>
+ {/* Backdrop */}
+ setIsOpen(false)}
+ />
+
+ {/* Command Palette */}
+
+
+ {/* Search Input */}
+
+
+ setQuery(e.target.value)}
+ className="flex-1 bg-transparent py-4 px-4 outline-none placeholder:text-muted-foreground"
+ />
+
+ ESC
+
+
+
+ {/* Commands List */}
+
+ {filteredCommands.length === 0 ? (
+
+ No commands found
+
+ ) : (
+ filteredCommands.map((command, index) => {
+ const Icon = command.icon;
+ return (
+
+ );
+ })
+ )}
+
+
+ {/* Footer */}
+
+
+ ↑
+ ↓
+ Navigate
+
+
+ Enter
+ Select
+
+
+ ESC
+ Close
+
+
+
+
+ >
+ );
+}
diff --git a/components/ui/Footer.tsx b/components/ui/Footer.tsx
new file mode 100644
index 0000000..edabae4
--- /dev/null
+++ b/components/ui/Footer.tsx
@@ -0,0 +1,94 @@
+import { Heart, Github, Code2 } from 'lucide-react';
+
+export default function Footer() {
+ const currentYear = new Date().getFullYear();
+
+ return (
+
+ );
+}