feat: add media converter app and fix compilation errors
This commit is contained in:
@@ -14,7 +14,7 @@ import {
|
||||
} from '@/components/ui/select';
|
||||
import SearchUnits from './SearchUnits';
|
||||
import VisualComparison from './VisualComparison';
|
||||
import CommandPalette from '@/components/units/ui/CommandPalette';
|
||||
|
||||
import {
|
||||
getAllMeasures,
|
||||
getUnitsForMeasure,
|
||||
@@ -118,11 +118,6 @@ export default function MainConverter() {
|
||||
|
||||
return (
|
||||
<div className="w-full space-y-8">
|
||||
{/* Command Palette */}
|
||||
<CommandPalette
|
||||
onSelectMeasure={setSelectedMeasure}
|
||||
onSelectUnit={handleSearchSelect}
|
||||
/>
|
||||
|
||||
{/* Quick Access Row */}
|
||||
<div className="flex flex-col md:flex-row md:items-center gap-4 justify-between bg-card p-4 rounded-lg border">
|
||||
@@ -1,77 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
|
||||
type Theme = 'dark' | 'light' | 'system';
|
||||
|
||||
interface ThemeProviderProps {
|
||||
children: React.ReactNode;
|
||||
defaultTheme?: Theme;
|
||||
storageKey?: string;
|
||||
}
|
||||
|
||||
interface ThemeProviderState {
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
}
|
||||
|
||||
const ThemeProviderContext = createContext<ThemeProviderState | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
defaultTheme = 'system',
|
||||
storageKey = 'units-ui-theme',
|
||||
}: ThemeProviderProps) {
|
||||
const [theme, setTheme] = useState<Theme>(defaultTheme);
|
||||
|
||||
useEffect(() => {
|
||||
// Load theme from localStorage
|
||||
const stored = localStorage.getItem(storageKey) as Theme | null;
|
||||
if (stored) {
|
||||
setTheme(stored);
|
||||
}
|
||||
}, [storageKey]);
|
||||
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement;
|
||||
|
||||
root.classList.remove('light', 'dark');
|
||||
|
||||
if (theme === 'system') {
|
||||
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
.matches
|
||||
? 'dark'
|
||||
: 'light';
|
||||
|
||||
root.classList.add(systemTheme);
|
||||
return;
|
||||
}
|
||||
|
||||
root.classList.add(theme);
|
||||
}, [theme]);
|
||||
|
||||
const value = {
|
||||
theme,
|
||||
setTheme: (theme: Theme) => {
|
||||
localStorage.setItem(storageKey, theme);
|
||||
setTheme(theme);
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProviderContext.Provider value={value}>
|
||||
{children}
|
||||
</ThemeProviderContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useTheme = () => {
|
||||
const context = useContext(ThemeProviderContext);
|
||||
|
||||
if (context === undefined)
|
||||
throw new Error('useTheme must be used within a ThemeProvider');
|
||||
|
||||
return context;
|
||||
};
|
||||
@@ -1,174 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useMemo, useRef } from 'react';
|
||||
import { Command, Hash, Star, Moon, Sun } from 'lucide-react';
|
||||
import { useTheme } from '@/components/providers/ThemeProvider';
|
||||
import {
|
||||
getAllMeasures,
|
||||
formatMeasureName,
|
||||
getCategoryColor,
|
||||
getCategoryColorHex,
|
||||
type Measure,
|
||||
} from '@/lib/units/units';
|
||||
import { getFavorites } from '@/lib/units/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<HTMLInputElement>(null);
|
||||
|
||||
// Commands
|
||||
const commands: Array<{
|
||||
id: string;
|
||||
label: string;
|
||||
icon: any;
|
||||
action: () => void;
|
||||
keywords: string[];
|
||||
color?: string;
|
||||
}> = [
|
||||
{
|
||||
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: getCategoryColorHex(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;
|
||||
|
||||
// Focus input when opened
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
inputRef.current?.focus();
|
||||
setQuery('');
|
||||
setSelectedIndex(0);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
// Reset selected index when query changes
|
||||
useEffect(() => {
|
||||
setSelectedIndex(0);
|
||||
}, [query]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="fixed inset-0 bg-background/80 backdrop-blur-sm z-50"
|
||||
onClick={() => setIsOpen(false)}
|
||||
/>
|
||||
|
||||
{/* Command Palette */}
|
||||
<div className="fixed left-1/2 top-1/4 -translate-x-1/2 w-full max-w-2xl z-50 animate-scale-in">
|
||||
<div className="bg-popover border rounded-lg shadow-2xl overflow-hidden">
|
||||
{/* Search Input */}
|
||||
<div className="flex items-center border-b px-4">
|
||||
<Command className="h-5 w-5 text-muted-foreground" />
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
placeholder="Type a command or search..."
|
||||
value={query}
|
||||
onChange={e => setQuery(e.target.value)}
|
||||
className="flex-1 bg-transparent py-4 px-4 outline-none placeholder:text-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Commands List */}
|
||||
<div className="max-h-96 overflow-y-auto p-2">
|
||||
{filteredCommands.length === 0 ? (
|
||||
<div className="py-8 text-center text-muted-foreground">
|
||||
No commands found
|
||||
</div>
|
||||
) : (
|
||||
filteredCommands.map((command, index) => {
|
||||
const Icon = command.icon;
|
||||
return (
|
||||
<button
|
||||
key={command.id}
|
||||
onClick={() => {
|
||||
command.action();
|
||||
setIsOpen(false);
|
||||
}}
|
||||
className={cn(
|
||||
'w-full flex items-center gap-3 px-4 py-3 rounded-md transition-colors text-left',
|
||||
index === selectedIndex
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'hover:bg-accent/50'
|
||||
)}
|
||||
>
|
||||
{command.color ? (
|
||||
<div
|
||||
className="w-5 h-5 rounded flex-shrink-0"
|
||||
style={{
|
||||
backgroundColor: command.color,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Icon className="h-5 w-5 flex-shrink-0 text-muted-foreground" />
|
||||
)}
|
||||
<span className="flex-1">{command.label}</span>
|
||||
</button>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="border-t px-4 py-2 text-xs text-muted-foreground flex items-center gap-4">
|
||||
<span>Navigate with arrows</span>
|
||||
<span>Select with Enter</span>
|
||||
<span>Close with click outside</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user