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:
parent
6b30ee4eb7
commit
c3c0ab3542
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue