Skip to content

Commit b64e7dd

Browse files
feat: build dashboard sections and components
1 parent 4965150 commit b64e7dd

6 files changed

Lines changed: 164 additions & 19 deletions

File tree

apps/web/app/dashboard/page.tsx

Lines changed: 57 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,71 @@
1-
import Link from 'next/link';
1+
import { ActivityDto, AuthUserDto } from '@fairshare/shared-types';
2+
3+
import { ActivityList, QuickActions, SummaryCard } from '../../src/components/dashboard';
24
import { DashboardLayout } from '../../src/components/layout';
35
import { backendFetch } from '../../src/lib/backend';
46

57
type Group = { id: string; name: string; currency: string };
8+
type Balance = { id: string; userId: string; counterpartyUserId: string; amountCents: string };
9+
10+
function formatUsd(cents: bigint): string {
11+
const dollars = Number(cents) / 100;
12+
return dollars.toLocaleString(undefined, { style: 'currency', currency: 'USD' });
13+
}
614

715
export default async function DashboardPage() {
8-
let groups: Group[] = [];
9-
try {
10-
groups = await backendFetch<Group[]>('/groups');
11-
} catch {
12-
groups = [];
16+
const [me, groups] = await Promise.all([
17+
backendFetch<AuthUserDto>('/users/me').catch(() => null),
18+
backendFetch<Group[]>('/groups').catch(() => [] as Group[]),
19+
]);
20+
21+
const activeGroupId = groups[0]?.id;
22+
23+
const [balances, activity] = await Promise.all([
24+
activeGroupId
25+
? backendFetch<Balance[]>(`/groups/${activeGroupId}/balances`).catch(() => [] as Balance[])
26+
: Promise.resolve([] as Balance[]),
27+
activeGroupId
28+
? backendFetch<ActivityDto[]>(`/groups/${activeGroupId}/activity?cursor=0&limit=5`).catch(() => [] as ActivityDto[])
29+
: Promise.resolve([] as ActivityDto[]),
30+
]);
31+
32+
let oweCents = 0n;
33+
let owedToMeCents = 0n;
34+
if (me) {
35+
for (const bal of balances) {
36+
if (bal.userId !== me.id) continue;
37+
const amt = BigInt(bal.amountCents);
38+
if (amt < 0n) oweCents += -amt;
39+
else owedToMeCents += amt;
40+
}
1341
}
1442

1543
return (
1644
<DashboardLayout>
1745
<section className="grid gap-4 md:grid-cols-3">
18-
<Link href="/dashboard/groups" className="rounded-2xl border border-border bg-card p-5 shadow-glass backdrop-blur-glass">
19-
<h2 className="font-semibold text-text-primary">Groups</h2>
20-
<p className="mt-1 text-sm text-text-secondary">{groups.length} total groups</p>
21-
</Link>
22-
<div className="rounded-2xl border border-border bg-card p-5 shadow-glass backdrop-blur-glass">
23-
<h2 className="font-semibold text-text-primary">Quick actions</h2>
24-
<p className="mt-1 text-sm text-text-secondary">Create expenses and settle balances.</p>
25-
</div>
26-
<div className="rounded-2xl border border-border bg-card p-5 shadow-glass backdrop-blur-glass">
27-
<h2 className="font-semibold text-text-primary">Recent activity</h2>
28-
<p className="mt-1 text-sm text-text-secondary">Coming next.</p>
29-
</div>
46+
<SummaryCard title="Groups" value={groups.length} hint="Your active groups" />
47+
<SummaryCard
48+
title="You owe"
49+
value={formatUsd(oweCents)}
50+
hint={activeGroupId ? `Based on ${activeGroupId}` : 'No group selected'}
51+
/>
52+
<SummaryCard
53+
title="You are owed"
54+
value={formatUsd(owedToMeCents)}
55+
hint={me ? `Signed in as ${me.email}` : 'Session active'}
56+
/>
57+
</section>
58+
59+
<section className="grid gap-4 md:grid-cols-[1.2fr_0.8fr]">
60+
{activeGroupId ? (
61+
<ActivityList groupId={activeGroupId} items={activity} />
62+
) : (
63+
<div className="rounded-2xl border border-border bg-card p-5 text-sm text-text-secondary shadow-glass backdrop-blur-glass">
64+
Create or join a group to see activity.
65+
</div>
66+
)}
67+
<QuickActions groupId={activeGroupId} />
3068
</section>
3169
</DashboardLayout>
3270
);
33-
}
71+
}

