feat: implement Phase 4 - Command palette, visual comparison, and polish

Add final layer of advanced features for spectacular UX:

⌨️ Command Palette (CommandPalette.tsx):
- Trigger with Ctrl/Cmd+K keyboard shortcut
- Search and execute commands instantly
- Quick access to all 23 measurement categories
- Theme switching commands (light/dark/system)
- Keyboard navigation (↑/↓ arrows, Enter to select)
- Escape to close
- Color-coded category commands
- Backdrop blur overlay
- Animated scale-in entrance
- Footer with keyboard hints
- Smart filtering by keywords

📊 Visual Comparison (VisualComparison.tsx):
- Toggle between grid and chart view
- Horizontal bar chart showing magnitude differences
- Animated bars with 500ms transitions
- Auto-calculated percentages relative to max value
- Color-coded bars matching category colors
- Tabular number formatting
- Clean, minimal design

🔄 Unit Swap Functionality:
- Swap button between From/To units
- ArrowLeftRight icon
- Automatically converts value on swap
- Two-unit quick converter mode
- Large result display with color accent
- Responsive layout

📱 Footer Component (Footer.tsx):
- Three-column grid layout (About, Features, Links)
- GitHub repository link
- convert-units library attribution
- Keyboard shortcuts reference
- Feature highlights
- Made with ❤️ message
- Responsive design (stacks on mobile)
- Copyright notice with current year
- Smooth hover transitions

🎨 Enhanced MainConverter:
- From/To unit selector with swap button
- Quick result display between selectors
- Toggle between grid and chart views
- BarChart3 icon for view switcher
- Integrated command palette
- Better spacing and layout
- Target unit state management
- Auto-update target unit on measure change

 Enhanced Page Layout:
- Sticky header with backdrop blur
- Flexbox layout for footer at bottom
- Keyboard shortcuts hint (/ and Ctrl+K)
- Improved header spacing
- Better visual hierarchy

Features Now Complete:
 Command palette with Ctrl+K
 Visual comparison bar charts
 Unit swap functionality
 Professional footer
 From/To quick converter
 Chart/Grid view toggle
 Sticky navigation header
 Full keyboard navigation

The app is now feature-complete with:
- 23 measurement categories
- 187 individual units
- Real-time conversion
- Fuzzy search (/)
- Command palette (Ctrl+K)
- Dark mode
- Conversion history
- Favorites & copy
- Visual comparisons
- Unit swapping
- Complete footer

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-08 10:20:32 +01:00
parent 5a7bb9a05c
commit 7f7bc69d04
5 changed files with 477 additions and 14 deletions

View File

