-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathpage.tsx
More file actions
132 lines (124 loc) · 6.71 KB
/
Copy pathpage.tsx
File metadata and controls
132 lines (124 loc) · 6.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
import {
AuthUserDto,
BalanceDto,
ExpenseDto,
GroupDto,
GroupMemberSummaryDto,
GroupSummaryDto,
PaginatedExpensesResponseDto,
RecurringExpenseDto,
formatCurrencyFromCents,
} from '@fairshare/shared-types';
import { notFound } from 'next/navigation';
import { Suspense } from 'react';
import { DashboardLayout } from '../../../../src/components/layout';
import { ExpenseTable, GroupActions, GroupSummaryPanel, MemberList, RecurringExpenseList } from '../../../../src/components/groups';
import { backendFetch } from '../../../../src/lib/backend';
interface GroupDetailPageProps {
params: Promise<{
groupId: string;
}>;
}
/**
* Renders the group detail dashboard for a specified group.
*
* @param params - An object containing `groupId` that identifies which group's data to load and display.
* @returns The page JSX for the group's detail dashboard, including the header (name, creation date, currency, member count, total spend), current user's balance pill, summary panel, recurring expenses, expense table, member list, and group actions.
*/
export default async function GroupDetailPage({ params }: GroupDetailPageProps) {
const { groupId } = await params;
try {
const [group, members, expenses, summary, balances, currentUser, recurringExpenses] = await Promise.all([
backendFetch<GroupDto>(`/groups/${groupId}`),
backendFetch<GroupMemberSummaryDto[]>(`/groups/${groupId}/members`),
backendFetch<PaginatedExpensesResponseDto>(`/groups/${groupId}/expenses?limit=50`),
backendFetch<GroupSummaryDto>(`/groups/${groupId}/summary`),
backendFetch<BalanceDto[]>(`/groups/${groupId}/balances`),
backendFetch<AuthUserDto>('/users/me'),
backendFetch<RecurringExpenseDto[]>(`/groups/${groupId}/recurring-expenses`),
]);
const totalExpensesCents = expenses.items.reduce((sum, expense) => sum + BigInt(expense.totalAmountCents), 0n);
const netBalanceCents = BigInt(summary.perUserOwedCents[currentUser.id] ?? '0');
const netBalance = Number(netBalanceCents) / 100;
const balanceLabel = netBalance > 0 ? 'You are owed' : netBalance < 0 ? 'You owe' : 'All settled';
const balanceTone =
netBalance > 0
? 'text-emerald-500 bg-emerald-500/10 border-emerald-500/20'
: netBalance < 0
? 'text-rose-500 bg-rose-500/10 border-rose-500/20'
: 'text-[var(--fs-text-muted)] bg-[var(--fs-background)] border-[var(--fs-border)]';
return (
<DashboardLayout>
<div className="space-y-10">
<header className="rounded-3xl border border-[var(--fs-border)] bg-[var(--fs-card)] p-5 sm:p-8 shadow-[var(--fs-shadow-soft)]">
<div className="flex items-start justify-between gap-4 flex-wrap">
<div className="space-y-2">
<p className="text-[11px] font-semibold uppercase tracking-[0.14em] text-[var(--fs-text-muted)]">Group</p>
<h1 className="text-2xl sm:text-3xl font-extrabold tracking-tight text-[var(--fs-text-primary)]">{group.name}</h1>
<p className="text-[12px] font-medium text-[var(--fs-text-muted)]">
Created {new Date(group.createdAt).toLocaleDateString()} - ID {groupId.slice(0, 8)}
</p>
<div className={`mt-3 inline-flex items-center gap-2 rounded-full border px-3 py-1.5 text-[11px] font-bold uppercase tracking-[0.12em] ${balanceTone}`}>
<span>{balanceLabel}</span>
<span>{formatCurrencyFromCents(netBalanceCents < 0n ? -netBalanceCents : netBalanceCents, group.currency)}</span>
</div>
</div>
<div className="grid grid-cols-3 gap-2 sm:flex sm:items-center sm:gap-3 w-full sm:w-auto">
<div className="rounded-xl border border-[var(--fs-border)] bg-[var(--fs-background)]/60 px-3 py-2 sm:px-4 sm:py-3 text-right">
<p className="text-[11px] font-bold uppercase tracking-[0.14em] text-[var(--fs-text-muted)]">Currency</p>
<p className="text-lg font-extrabold text-[var(--fs-text-primary)]">{group.currency}</p>
</div>
<div className="rounded-xl border border-[var(--fs-border)] bg-[var(--fs-background)]/60 px-3 py-2 sm:px-4 sm:py-3 text-right">
<p className="text-[11px] font-bold uppercase tracking-[0.14em] text-[var(--fs-text-muted)]">Members</p>
<p className="text-lg font-extrabold text-[var(--fs-text-primary)]">{members.length}</p>
</div>
<div className="rounded-xl border border-[var(--fs-border)] bg-[var(--fs-background)]/60 px-3 py-2 sm:px-4 sm:py-3 text-right">
<p className="text-[11px] font-bold uppercase tracking-[0.14em] text-[var(--fs-text-muted)]">Total spend</p>
<p className="text-lg font-extrabold text-[var(--fs-text-primary)]">
{formatCurrencyFromCents(totalExpensesCents, group.currency)}
</p>
</div>
</div>
</div>
</header>
<div className="grid gap-6 sm:gap-10 lg:grid-cols-[1fr_360px]">
<div className="space-y-10">
<GroupSummaryPanel
currency={group.currency}
summary={summary}
balances={balances}
currentUserId={currentUser.id}
members={members}
/>
<RecurringExpenseList recurringExpenses={recurringExpenses} members={members} currency={group.currency} />
<ExpenseTable
expenses={expenses.items as ExpenseDto[]}
members={members}
totalRecordsLabel={expenses.nextCursor !== null ? `${expenses.items.length}+` : String(expenses.items.length)}
/>
</div>
<aside className="space-y-10">
<Suspense fallback={<div className="h-52 rounded-3xl border border-[var(--fs-border)] bg-[var(--fs-card)] animate-pulse" />}>
<MemberList groupId={groupId} members={members} />
</Suspense>
<Suspense fallback={<div className="h-40 rounded-3xl border border-[var(--fs-border)] bg-[var(--fs-card)] animate-pulse" />}>
<GroupActions
groupId={groupId}
groupName={group.name}
currency={group.currency}
members={members}
shareEnabled={group.shareEnabled}
shareToken={group.shareToken}
defaultSplitPreference={group.defaultSplitPreference}
/>
</Suspense>
</aside>
</div>
</div>
</DashboardLayout>
);
} catch (error) {
console.error('Failed to fetch group details:', error);
return notFound();
}
}