apps/web/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"test": "echo \"No tests yet\""
1111
},
1212
"dependencies": {
13+
"@fairshare/shared-types": "workspace:*",
1314
"framer-motion": "^12.6.5",
1415
"next": "15.2.0",
1516
"react": "19.0.0",
@@ -25,3 +26,6 @@
2526
"typescript": "^5.7.3"
2627
}
2728
}
29+
30+
31+
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import Link from 'next/link';
2+
import { ActivityDto } from '@fairshare/shared-types';
3+
4+
function labelForType(type: ActivityDto['type']): string {
5+
switch (type) {
6+
case 'expense_created':
7+
return 'Expense created';
8+
case 'expense_updated':
9+
return 'Expense updated';
10+
case 'expense_deleted':
11+
return 'Expense deleted';
12+
case 'settlement_created':
13+
return 'Settlement created';
14+
case 'member_joined':
15+
return 'Member joined';
16+
case 'member_invited':
17+
return 'Member invited';
18+
default:
19+
return 'Activity';
20+
}
21+
}
22+
23+
export function ActivityList({ items, groupId }: { items: ActivityDto[]; groupId: string }) {
24+
return (
25+
<div className="rounded-2xl border border-border bg-card p-5 shadow-glass backdrop-blur-glass">
26+
<div className="flex items-center justify-between gap-3">
27+
<h2 className="text-base font-semibold text-text-primary">Recent activity</h2>
28+
<Link
29+
href={`/dashboard/activity?groupId=${encodeURIComponent(groupId)}`}
30+
className="text-sm text-text-secondary underline underline-offset-4 hover:text-text-primary"
31+
>
32+
View all
33+
</Link>
34+
</div>
35+
36+
<div className="mt-4 space-y-2">
37+
{items.map((item) => (
38+
<div key={item.id} className="rounded-xl border border-border/60 bg-surface/20 p-3">
39+
<p className="text-sm font-medium text-text-primary">{labelForType(item.type)}</p>
40+
<p className="mt-1 text-xs text-text-secondary">{new Date(item.createdAt).toLocaleString()}</p>
41+
</div>
42+
))}
43+
{items.length === 0 ? <p className="text-sm text-text-secondary">No activity yet.</p> : null}
44+
</div>
45+
</div>
46+
);
47+
}
48+
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import Link from 'next/link';
2+
3+
export function QuickActions({ groupId }: { groupId?: string }) {
4+
return (
5+
<div className="rounded-2xl border border-border bg-card p-5 shadow-glass backdrop-blur-glass">
6+
<h2 className="text-base font-semibold text-text-primary">Quick actions</h2>
7+
<div className="mt-4 grid gap-2">
8+
<Link
9+
href="/dashboard/groups"
10+
className="rounded-xl border border-border/60 bg-surface/20 px-4 py-3 text-sm text-text-primary hover:bg-white/5"
11+
>
12+
Browse groups
13+
</Link>
14+
<Link
15+
href={groupId ? `/dashboard/groups/${encodeURIComponent(groupId)}` : '/dashboard/groups'}
16+
className="rounded-xl border border-border/60 bg-surface/20 px-4 py-3 text-sm text-text-primary hover:bg-white/5"
17+
>
18+
Open group
19+
</Link>
20+
<Link
21+
href="/dashboard/activity"
22+
className="rounded-xl border border-border/60 bg-surface/20 px-4 py-3 text-sm text-text-primary hover:bg-white/5"
23+
>
24+
Activity timeline
25+
</Link>
26+
</div>
27+
<p className="mt-3 text-xs text-text-secondary">Expense creation lands in the Groups view.</p>
28+
</div>
29+
);
30+
}
31+
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { ReactNode } from 'react';
2+
3+
export function SummaryCard({
4+
title,
5+
value,
6+
hint,
7+
}: {
8+
title: string;
9+
value: ReactNode;
10+
hint?: ReactNode;
11+
}) {
12+
return (
13+
<div className="rounded-2xl border border-border bg-card p-5 shadow-glass backdrop-blur-glass">
14+
<p className="text-sm text-text-secondary">{title}</p>
15+
<div className="mt-1 text-2xl font-semibold tracking-tight text-text-primary">{value}</div>
16+
{hint ? <div className="mt-2 text-sm text-text-secondary">{hint}</div> : null}
17+
</div>
18+
);
19+
}
20+
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export * from './ActivityList';
2+
export * from './QuickActions';
3+
export * from './SummaryCard';
4+

0 commit comments

Comments
 (0)