refactor(ts): CommandPalette → TypeScript (typed Command + Bill results)

This commit is contained in:
null 2026-07-04 22:09:36 -05:00
parent d4eb48de92
commit 2f62d6383a
1 changed files with 31 additions and 18 deletions

View File

@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { useEffect, useMemo, useRef, useState, type ComponentType } from 'react';
import { useNavigate } from 'react-router-dom';
import {
BarChart2, Calendar, CreditCard, Loader2, Navigation, Plus,
@ -7,20 +7,29 @@ import {
} from 'lucide-react';
import { toast } from 'sonner';
import { api } from '@/api';
import { cn, fmt } from '@/lib/utils';
import { cn, fmt, errMessage } from '@/lib/utils';
import { scheduleLabel, scheduleValue } from '@/lib/billingSchedule';
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import type { Bill } from '@/types';
// ─── Navigation commands ──────────────────────────────────────────────────────
interface Command {
id: string;
label: string;
icon: ComponentType<{ className?: string }>;
path: string;
group: string;
}
const MONTHS = [
'January','February','March','April','May','June',
'July','August','September','October','November','December',
];
const NAV_COMMANDS = [
const NAV_COMMANDS: Command[] = [
{ id: 'nav-tracker', label: 'Go to Tracker', icon: Receipt, path: '/', group: 'Navigate' },
{ id: 'nav-bills', label: 'Go to Bills', icon: CreditCard, path: '/bills', group: 'Navigate' },
{ id: 'nav-calendar', label: 'Go to Calendar', icon: Calendar, path: '/calendar', group: 'Navigate' },
@ -39,10 +48,10 @@ const NAV_COMMANDS = [
];
// Generate jump-to-month commands for the current year ± 1
function buildMonthCommands() {
function buildMonthCommands(): Command[] {
const now = new Date();
const year = now.getFullYear();
const commands = [];
const commands: Command[] = [];
for (let y = year - 1; y <= year + 1; y++) {
for (let m = 1; m <= 12; m++) {
commands.push({
@ -59,7 +68,7 @@ function buildMonthCommands() {
// ─── Helpers ──────────────────────────────────────────────────────────────────
function amountSearchText(...values) {
function amountSearchText(...values: unknown[]): string {
return values
.filter(value => value !== null && value !== undefined && Number.isFinite(Number(value)))
.flatMap(value => {
@ -69,7 +78,7 @@ function amountSearchText(...values) {
.join(' ');
}
function billSearchText(bill) {
function billSearchText(bill: Bill): string {
return [
bill.name,
bill.category_name,
@ -87,7 +96,7 @@ function billSearchText(bill) {
].filter(Boolean).join(' ').toLowerCase();
}
function shortcutLabel() {
function shortcutLabel(): string {
if (typeof navigator !== 'undefined' && /Mac|iPhone|iPad|iPod/i.test(navigator.platform)) {
return 'Cmd K';
}
@ -96,7 +105,11 @@ function shortcutLabel() {
// ─── Result item components ───────────────────────────────────────────────────
function BillResult({ bill, onOpenBills, onOpenTracker }) {
function BillResult({ bill, onOpenBills, onOpenTracker }: {
bill: Bill;
onOpenBills: (bill: Bill) => void;
onOpenTracker: (bill: Bill) => void;
}) {
return (
<div
className={cn(
@ -124,7 +137,7 @@ function BillResult({ bill, onOpenBills, onOpenTracker }) {
<span className="mt-0.5 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-muted-foreground">
<span>{bill.category_name || 'Uncategorized'}</span>
<span>Due {bill.due_day}</span>
<span>{fmt(bill.expected_amount || 0)}</span>
<span>{fmt(bill.expected_amount)}</span>
</span>
</span>
</button>
@ -140,7 +153,7 @@ function BillResult({ bill, onOpenBills, onOpenTracker }) {
);
}
function CommandResult({ cmd, onRun }) {
function CommandResult({ cmd, onRun }: { cmd: Command; onRun: (cmd: Command) => void }) {
const Icon = cmd.icon || Navigation;
return (
<button
@ -166,17 +179,17 @@ function CommandResult({ cmd, onRun }) {
export default function CommandPalette() {
const navigate = useNavigate();
const inputRef = useRef(null);
const inputRef = useRef<HTMLInputElement>(null);
const [open, setOpen] = useState(false);
const [query, setQuery] = useState('');
const [bills, setBills] = useState([]);
const [bills, setBills] = useState<Bill[]>([]);
const [loading, setLoading] = useState(false);
const [loaded, setLoaded] = useState(false);
const monthCommands = useMemo(() => buildMonthCommands(), []);
useEffect(() => {
const openPalette = () => setOpen(true);
const onKeyDown = (event) => {
const onKeyDown = (event: KeyboardEvent) => {
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'k') {
event.preventDefault();
setOpen(value => !value);
@ -203,7 +216,7 @@ export default function CommandPalette() {
setLoaded(true);
})
.catch(err => {
toast.error(err.message || 'Failed to load bills');
toast.error(errMessage(err, 'Failed to load bills'));
})
.finally(() => setLoading(false));
}, [loaded, loading, open]);
@ -213,20 +226,20 @@ export default function CommandPalette() {
setQuery('');
};
const openBills = (bill) => {
const openBills = (bill: Bill) => {
const params = new URLSearchParams({ search: bill.name });
if (!bill.active) params.set('inactive', '1');
navigate(`/bills?${params.toString()}`);
close();
};
const openTracker = (bill) => {
const openTracker = (bill: Bill) => {
const params = new URLSearchParams({ search: bill.name });
navigate(`/?${params.toString()}`);
close();
};
const runCommand = (cmd) => {
const runCommand = (cmd: Command) => {
navigate(cmd.path);
close();
};