@@ -1,12 +1,14 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { Copy, Star, Check } from 'lucide-react';
import { Copy, Star, Check, ArrowLeftRight, BarChart3 } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import SearchUnits from './SearchUnits';
import ConversionHistory from './ConversionHistory';
import VisualComparison from './VisualComparison';
import CommandPalette from '@/components/ui/CommandPalette';
import {
getAllMeasures,
getUnitsForMeasure,
@@ -23,10 +25,12 @@ import { saveToHistory, getFavorites, toggleFavorite } from '@/lib/storage';
export default function MainConverter() {
const [selectedMeasure, setSelectedMeasure] = useState<Measure>('length');
const [selectedUnit, setSelectedUnit] = useState<string>('m');
const [targetUnit, setTargetUnit] = useState<string>('ft');
const [inputValue, setInputValue] = useState<string>('1');
const [conversions, setConversions] = useState<ConversionResult[]>([]);
const [favorites, setFavorites] = useState<string[]>([]);
const [copiedUnit, setCopiedUnit] = useState<string | null>(null);
const [showVisualComparison, setShowVisualComparison] = useState(false);
const measures = getAllMeasures();
const units = getUnitsForMeasure(selectedMeasure);
@@ -52,9 +56,24 @@ export default function MainConverter() {
const availableUnits = getUnitsForMeasure(selectedMeasure);
if (availableUnits.length > 0) {
setSelectedUnit(availableUnits[0]);
setTargetUnit(availableUnits[1] || availableUnits[0]);
}
}, [selectedMeasure]);
// Swap units
const handleSwapUnits = useCallback(() => {
const temp = selectedUnit;
setSelectedUnit(targetUnit);
setTargetUnit(temp);
// Convert the value
const numValue = parseNumberInput(inputValue);
if (numValue !== null) {
const converted = convertUnit(numValue, selectedUnit, targetUnit);
setInputValue(converted.toString());
}
}, [selectedUnit, targetUnit, inputValue]);
// Copy to clipboard
const copyToClipboard = useCallback(async (value: number, unit: string) => {
try {
@@ -105,6 +124,12 @@ export default function MainConverter() {
return (
<div className="w-full max-w-6xl mx-auto space-y-6">
{/* Command Palette */}
<CommandPalette
onSelectMeasure={setSelectedMeasure}
onSelectUnit={handleSearchSelect}
/>
{/* Search */}
<div className="flex justify-center">
<SearchUnits onSelectUnit={handleSearchSelect} />
@@ -144,7 +169,7 @@ export default function MainConverter() {
<CardTitle>Convert {formatMeasureName(selectedMeasure)}</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex gap-4">
<div className="flex gap-2 items-end">
<div className="flex-1">
<label className="text-sm font-medium mb-2 block">Value</label>
<Input
@@ -156,8 +181,8 @@ export default function MainConverter() {
className="text-lg"
/>
</div>
<div className="w-48">
<label className="text-sm font-medium mb-2 block">From Unit</label>
<div className="w-40">
<label className="text-sm font-medium mb-2 block">From</label>
<select
value={selectedUnit}
onChange={(e) => setSelectedUnit(e.target.value)}
@@ -170,18 +195,69 @@ export default function MainConverter() {
))}
</select>
</div>
<Button
variant="outline"
size="icon"
onClick={handleSwapUnits}
className="flex-shrink-0"
title="Swap units"
>
<ArrowLeftRight className="h-4 w-4" />
</Button>
<div className="w-40">
<label className="text-sm font-medium mb-2 block">To</label>
<select
value={targetUnit}
onChange={(e) => setTargetUnit(e.target.value)}
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
{units.map((unit) => (
<option key={unit} value={unit}>
{unit}
</option>
))}
</select>
</div>
</div>
{/* Quick result */}
{parseNumberInput(inputValue) !== null && (
<div className="p-4 rounded-lg bg-accent/50 border-l-4" style={{
borderLeftColor: `var(--color-${getCategoryColor(selectedMeasure)})`,
}}>
<div className="text-sm text-muted-foreground">Result</div>
<div className="text-3xl font-bold mt-1">
{formatNumber(convertUnit(parseNumberInput(inputValue)!, selectedUnit, targetUnit))} {targetUnit}
</div>
</div>
)}
</CardContent>
</Card>
{/* Results */}
<Card>
<CardHeader>
<CardTitle>Conversions</CardTitle>
<div className="flex items-center justify-between">
<CardTitle>All Conversions</CardTitle>
<Button
variant="outline"
size="sm"
onClick={() => setShowVisualComparison(!showVisualComparison)}
>
<BarChart3 className="h-4 w-4 mr-2" />
{showVisualComparison ? 'Grid View' : 'Chart View'}
</Button>
</div>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{conversions.map((conversion) => {
{showVisualComparison ? (
<VisualComparison
conversions={conversions}
color={getCategoryColor(selectedMeasure)}
/>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{conversions.map((conversion) => {
const isFavorite = favorites.includes(conversion.unit);
const isCopied = copiedUnit === conversion.unit;
@@ -235,7 +311,8 @@ export default function MainConverter() {
</div>
);
})}
</div>
</div>
)}
</CardContent>
</Card>

View File

@@ -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 (
<div className="text-center py-8 text-muted-foreground">
Enter a value to see conversions
</div>
);
}
return (
<div className="space-y-3">
{withPercentages.map(item => (
<div key={item.unit} className="space-y-1">
<div className="flex items-center justify-between text-sm">
<span className="font-medium">{item.unitInfo.plural}</span>
<span className="tabular-nums">
{formatNumber(item.value)} {item.unit}
</span>
</div>
<div className="h-2 bg-muted rounded-full overflow-hidden">
<div
className="h-full rounded-full transition-all duration-500 ease-out"
style={{
width: `${item.percentage}%`,
backgroundColor: `var(--color-${color})`,
}}
/>
</div>
</div>
))}
</div>
);
}