import React, { useEffect, useMemo, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Loader2, Receipt, Search, X } from 'lucide-react'; import { toast } from 'sonner'; import { api } from '@/api'; import { cn, fmt } from '@/lib/utils'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; function amountSearchText(...values) { return values .filter(value => value !== null && value !== undefined && Number.isFinite(Number(value))) .flatMap(value => { const num = Number(value); return [String(num), num.toFixed(2), `$${num.toFixed(2)}`]; }) .join(' '); } function billSearchText(bill) { return [ bill.name, bill.category_name, bill.notes, bill.billing_cycle, bill.bucket, bill.website, amountSearchText( bill.expected_amount, bill.current_balance, bill.minimum_payment, bill.interest_rate, ), ].filter(Boolean).join(' ').toLowerCase(); } function shortcutLabel() { if (typeof navigator !== 'undefined' && /Mac|iPhone|iPad|iPod/i.test(navigator.platform)) { return 'Cmd K'; } return 'Ctrl K'; } export default function CommandPalette() { const navigate = useNavigate(); const inputRef = useRef(null); const [open, setOpen] = useState(false); const [query, setQuery] = useState(''); const [bills, setBills] = useState([]); const [loading, setLoading] = useState(false); const [loaded, setLoaded] = useState(false); useEffect(() => { const openPalette = () => setOpen(true); const onKeyDown = (event) => { if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'k') { event.preventDefault(); setOpen(value => !value); } }; window.addEventListener('command-palette:open', openPalette); window.addEventListener('keydown', onKeyDown); return () => { window.removeEventListener('command-palette:open', openPalette); window.removeEventListener('keydown', onKeyDown); }; }, []); useEffect(() => { if (!open) return; window.setTimeout(() => inputRef.current?.focus(), 0); if (loaded || loading) return; setLoading(true); api.allBills({ inactive: true }) .then(rows => { setBills(Array.isArray(rows) ? rows : []); setLoaded(true); }) .catch(err => { toast.error(err.message || 'Failed to load bills'); }) .finally(() => setLoading(false)); }, [loaded, loading, open]); const results = useMemo(() => { const q = query.trim().toLowerCase(); const sorted = [...bills].sort((a, b) => { if (!!a.active !== !!b.active) return a.active ? -1 : 1; return String(a.name || '').localeCompare(String(b.name || '')); }); if (!q) return sorted.slice(0, 8); return sorted .filter(bill => billSearchText(bill).includes(q)) .slice(0, 12); }, [bills, query]); const close = () => { setOpen(false); setQuery(''); }; const openBills = (bill) => { const params = new URLSearchParams({ search: bill.name }); if (!bill.active) params.set('inactive', '1'); navigate(`/bills?${params.toString()}`); close(); }; const openTracker = (bill) => { const params = new URLSearchParams({ search: bill.name }); navigate(`/?${params.toString()}`); close(); }; return ( Command palette
setQuery(event.target.value)} onKeyDown={event => { if (event.key === 'Enter' && results[0]) openBills(results[0]); }} placeholder="Find a bill by name, category, notes, or amount" className="h-9 border-0 bg-transparent px-0 text-base shadow-none focus-visible:ring-0" /> {query && ( )}
{loading ? (
Loading bills...
) : results.length > 0 ? (
{results.map(bill => (
))}
) : (
No bills found.
)}
Enter opens Bills {shortcutLabel()}
); }