style: SubscriptionsPage layout and responsiveness pass

- Page width consistent with rest of app
- Subscription/recommendation names no longer overflow
- Improved mobile/tablet wrapping for rows, amounts, action buttons
- Two-column layout delayed until very wide screens
- Added missing labels for food, education, shopping, security types
This commit is contained in:
null 2026-05-29 03:49:36 -05:00
parent 6b30ee4eb7
commit c3c0ab3542
2 changed files with 27 additions and 19 deletions

View File

@ -35,6 +35,10 @@ const TYPE_LABELS = {
gaming: 'Gaming', gaming: 'Gaming',
utilities: 'Utilities', utilities: 'Utilities',
insurance: 'Insurance', insurance: 'Insurance',
food: 'Food',
education: 'Education',
shopping: 'Shopping',
security: 'Security',
other: 'Other', other: 'Other',
}; };
@ -45,12 +49,12 @@ function cycleLabel(item) {
function StatCard({ icon: Icon, label, value, hint }) { function StatCard({ icon: Icon, label, value, hint }) {
return ( return (
<div className="rounded-lg border border-border/70 bg-card/80 p-4"> <div className="min-w-0 rounded-lg border border-border/70 bg-card/80 p-4">
<div className="flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-foreground/70"> <div className="flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-foreground/70">
<Icon className="h-4 w-4 text-primary" /> <Icon className="h-4 w-4 text-primary" />
{label} <span className="truncate">{label}</span>
</div> </div>
<p className="tracker-number mt-2 text-2xl font-bold tracking-tight text-foreground">{value}</p> <p className="tracker-number mt-2 truncate text-2xl font-bold tracking-tight text-foreground">{value}</p>
{hint && <p className="mt-1 text-xs font-medium text-muted-foreground">{hint}</p>} {hint && <p className="mt-1 text-xs font-medium text-muted-foreground">{hint}</p>}
</div> </div>
); );
@ -59,7 +63,8 @@ function StatCard({ icon: Icon, label, value, hint }) {
function SubscriptionRow({ item, onEdit, onToggle }) { function SubscriptionRow({ item, onEdit, onToggle }) {
return ( return (
<div className={cn( <div className={cn(
'flex flex-col gap-3 border-b border-border/40 px-4 py-4 last:border-b-0 sm:flex-row sm:items-center', 'grid min-w-0 gap-3 border-b border-border/40 px-4 py-4 last:border-b-0',
'md:grid-cols-[minmax(0,1fr)_14rem_auto] md:items-center',
!item.active && 'opacity-60', !item.active && 'opacity-60',
)}> )}>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
@ -67,7 +72,7 @@ function SubscriptionRow({ item, onEdit, onToggle }) {
<button <button
type="button" type="button"
onClick={() => onEdit(item)} onClick={() => onEdit(item)}
className="truncate text-left text-[15px] font-semibold text-foreground hover:text-primary" className="min-w-0 max-w-full truncate text-left text-[15px] font-semibold text-foreground hover:text-primary"
> >
{item.name} {item.name}
</button> </button>
@ -88,7 +93,7 @@ function SubscriptionRow({ item, onEdit, onToggle }) {
</div> </div>
</div> </div>
<div className="grid grid-cols-2 gap-3 sm:w-[260px]"> <div className="grid grid-cols-2 gap-3 md:w-56">
<div> <div>
<p className="text-xs text-muted-foreground">Per cycle</p> <p className="text-xs text-muted-foreground">Per cycle</p>
<p className="tracker-number text-sm font-semibold text-foreground">{fmt(item.expected_amount)}</p> <p className="tracker-number text-sm font-semibold text-foreground">{fmt(item.expected_amount)}</p>
@ -99,15 +104,18 @@ function SubscriptionRow({ item, onEdit, onToggle }) {
</div> </div>
</div> </div>
<div className="flex gap-2 sm:justify-end"> <div className="flex gap-2 md:justify-end">
<Button type="button" variant="outline" size="sm" onClick={() => onEdit(item)}> <Button type="button" variant="outline" size="sm" className="flex-1 md:flex-none" onClick={() => onEdit(item)}>
Edit Edit
</Button> </Button>
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
size="sm" size="sm"
className={item.active ? 'text-amber-300 hover:bg-amber-500/10 hover:text-amber-200' : 'text-emerald-300 hover:bg-emerald-500/10 hover:text-emerald-200'} className={cn(
'flex-1 md:flex-none',
item.active ? 'text-amber-300 hover:bg-amber-500/10 hover:text-amber-200' : 'text-emerald-300 hover:bg-emerald-500/10 hover:text-emerald-200',
)}
onClick={() => onToggle(item)} onClick={() => onToggle(item)}
> >
{item.active ? 'Pause' : 'Resume'} {item.active ? 'Pause' : 'Resume'}
@ -189,8 +197,8 @@ function RecommendationCard({ recommendation, categoryId, onAccept, onDecline, o
<div className="rounded-lg border border-primary/20 bg-primary/[0.05] p-4"> <div className="rounded-lg border border-primary/20 bg-primary/[0.05] p-4">
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div className="min-w-0"> <div className="min-w-0">
<div className="flex flex-wrap items-center gap-2"> <div className="flex min-w-0 flex-wrap items-center gap-2">
<p className="truncate text-sm font-semibold text-foreground">{recommendation.name}</p> <p className="min-w-0 max-w-full truncate text-sm font-semibold text-foreground">{recommendation.name}</p>
<Badge variant="outline" className="border-primary/25 bg-primary/10 text-primary"> <Badge variant="outline" className="border-primary/25 bg-primary/10 text-primary">
{recommendation.confidence}% match {recommendation.confidence}% match
</Badge> </Badge>
@ -211,7 +219,7 @@ function RecommendationCard({ recommendation, categoryId, onAccept, onDecline, o
<p className="tracker-number text-xs font-semibold text-emerald-600 dark:text-emerald-300"> <p className="tracker-number text-xs font-semibold text-emerald-600 dark:text-emerald-300">
{fmt(recommendation.monthly_equivalent)} / mo {fmt(recommendation.monthly_equivalent)} / mo
</p> </p>
<div className="mt-3 flex flex-wrap gap-2 sm:justify-end"> <div className="mt-3 grid grid-cols-1 gap-2 min-[380px]:grid-cols-3 sm:flex sm:flex-wrap sm:justify-end">
<Button <Button
type="button" type="button"
size="sm" size="sm"
@ -383,9 +391,9 @@ export default function SubscriptionsPage() {
const paused = subscriptions.filter(item => !item.active); const paused = subscriptions.filter(item => !item.active);
return ( return (
<div className="space-y-6"> <div className="mx-auto w-full max-w-6xl space-y-6">
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between"> <div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
<div> <div className="min-w-0">
<p className="mb-1 text-[11px] font-semibold uppercase tracking-[0.15em] text-muted-foreground"> <p className="mb-1 text-[11px] font-semibold uppercase tracking-[0.15em] text-muted-foreground">
Recurring Services Recurring Services
</p> </p>
@ -394,7 +402,7 @@ export default function SubscriptionsPage() {
Track manual subscriptions and review recurring SimpleFIN charges. Track manual subscriptions and review recurring SimpleFIN charges.
</p> </p>
</div> </div>
<div className="flex flex-wrap gap-2"> <div className="grid grid-cols-2 gap-2 sm:flex sm:flex-wrap">
<Button type="button" variant="outline" className="gap-2" onClick={refreshAll} disabled={loading || recommendationsLoading}> <Button type="button" variant="outline" className="gap-2" onClick={refreshAll} disabled={loading || recommendationsLoading}>
<RefreshCw className={cn('h-4 w-4', (loading || recommendationsLoading) && 'animate-spin')} /> <RefreshCw className={cn('h-4 w-4', (loading || recommendationsLoading) && 'animate-spin')} />
Refresh Refresh
@ -413,9 +421,9 @@ export default function SubscriptionsPage() {
<StatCard icon={Cloud} label="Top Type" value={summary.top_type ? TYPE_LABELS[summary.top_type.type] || summary.top_type.type : '—'} hint={summary.top_type ? `${fmt(summary.top_type.monthly_total)} / mo` : 'No active subscriptions'} /> <StatCard icon={Cloud} label="Top Type" value={summary.top_type ? TYPE_LABELS[summary.top_type.type] || summary.top_type.type : '—'} hint={summary.top_type ? `${fmt(summary.top_type.monthly_total)} / mo` : 'No active subscriptions'} />
</div> </div>
<div className="grid gap-5 xl:grid-cols-[minmax(0,1fr)_420px]"> <div className="grid min-w-0 gap-5 2xl:grid-cols-[minmax(0,1fr)_minmax(360px,420px)]">
<Card className="overflow-hidden"> <Card className="overflow-hidden">
<CardHeader className="pb-3"> <CardHeader className="px-4 pb-3 sm:px-6">
<CardTitle className="text-base">Tracked Subscriptions</CardTitle> <CardTitle className="text-base">Tracked Subscriptions</CardTitle>
<CardDescription>Subscriptions are bills with recurring-service metadata.</CardDescription> <CardDescription>Subscriptions are bills with recurring-service metadata.</CardDescription>
</CardHeader> </CardHeader>
@ -446,7 +454,7 @@ export default function SubscriptionsPage() {
</Card> </Card>
<Card className="overflow-hidden"> <Card className="overflow-hidden">
<CardHeader className="pb-3"> <CardHeader className="px-4 pb-3 sm:px-6">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Sparkles className="h-4 w-4 text-primary" /> <Sparkles className="h-4 w-4 text-primary" />
<CardTitle className="text-base">SimpleFIN Recommendations</CardTitle> <CardTitle className="text-base">SimpleFIN Recommendations</CardTitle>

View File

@ -1,6 +1,6 @@
{ {
"name": "bill-tracker", "name": "bill-tracker",
"version": "0.33.6", "version": "0.33.7",
"description": "Monthly bill tracking system", "description": "Monthly bill tracking system",
"main": "server.js", "main": "server.js",
"scripts": { "scripts": {