Milestone 2: the full picture (20261007.12)
Budgets: one amount per category for every month with any month set on its own, filled from recent spending in one step and copied into the next year. Home shows what's left to spend, year pacing, insights, balances and upcoming charges. Insights covers year pacing, money in and out, budget against actual, categories by month, merchants, balances over time and tags. Subscriptions and bills are found across every account (rhythm and price matching plus a built-in catalog), confirmed with one tap, and watched for price changes, missed charges, charges after cancelling, double billing, renewals and trials. Alerts are worked out again whenever the data changes. Also: category, rule, tag and alert settings; Excel and CSV export from a small built-in writer; the AI module designed in and switched off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
46
web/src/components/AlertCard.tsx
Normal file
46
web/src/components/AlertCard.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { AlertItem } from '@spendlight/shared';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { ArrowRight, CircleAlert, Lightbulb, PartyPopper, TriangleAlert, X } from 'lucide-react';
|
||||
import { Link } from 'react-router';
|
||||
import { api } from '@/lib/api';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const LOOK = {
|
||||
bad: { icon: CircleAlert, tone: 'bg-bad/12 text-bad' },
|
||||
warn: { icon: TriangleAlert, tone: 'bg-warn/14 text-warn' },
|
||||
good: { icon: PartyPopper, tone: 'bg-good/12 text-good' },
|
||||
info: { icon: Lightbulb, tone: 'bg-accent text-accent-foreground' },
|
||||
} as const;
|
||||
|
||||
/** One insight or alert: what happened, why it matters, and a way to see the transactions. */
|
||||
export function AlertCard({ alert, compact }: { alert: AlertItem; compact?: boolean }) {
|
||||
const client = useQueryClient();
|
||||
const dismiss = useMutation({
|
||||
mutationFn: () => api(`/alerts/${alert.id}/dismiss`, { method: 'POST' }),
|
||||
onSuccess: () => client.invalidateQueries({ queryKey: ['alerts'] }),
|
||||
});
|
||||
const { icon: Icon, tone } = LOOK[alert.severity];
|
||||
return (
|
||||
<div className={cn('group relative flex gap-3.5 rounded-2xl border bg-card p-4 shadow-soft', compact && 'p-3.5')}>
|
||||
<span className={cn('flex size-9 shrink-0 items-center justify-center rounded-xl [&_svg]:size-[18px]', tone)}>
|
||||
<Icon strokeWidth={2} />
|
||||
</span>
|
||||
<div className="min-w-0 flex-1 pr-6">
|
||||
<p className="text-[14.5px] font-medium leading-snug">{alert.title}</p>
|
||||
{alert.body ? <p className="mt-0.5 text-[13px] leading-relaxed text-muted-foreground">{alert.body}</p> : null}
|
||||
{alert.link ? (
|
||||
<Link to={alert.link} className="mt-1.5 inline-flex items-center gap-1 text-[13px] font-medium text-primary">
|
||||
See {alert.link.startsWith('/activity') ? 'transactions' : 'details'} <ArrowRight className="size-3.5" />
|
||||
</Link>
|
||||
) : null}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => dismiss.mutate()}
|
||||
className="absolute right-2.5 top-2.5 flex size-8 items-center justify-center rounded-lg text-muted-foreground opacity-70 transition hover:bg-muted hover:opacity-100"
|
||||
aria-label="Dismiss"
|
||||
>
|
||||
<X className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user