Skip to content

Commit e13490f

Browse files
author
root
committed
feat(web): align dashboard with mobile parity
1 parent e39dcbe commit e13490f

16 files changed

Lines changed: 806 additions & 8 deletions

File tree

apps/web/app/api/activity/route.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
import { ActivityDto } from '@fairshare/shared-types';
3+
import { backendFetch } from '../../../src/lib/backend';
4+
5+
type ActivityResponse = { items: ActivityDto[]; nextCursor: number | null };
6+
7+
export async function GET(req: NextRequest) {
8+
const { searchParams } = new URL(req.url);
9+
const cursor = searchParams.get('cursor') ?? '0';
10+
const limit = searchParams.get('limit') ?? '20';
11+
const groupId = searchParams.get('groupId');
12+
13+
const path = groupId
14+
? `/activity/group/${encodeURIComponent(groupId)}?cursor=${cursor}&limit=${limit}`
15+
: `/activity?cursor=${cursor}&limit=${limit}`;
16+
17+
try {
18+
const data = await backendFetch<ActivityResponse>(path);
19+
return NextResponse.json(data);
20+
} catch (error) {
21+
console.error('activity route failed', error);
22+
return NextResponse.json({ items: [], nextCursor: null }, { status: 500 });
23+
}
24+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { ActivityDto, GroupDto } from '@fairshare/shared-types';
2+
import { DashboardLayout } from '../../../src/components/layout';
3+
import { ActivityFeed } from '../../../src/components/activity/ActivityFeed';
4+
import { backendFetch } from '../../../src/lib/backend';
5+
6+
type ActivityResponse = { items: ActivityDto[]; nextCursor: number | null };
7+
8+
export default async function ActivityPage() {
9+
let groups: GroupDto[] = [];
10+
let activity: ActivityResponse = { items: [], nextCursor: null };
11+
12+
try {
13+
groups = await backendFetch<GroupDto[]>('/groups');
14+
} catch {
15+
groups = [];
16+
}
17+
18+
try {
19+
activity = await backendFetch<ActivityResponse>('/activity?cursor=0&limit=20');
20+
} catch {
21+
activity = { items: [], nextCursor: null };
22+
}
23+
24+
return (
25+
<DashboardLayout>
26+
<div className="space-y-10">
27+
<div className="rounded-3xl border border-[var(--fs-border)] bg-[var(--fs-card)] p-8 shadow-[var(--fs-shadow-soft)]">
28+
<p className="text-[11px] font-semibold uppercase tracking-[0.14em] text-[var(--fs-text-muted)]">Activity</p>
29+
<h1 className="text-3xl font-extrabold tracking-tight text-[var(--fs-text-primary)]">Timeline parity</h1>
30+
<p className="text-[12px] font-medium text-[var(--fs-text-muted)]">
31+
Mirror the mobile activity feed with filtering by group, realtime-friendly ordering, and readable contrasts in both themes.
32+
</p>
33+
</div>
34+
35+
<ActivityFeed groups={groups} initialItems={activity.items} initialCursor={activity.nextCursor} />
36+
</div>
37+
</DashboardLayout>
38+
);
39+
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { GroupDto } from '@fairshare/shared-types';
22
import { DashboardLayout } from '../../../src/components/layout';
3-
import { GroupCard } from '../../../src/components/groups';
3+
import { GroupCard, CreateGroupButton } from '../../../src/components/groups';
44
import { backendFetch } from '../../../src/lib/backend';
55

66
export default async function DashboardGroupsPage() {
@@ -12,7 +12,7 @@ export default async function DashboardGroupsPage() {
1212
}
1313

1414
return (
15-
<DashboardLayout>
15+
<DashboardLayout topbarRight={<CreateGroupButton />}>
1616
<div className="space-y-10">
1717
<div className="rounded-3xl border border-[var(--fs-border)] bg-[var(--fs-card)] p-8 shadow-[var(--fs-shadow-soft)]">
1818
<div className="flex items-center justify-between gap-4">
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { AuthUserDto } from '@fairshare/shared-types';
2+
import { DashboardLayout } from '../../../src/components/layout';
3+
import { ProfilePanel } from '../../../src/components/profile/ProfilePanel';
4+
import { backendFetch } from '../../../src/lib/backend';
5+
6+
export default async function ProfilePage() {
7+
let user: AuthUserDto | null = null;
8+
try {
9+
user = await backendFetch<AuthUserDto>('/users/me');
10+
} catch {
11+
user = null;
12+
}
13+
14+
return (
15+
<DashboardLayout>
16+
<div className="space-y-8">
17+
<div className="rounded-3xl border border-[var(--fs-border)] bg-[var(--fs-card)] p-8 shadow-[var(--fs-shadow-soft)]">
18+
<p className="text-[11px] font-semibold uppercase tracking-[0.14em] text-[var(--fs-text-muted)]">Profile</p>
19+
<h1 className="text-3xl font-extrabold tracking-tight text-[var(--fs-text-primary)]">Your account</h1>
20+
<p className="text-[12px] font-medium text-[var(--fs-text-muted)]">
21+
Mirror the mobile profile: review your identity, hop to settings, and sign out securely.
22+
</p>
23+
</div>
24+
25+
<ProfilePanel fallbackUser={user} />
26+
</div>
27+
</DashboardLayout>
28+
);
29+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { DashboardLayout } from '../../../src/components/layout';
2+
import { AppearanceSettings } from '../../../src/components/settings/AppearanceSettings';
3+
4+
export default function SettingsPage() {
5+
return (
6+
<DashboardLayout>
7+
<div className="space-y-10">
8+
<div className="rounded-3xl border border-[var(--fs-border)] bg-[var(--fs-card)] p-8 shadow-[var(--fs-shadow-soft)]">
9+
<p className="text-[11px] font-semibold uppercase tracking-[0.14em] text-[var(--fs-text-muted)]">Appearance</p>
10+
<h1 className="text-3xl font-extrabold tracking-tight text-[var(--fs-text-primary)]">Display & theme</h1>
11+
<p className="text-[12px] font-medium text-[var(--fs-text-muted)]">
12+
Match the mobile experience with instant light, dark, or system-aware themes. All text automatically keeps contrast for readability.
13+
</p>
14+
</div>
15+
16+
<AppearanceSettings />
17+
</div>
18+
</DashboardLayout>
19+
);
20+
}

apps/web/app/layout.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,23 @@ import { Manrope, Space_Grotesk } from 'next/font/google';
55
import { Providers } from '../src/components/Providers';
66
import { themeStylesheet } from '../src/design/theme';
77

8+
const themeInitScript = `
9+
(function() {
10+
try {
11+
const storageKey = 'fs-theme';
12+
const stored = localStorage.getItem(storageKey);
13+
const mode = stored === 'light' || stored === 'dark' || stored === 'system' ? stored : 'system';
14+
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
15+
const resolved = mode === 'system' ? (prefersDark ? 'dark' : 'light') : mode;
16+
document.documentElement.dataset.theme = resolved;
17+
document.body.dataset.theme = resolved;
18+
} catch (_) {
19+
document.documentElement.dataset.theme = 'light';
20+
document.body.dataset.theme = 'light';
21+
}
22+
})();
23+
`;
24+
825
export const metadata: Metadata = {
926
title: 'FairShare | Smart Group Expense Sharing',
1027
description: 'Track shared expenses, simplify debts, and settle quickly with FairShare.',
@@ -24,6 +41,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
2441
<html lang="en" data-theme="light" className={`${manrope.variable} ${spaceGrotesk.variable}`}>
2542
<head>
2643
<style id="fs-theme-vars">{themeStylesheet}</style>
44+
<script dangerouslySetInnerHTML={{ __html: themeInitScript }} />
2745
</head>
2846
<body className="min-h-screen">
2947
<Providers>

apps/web/src/components/Providers.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@
33
import { AuthProvider } from './auth/AuthProvider';
44
import { RealtimeProvider } from './realtime/RealtimeProvider';
55
import { ToastProvider } from './ui/Toaster';
6+
import { ThemeProvider } from './theme/ThemeProvider';
67

78
export function Providers({ children }: { children: React.ReactNode }) {
89
return (
910
<ToastProvider>
10-
<AuthProvider>
11-
<RealtimeProvider>
12-
{children}
13-
</RealtimeProvider>
14-
</AuthProvider>
11+
<ThemeProvider>
12+
<AuthProvider>
13+
<RealtimeProvider>
14+
{children}
15+
</RealtimeProvider>
16+
</AuthProvider>
17+
</ThemeProvider>
1518
</ToastProvider>
1619
);
1720
}

0 commit comments

Comments
 (0)