refactor(ts): CommandPalette → TypeScript (typed Command + Bill results)
This commit is contained in:
parent
d4eb48de92
commit
2f62d6383a
|
|
@ -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();
|
||||
};
|
||||
Loading…
Reference in New Issue