Skip to content

Commit fc7c7e8

Browse files
feat(web): implement Groups UI refactor (Phase 5)
- Added new glassmorphism components: GroupCard, MemberList, and ExpenseTable. - Overhauled the groups dashboard list view with a modern grid layout. - Implemented a detailed group view route at `/dashboard/groups/[groupId]`. - Added redirection for legacy `/dashboard/group/[id]` paths to maintain compatibility. - Ensured full type safety using @fairshare/shared-types.
1 parent b64e7dd commit fc7c7e8

11 files changed

Lines changed: 254 additions & 18 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ coverage
88
.agents
99
.agent
1010
.turbo
11+
skills-lock.json
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { redirect } from 'next/navigation';
2+
3+
interface LegacyGroupPageProps {
4+
params: Promise<{
5+
id: string;
6+
}>;
7+
}
8+
9+
export default async function LegacyGroupPage({ params }: LegacyGroupPageProps) {
10+
const { id } = await params;
11+
redirect(`/dashboard/groups/${id}`);
12+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import {
2+
ExpenseDto,
3+
GroupDto,
4+
GroupMemberSummaryDto,
5+
PaginatedExpensesResponseDto
6+
} from '@fairshare/shared-types';
7+
import { notFound } from 'next/navigation';
8+
9+
import { DashboardLayout } from '../../../../src/components/layout';
10+
import { MemberList, ExpenseTable } from '../../../../src/components/groups';
11+
import { backendFetch } from '../../../../src/lib/backend';
12+
13+
interface GroupDetailPageProps {
14+
params: Promise<{
15+
groupId: string;
16+
}>;
17+
}
18+
19+
export default async function GroupDetailPage({ params }: GroupDetailPageProps) {
20+
const { groupId } = await params;
21+
22+
try {
23+
const [group, members, expenses] = await Promise.all([
24+
backendFetch<GroupDto>(`/groups/${groupId}`),
25+
backendFetch<GroupMemberSummaryDto[]>(`/groups/${groupId}/members`),
26+
backendFetch<PaginatedExpensesResponseDto>(`/groups/${groupId}/expenses?limit=50`),
27+
]);
28+
29+
return (
30+
<DashboardLayout>
31+
<header className="mb-8">
32+
<div className="flex items-center justify-between">
33+
<h1 className="text-3xl font-bold text-text-primary">{group.name}</h1>
34+
<div className="rounded-full bg-brand/10 px-4 py-1.5 text-xs font-semibold uppercase tracking-wider text-brand">
35+
{group.currency}
36+
</div>
37+
</div>
38+
<p className="mt-2 text-text-secondary">
39+
Manage members and track shared expenses.
40+
</p>
41+
</header>
42+
43+
<div className="grid gap-6 lg:grid-cols-[1fr_320px]">
44+
<div className="space-y-6">
45+
<ExpenseTable expenses={expenses.items} />
46+
</div>
47+
48+
<aside className="space-y-6">
49+
<MemberList members={members} />
50+
51+
<div className="rounded-2xl border border-border bg-card p-5 shadow-glass backdrop-blur-glass">
52+
<h3 className="text-sm font-semibold text-text-primary">Quick Actions</h3>
53+
<div className="mt-4 grid gap-2">
54+
<button className="w-full rounded-xl bg-brand py-2.5 text-sm font-semibold text-white transition-opacity hover:opacity-90">
55+
Add Expense
56+
</button>
57+
<button className="w-full rounded-xl border border-border bg-surface/10 py-2.5 text-sm font-semibold text-text-primary transition-colors hover:bg-surface/20">
58+
Invite Member
59+
</button>
60+
</div>
61+
</div>
62+
</aside>
63+
</div>
64+
</DashboardLayout>
65+
);
66+
} catch (error) {
67+
console.error('Failed to fetch group details:', error);
68+
return notFound();
69+
}
70+
}

apps/web/app/dashboard/groups/page.tsx

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,39 @@
1-
import Link from 'next/link';
1+
import { GroupDto } from '@fairshare/shared-types';
22
import { DashboardLayout } from '../../../src/components/layout';
3+
import { GroupCard } from '../../../src/components/groups';
34
import { backendFetch } from '../../../src/lib/backend';
45

5-
type Group = { id: string; name: string; currency: string };
6-
76
export default async function DashboardGroupsPage() {
8-
let groups: Group[] = [];
7+
let groups: GroupDto[] = [];
98
try {
10-
groups = await backendFetch<Group[]>('/groups');
9+
groups = await backendFetch<GroupDto[]>('/groups');
1110
} catch {
1211
groups = [];
1312
}
1413

1514
return (
1615
<DashboardLayout>
17-
<section className="space-y-3">
16+
<div className="mb-6 flex items-center justify-between">
17+
<h1 className="text-2xl font-bold text-text-primary">Your Groups</h1>
18+
</div>
19+
20+
<section className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
1821
{groups.map((group) => (
19-
<Link
22+
<GroupCard
2023
key={group.id}
21-
href={`/dashboard/group/${group.id}`}
22-
className="block rounded-2xl border border-border bg-card p-5 shadow-glass backdrop-blur-glass"
23-
>
24-
<p className="font-semibold text-text-primary">{group.name}</p>
25-
<p className="mt-1 text-sm text-text-secondary">{group.currency}</p>
26-
</Link>
24+
id={group.id}
25+
name={group.name}
26+
currency={group.currency}
27+
memberCount={0} // Future: API to provide member count
28+
/>
2729
))}
28-
{groups.length === 0 ? (
29-
<div className="rounded-2xl border border-border bg-card p-6 text-sm text-text-secondary shadow-glass backdrop-blur-glass">
30-
No groups yet.
30+
31+
{groups.length === 0 && (
32+
<div className="col-span-full rounded-2xl border border-border bg-card p-10 text-center text-text-secondary shadow-glass backdrop-blur-glass">
33+
<p className="text-lg font-medium text-text-primary">No groups yet</p>
34+
<p className="mt-1">Create or join a group to start sharing expenses.</p>
3135
</div>
32-
) : null}
36+
)}
3337
</section>
3438
</DashboardLayout>
3539
);
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { ExpenseDto } from '@fairshare/shared-types';
2+
3+
interface ExpenseTableProps {
4+
expenses: ExpenseDto[];
5+
}
6+
7+
export function ExpenseTable({ expenses }: ExpenseTableProps) {
8+
function formatUsd(cents: string): string {
9+
const dollars = Number(cents) / 100;
10+
return dollars.toLocaleString(undefined, { style: 'currency', currency: 'USD' });
11+
}
12+
13+
return (
14+
<div className="rounded-2xl border border-border bg-card shadow-glass backdrop-blur-glass overflow-hidden">
15+
<div className="p-5 border-b border-border bg-surface/5">
16+
<h2 className="text-base font-semibold text-text-primary">Expenses</h2>
17+
</div>
18+
<div className="overflow-x-auto">
19+
<table className="w-full text-left text-sm border-collapse">
20+
<thead>
21+
<tr className="bg-surface/5 text-text-secondary font-medium">
22+
<th className="px-5 py-3 border-b border-border">Description</th>
23+
<th className="px-5 py-3 border-b border-border text-right">Amount</th>
24+
<th className="px-5 py-3 border-b border-border text-right">Date</th>
25+
</tr>
26+
</thead>
27+
<tbody className="divide-y divide-border">
28+
{expenses.map((expense) => (
29+
<tr key={expense.id} className="hover:bg-surface/5 transition-colors group">
30+
<td className="px-5 py-4 text-text-primary font-medium">
31+
{expense.description}
32+
</td>
33+
<td className="px-5 py-4 text-right text-text-primary font-mono">
34+
{formatUsd(expense.totalAmountCents)}
35+
</td>
36+
<td className="px-5 py-4 text-right text-text-secondary">
37+
{new Date(expense.createdAt).toLocaleDateString(undefined, {
38+
month: 'short',
39+
day: 'numeric',
40+
})}
41+
</td>
42+
</tr>
43+
))}
44+
{expenses.length === 0 && (
45+
<tr>
46+
<td colSpan={3} className="px-5 py-10 text-center text-text-secondary italic">
47+
No expenses recorded yet.
48+
</td>
49+
</tr>
50+
)}
51+
</tbody>
52+
</table>
53+
</div>
54+
</div>
55+
);
56+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import Link from 'next/link';
2+
3+
interface GroupCardProps {
4+
id: string;
5+
name: string;
6+
currency: string;
7+
memberCount: number;
8+
balance?: {
9+
owe: string;
10+
owed: string;
11+
};
12+
}
13+
14+
export function GroupCard({ id, name, currency, memberCount, balance }: GroupCardProps) {
15+
return (
16+
<Link
17+
href={`/dashboard/groups/${id}`}
18+
className="group block rounded-2xl border border-border bg-card p-5 shadow-glass transition-all hover:bg-surface/10 backdrop-blur-glass"
19+
>
20+
<div className="flex items-start justify-between">
21+
<div>
22+
<h3 className="text-lg font-semibold text-text-primary group-hover:text-brand transition-colors">
23+
{name}
24+
</h3>
25+
<p className="text-sm text-text-secondary mt-1">
26+
{memberCount} {memberCount === 1 ? 'member' : 'members'}{currency}
27+
</p>
28+
</div>
29+
30+
{balance && (
31+
<div className="text-right">
32+
{parseFloat(balance.owed) > 0 && (
33+
<p className="text-sm text-success font-medium">
34+
You are owed {balance.owed}
35+
</p>
36+
)}
37+
{parseFloat(balance.owe) > 0 && (
38+
<p className="text-sm text-danger font-medium mt-1">
39+
You owe {balance.owe}
40+
</p>
41+
)}
42+
{parseFloat(balance.owed) === 0 && parseFloat(balance.owe) === 0 && (
43+
<p className="text-sm text-text-secondary">Settle up</p>
44+
)}
45+
</div>
46+
)}
47+
</div>
48+
</Link>
49+
);
50+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { GroupMemberSummaryDto } from '@fairshare/shared-types';
2+
3+
interface MemberListProps {
4+
members: GroupMemberSummaryDto[];
5+
}
6+
7+
export function MemberList({ members }: MemberListProps) {
8+
return (
9+
<div className="rounded-2xl border border-border bg-card p-5 shadow-glass backdrop-blur-glass">
10+
<h2 className="text-base font-semibold text-text-primary">Group Members</h2>
11+
<div className="mt-4 space-y-4">
12+
{members.map((member) => (
13+
<div key={member.memberId} className="flex items-center gap-3">
14+
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-brand/10 text-brand font-bold">
15+
{member.name.charAt(0).toUpperCase()}
16+
</div>
17+
<div className="flex-1 min-w-0">
18+
<p className="text-sm font-medium text-text-primary truncate">
19+
{member.name}
20+
</p>
21+
<p className="text-xs text-text-secondary capitalize truncate">
22+
{member.role.toLowerCase()}
23+
</p>
24+
</div>
25+
</div>
26+
))}
27+
</div>
28+
</div>
29+
);
30+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './GroupCard';
2+
export * from './MemberList';
3+
export * from './ExpenseTable';

apps/web/tsconfig.tsbuildinfo

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

build_output.txt

146 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)