Skip to content

Commit 96e75ba

Browse files
feat: updated initial dashboard page with spending chart, activity list, summary cards, and a new glass card UI component.
1 parent 8e00c2e commit 96e75ba

7 files changed

Lines changed: 322 additions & 89 deletions

File tree

apps/web/app/dashboard/page.tsx

Lines changed: 104 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
11
import { ActivityDto } from '@fairshare/shared-types';
2-
2+
import {
3+
DollarSign,
4+
Users,
5+
TrendingDown,
6+
Receipt,
7+
ArrowUpRight,
8+
TrendingUp,
9+
CreditCard,
10+
Plus
11+
} from 'lucide-react';
312
import { ActivityList, QuickActions, SummaryCard } from '../../src/components/dashboard';
13+
import { SpendingChart } from '../../src/components/dashboard/SpendingChart';
414
import { DashboardLayout } from '../../src/components/layout';
515
import { backendFetch } from '../../src/lib/backend';
16+
import { GlassCard } from '../../components/ui/GlassCard';
617

718
type Group = { id: string; name: string; currency: string };
819

@@ -28,41 +39,115 @@ export default async function DashboardPage() {
2839

2940
return (
3041
<DashboardLayout>
31-
<div className="space-y-6">
32-
<div className="grid grid-cols-1 gap-6 md:grid-cols-3">
42+
<div className="space-y-8">
43+
{/* ── 4-Stat Hero Grid ── */}
44+
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
3345
<SummaryCard
3446
title="Total Balance"
3547
value={formatMoney(totalBalanceCents, 'USD')}
36-
hint={isPositive ? 'You are owed overall' : 'You owe overall'}
48+
icon="dollar"
49+
change="+12.5%"
50+
trend={isPositive ? 'up' : 'down'}
51+
hint={isPositive ? 'Surplus Protocol' : 'Deficit Detected'}
3752
/>
3853
<SummaryCard
39-
title="Active Groups"
54+
title="Active Crews"
4055
value={groups.length.toString()}
41-
hint="Across all categories"
56+
icon="users"
57+
change="Live"
58+
trend="neutral"
59+
hint="System Synchronized"
60+
/>
61+
<SummaryCard
62+
title="Incoming"
63+
value="$1,248.00"
64+
icon="trendingUp"
65+
change="+4.2%"
66+
trend="up"
67+
hint="Verified Receivables"
68+
/>
69+
<SummaryCard
70+
title="Total Logs"
71+
value="142"
72+
icon="receipt"
73+
change="+12"
74+
trend="up"
75+
hint="Immutable Entries"
4276
/>
43-
<QuickActions />
4477
</div>
4578

79+
{/* ── Main Visualization Row ── */}
4680
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
4781
<div className="lg:col-span-2">
82+
<SpendingChart />
83+
</div>
84+
<div className="flex flex-col gap-6">
85+
<QuickActions />
4886
<ActivityList items={recentActivity} groupId="" />
4987
</div>
50-
<div>
51-
<h2 className="mb-4 text-xl font-semibold">Group Balances</h2>
52-
<div className="rounded-xl bg-white p-6 shadow-sm">
53-
{groups.map((group) => (
54-
<div key={group.id} className="flex items-center justify-between border-b py-3 last:border-0">
55-
<span className="font-medium">{group.name}</span>
56-
<span className="text-sm text-gray-500">{group.currency}</span>
88+
</div>
89+
90+
{/* ── Secondary Data Row ── */}
91+
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
92+
<div className="lg:col-span-2">
93+
<GlassCard className="p-8 border-white/5 bg-white/[0.01]">
94+
<div className="flex items-center justify-between mb-8">
95+
<div>
96+
<h2 className="text-sm font-black italic tracking-tight text-white uppercase">Crews & Nodes</h2>
97+
<p className="text-[10px] font-bold tracking-widest text-zinc-600 uppercase mt-0.5">Distributed spending groups</p>
98+
</div>
99+
<button
100+
title="Create New Crew"
101+
className="h-8 w-8 flex items-center justify-center rounded-lg bg-purple-600/10 text-purple-400 border border-purple-500/20 hover:bg-purple-600 hover:text-white transition-all"
102+
>
103+
<Plus size={16} />
104+
</button>
105+
</div>
106+
107+
<div className="grid gap-3 sm:grid-cols-2">
108+
{groups.map((group) => (
109+
<div key={group.id} className="flex items-center justify-between p-4 rounded-xl border border-white/5 bg-white/5 hover:bg-white/10 transition-all cursor-pointer group">
110+
<div className="flex items-center gap-3">
111+
<div className="h-8 w-8 rounded-lg bg-zinc-900 flex items-center justify-center text-zinc-500 group-hover:text-purple-400 transition-colors">
112+
<CreditCard size={14} />
113+
</div>
114+
<span className="text-xs font-black tracking-tight text-white uppercase">{group.name}</span>
115+
</div>
116+
<div className="flex items-center gap-2">
117+
<span className="text-[10px] font-bold text-zinc-500 uppercase tracking-widest">{group.currency}</span>
118+
<ArrowUpRight size={12} className="text-zinc-800 group-hover:text-purple-500 transition-colors" />
119+
</div>
120+
</div>
121+
))}
122+
{groups.length === 0 && (
123+
<div className="col-span-full py-12 text-center rounded-2xl border border-dashed border-white/5">
124+
<p className="text-[10px] font-black uppercase tracking-[0.2em] text-zinc-700">NO_ACTIVE_GROUPS_LOCATED</p>
125+
</div>
126+
)}
127+
</div>
128+
</GlassCard>
129+
</div>
130+
131+
{/* Action Trigger / Extra Slot */}
132+
<div className="flex flex-col gap-6">
133+
<div className="relative overflow-hidden rounded-[2.5rem] border border-white/5 bg-gradient-to-br from-purple-600/20 to-indigo-600/20 p-8 flex flex-col justify-between aspect-square">
134+
<div className="absolute top-0 right-0 p-8">
135+
<TrendingUp size={48} className="text-purple-500/20" />
57136
</div>
58-
))}
59-
{groups.length === 0 && (
60-
<p className="py-4 text-center text-sm text-gray-500">No active groups yet.</p>
61-
)}
62-
</div>
137+
<h3 className="text-3xl font-black italic tracking-tighter text-white leading-none">
138+
OPTIMIZE <br /> FLOW.
139+
</h3>
140+
<p className="text-xs font-bold text-purple-200/50 uppercase tracking-widest leading-relaxed">
141+
Our algorithm has detected 3 ways to optimize your settlements.
142+
</p>
143+
<button className="w-full h-12 rounded-2xl bg-white text-[10px] font-black uppercase tracking-widest text-[#030303] hover:bg-purple-400 transition-all">
144+
Recalculate Nodes
145+
</button>
146+
</div>
63147
</div>
64148
</div>
65149
</div>
66150
</DashboardLayout>
67151
);
68152
}
153+

apps/web/components/ui/GlassCard.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { ReactNode, HTMLAttributes } from 'react';
1+
'use client';
2+
3+
import React, { ReactNode } from 'react';
24
import { motion, HTMLMotionProps } from 'framer-motion';
35

46
interface GlassCardProps extends HTMLMotionProps<"div"> {

apps/web/src/components/dashboard/ActivityList.tsx

Lines changed: 71 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,93 @@
1+
'use client';
2+
13
import Link from 'next/link';
24
import { ActivityDto } from '@fairshare/shared-types';
3-
import { Clock } from 'lucide-react';
4-
import { glassPanel } from '../layout/layoutStyles';
5+
import { Clock, Receipt, Users, Plus, Trash2, UserPlus, Milestone, ArrowUpRight } from 'lucide-react';
6+
import { motion } from 'framer-motion';
57

6-
function labelForType(type: ActivityDto['type']): string {
8+
function getIconForType(type: ActivityDto['type']) {
79
switch (type) {
8-
case 'expense_created':
9-
return 'Expense created';
10-
case 'expense_updated':
11-
return 'Expense updated';
12-
case 'expense_deleted':
13-
return 'Expense deleted';
14-
case 'settlement_created':
15-
return 'Settlement created';
16-
case 'member_joined':
17-
return 'Member joined';
18-
case 'member_invited':
19-
return 'Member invited';
20-
default:
21-
return 'Activity';
10+
case 'expense_created': return { Icon: Plus, color: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20' };
11+
case 'expense_updated': return { Icon: Receipt, color: 'bg-purple-500/10 text-purple-400 border-purple-500/20' };
12+
case 'expense_deleted': return { Icon: Trash2, color: 'bg-rose-500/10 text-rose-400 border-rose-500/20' };
13+
case 'settlement_created': return { Icon: Milestone, color: 'bg-indigo-500/10 text-indigo-400 border-indigo-500/20' };
14+
case 'member_joined': return { Icon: UserPlus, color: 'bg-violet-500/10 text-violet-400 border-violet-500/20' };
15+
default: return { Icon: ActivitySquare, color: 'bg-zinc-500/10 text-zinc-400 border-zinc-500/20' };
2216
}
2317
}
2418

19+
function labelForType(type: ActivityDto['type']): string {
20+
return type.split('_').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
21+
}
22+
23+
import { ActivitySquare } from 'lucide-react';
24+
2525
export function ActivityList({ items = [], groupId = '' }: { items?: ActivityDto[]; groupId?: string }) {
2626
const safeItems = Array.isArray(items) ? items : [];
2727

2828
return (
29-
<div className={`${glassPanel} p-7`}>
30-
<div className="flex items-center justify-between gap-3 mb-6">
31-
<h2 className="text-xl font-extrabold tracking-tight text-[var(--fs-text-primary)]">Recent Activity</h2>
29+
<div className="group relative overflow-hidden rounded-2xl border border-white/5 bg-white/[0.01] p-6 transition-all hover:bg-white/[0.02]">
30+
<div className="flex items-center justify-between gap-3 mb-8">
31+
<div>
32+
<h2 className="text-sm font-black italic tracking-tight text-white uppercase">Live Signals</h2>
33+
<p className="text-[10px] font-bold tracking-widest text-zinc-600 uppercase mt-0.5">Real-time ledger audit</p>
34+
</div>
3235
<Link
3336
href={`/dashboard/activity?groupId=${encodeURIComponent(groupId)}`}
34-
className="text-[11px] font-bold uppercase tracking-[0.14em] text-[var(--fs-text-muted)] hover:text-[var(--fs-primary)] transition-colors"
37+
className="group/link flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-zinc-600 hover:text-white transition-colors"
3538
>
36-
View timeline
39+
Timeline <ArrowUpRight size={12} className="group-hover/link:translate-x-0.5 group-hover/link:-translate-y-0.5 transition-transform" />
3740
</Link>
3841
</div>
3942

40-
<div className="space-y-3">
41-
{safeItems.map((item) => (
42-
<div
43-
key={item.id}
44-
className="flex items-center justify-between gap-3 rounded-xl border border-[var(--fs-border)] bg-[var(--fs-background)]/70 px-4 py-3 hover:border-[var(--fs-primary)] transition-colors"
45-
>
46-
<div>
47-
<p className="text-sm font-semibold text-[var(--fs-text-primary)]">{labelForType(item.type)}</p>
48-
<p className="text-[11px] font-medium text-[var(--fs-text-muted)] flex items-center gap-2">
49-
<Clock size={14} strokeWidth={2} />
50-
{new Date(item.createdAt).toLocaleString()}
51-
</p>
52-
</div>
53-
<span className="text-[10px] font-bold uppercase tracking-[0.14em] text-[var(--fs-primary)] bg-[var(--fs-primary)]/10 px-3 py-1 rounded-lg">
54-
{item.type.replace('_', ' ')}
55-
</span>
56-
</div>
57-
))}
43+
<div className="space-y-4">
44+
{safeItems.map((item, idx) => {
45+
const { Icon, color } = getIconForType(item.type);
46+
return (
47+
<motion.div
48+
key={item.id}
49+
initial={{ opacity: 0, x: -10 }}
50+
animate={{ opacity: 1, x: 0 }}
51+
transition={{ delay: idx * 0.05 }}
52+
whileHover={{ x: 2 }}
53+
className="flex items-center gap-4 group/item cursor-pointer"
54+
>
55+
<div className={`h-10 w-10 shrink-0 flex items-center justify-center rounded-xl border ${color}`}>
56+
<Icon size={16} />
57+
</div>
58+
59+
<div className="flex-grow min-w-0">
60+
<p className="text-xs font-black tracking-tight text-white group-hover:text-purple-400 transition-colors truncate">
61+
{labelForType(item.type)}
62+
</p>
63+
<div className="flex items-center gap-2 mt-1">
64+
<p className="text-[10px] font-bold uppercase tracking-widest text-zinc-600">
65+
{new Date(item.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
66+
</p>
67+
<div className="h-0.5 w-0.5 rounded-full bg-zinc-800" />
68+
<p className="text-[10px] font-bold uppercase tracking-widest text-zinc-800">
69+
Protocol Signal
70+
</p>
71+
</div>
72+
</div>
5873

59-
{safeItems.length === 0 ? (
60-
<div className="rounded-2xl border border-[var(--fs-border)] bg-[var(--fs-background)]/50 px-6 py-8 text-center">
61-
<p className="text-sm font-semibold text-[var(--fs-text-primary)] mb-1">No activity yet</p>
62-
<p className="text-[12px] font-medium text-[var(--fs-text-muted)]">
63-
Refresh after new expenses or settlements to see the latest activity.
64-
</p>
74+
<div className="hidden sm:block">
75+
<div className="h-8 w-8 rounded-full border border-white/5 flex items-center justify-center text-[8px] font-black text-zinc-700 uppercase">
76+
FS
77+
</div>
78+
</div>
79+
</motion.div>
80+
);
81+
})}
82+
83+
{safeItems.length === 0 && (
84+
<div className="rounded-2xl border border-dashed border-white/5 p-10 text-center">
85+
<div className="mx-auto w-10 h-10 rounded-full border border-white/5 flex items-center justify-center text-zinc-800 mb-4">
86+
<Milestone size={18} />
87+
</div>
88+
<p className="text-[10px] font-black tracking-widest text-zinc-700 uppercase">NO_SIGNAL_DETECTED</p>
6589
</div>
66-
) : null}
90+
)}
6791
</div>
6892
</div>
6993
);
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
'use client';
2+
3+
import { motion } from 'framer-motion';
4+
5+
const chartData = [32, 58, 45, 72, 48, 65, 82, 55, 70, 90, 68, 85];
6+
const months = ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'];
7+
8+
export function SpendingChart() {
9+
return (
10+
<div className="group relative overflow-hidden rounded-2xl border border-white/5 bg-white/[0.01] p-6 transition-all hover:bg-white/[0.02]">
11+
<div className="flex items-center justify-between mb-8">
12+
<div>
13+
<h3 className="text-sm font-black italic tracking-tight text-white">SPENDING FLOW</h3>
14+
<p className="text-[10px] font-bold tracking-widest text-zinc-600 uppercase mt-0.5">Aggregate Protocol Metrics</p>
15+
</div>
16+
<div className="flex gap-4">
17+
<div className="flex items-center gap-2">
18+
<div className="h-1.5 w-1.5 rounded-full bg-purple-500" />
19+
<span className="text-[10px] font-bold text-zinc-400 uppercase tracking-widest">2026</span>
20+
</div>
21+
<div className="flex items-center gap-2">
22+
<div className="h-1.5 w-1.5 rounded-full bg-zinc-800" />
23+
<span className="text-[10px] font-bold text-zinc-700 uppercase tracking-widest">2025</span>
24+
</div>
25+
</div>
26+
</div>
27+
28+
<div className="flex items-end justify-between gap-2 h-44 sm:h-52">
29+
{chartData.map((h, i) => (
30+
<div key={i} className="flex-1 flex flex-col items-center gap-2 group/bar cursor-pointer">
31+
<div className="w-full relative h-[100%] flex items-end">
32+
<motion.div
33+
initial={{ height: 0 }}
34+
animate={{ height: `${h}%` }}
35+
transition={{ delay: 0.1 + i * 0.05, duration: 0.6, ease: [0.33, 1, 0.68, 1] }}
36+
className="w-full rounded-md bg-gradient-to-t from-purple-600/20 to-purple-400/10 border-t border-purple-500/40"
37+
/>
38+
39+
{/* Tooltip */}
40+
<div className="absolute -top-8 left-1/2 -translate-x-1/2 opacity-0 group-hover/bar:opacity-100 transition-opacity">
41+
<div className="bg-zinc-900 border border-white/10 px-2 py-1 rounded text-[9px] font-black text-white whitespace-nowrap shadow-xl">
42+
${(h * 32).toLocaleString()}
43+
</div>
44+
</div>
45+
46+
{/* Bar Glow */}
47+
<div className="absolute inset-0 bg-purple-500/10 blur-xl opacity-0 group-hover/bar:opacity-100 transition-opacity rounded-full" />
48+
</div>
49+
<span className="text-[9px] font-black text-zinc-800 group-hover/bar:text-zinc-500 transition-colors uppercase">
50+
{months[i]}
51+
</span>
52+
</div>
53+
))}
54+
</div>
55+
56+
{/* Decorative Overlays */}
57+
<div className="absolute top-0 right-0 p-4 pointer-events-none opacity-20">
58+
<div className="text-[8px] font-mono text-zinc-500 uppercase tracking-[0.5em] vertical-text">
59+
SIGNAL_STRENGTH_98%
60+
</div>
61+
</div>
62+
</div>
63+
);
64+
}

0 commit comments

Comments
 (0)