Skip to content

Commit f87c508

Browse files
authored
Merge pull request #170 from Meshmulla/main
Dashboard Data Flow & Mock State
2 parents 9fac5d6 + 0459813 commit f87c508

File tree

7 files changed

+270
-7
lines changed

7 files changed

+270
-7
lines changed
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { DashboardShell } from "@/components/dashboard/DashboardShell";
2+
import { DashboardProvider } from "@/components/dashboard/DashboardContext";
23

34
export default function DashboardLayout({
45
children,
@@ -7,7 +8,9 @@ export default function DashboardLayout({
78
}) {
89
return (
910
<div className="-mt-28 min-h-screen">
10-
<DashboardShell>{children}</DashboardShell>
11+
<DashboardProvider>
12+
<DashboardShell>{children}</DashboardShell>
13+
</DashboardProvider>
1114
</div>
1215
);
1316
}
Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import { DashboardStats } from "@/components/dashboard/DashboardStats";
2+
import { ActivityFeed } from "@/components/dashboard/ActivityFeed";
3+
14
export default function DashboardOverviewPage() {
25
return (
36
<div className="space-y-8">
@@ -10,12 +13,17 @@ export default function DashboardOverviewPage() {
1013
</p>
1114
</header>
1215

13-
<section className="rounded-xl border border-slate-800/80 bg-slate-900/50 p-6 backdrop-blur-sm">
14-
<h2 className="text-lg font-semibold text-white">Quick stats</h2>
15-
<p className="mt-2 text-sm text-slate-500">
16-
Stats and charts will appear in Part 2.
17-
</p>
18-
</section>
16+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
17+
<div className="lg:col-span-2 space-y-8">
18+
<section>
19+
<DashboardStats />
20+
</section>
21+
</div>
22+
23+
<div className="lg:col-span-1 h-[400px]">
24+
<ActivityFeed />
25+
</div>
26+
</div>
1927
</div>
2028
);
2129
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"use client";
2+
3+
import React from "react";
4+
import { useDashboard } from "./DashboardContext";
5+
import { ActivityItem, ActivityItemSkeleton } from "./ActivityItem";
6+
import { History } from "lucide-react";
7+
8+
export function ActivityFeed() {
9+
const { activities, isLoading } = useDashboard();
10+
11+
return (
12+
<div className="rounded-xl border border-slate-800/80 bg-slate-900/50 p-6 backdrop-blur-sm h-full flex flex-col">
13+
<div className="flex items-center gap-2 mb-6">
14+
<History className="h-5 w-5 text-indigo-400" />
15+
<h2 className="text-lg font-semibold text-white">Recent Activity</h2>
16+
</div>
17+
18+
<div className="space-y-2 pr-2 overflow-y-auto flex-1 [&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar-thumb]:bg-slate-700/50 hover:[&::-webkit-scrollbar-thumb]:bg-slate-600 [&::-webkit-scrollbar-thumb]:rounded-full">
19+
{isLoading ? (
20+
<>
21+
{[1, 2, 3, 4].map((i) => (
22+
<ActivityItemSkeleton key={i} />
23+
))}
24+
</>
25+
) : activities.length > 0 ? (
26+
activities.map((activity) => (
27+
<ActivityItem key={activity.id} activity={activity} />
28+
))
29+
) : (
30+
<p className="text-sm text-slate-500 py-4 text-center">No recent activity found.</p>
31+
)}
32+
</div>
33+
</div>
34+
);
35+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import React from "react";
2+
import { Heart, Plus, Trophy } from "lucide-react";
3+
import { ActivityData } from "./DashboardContext";
4+
5+
export function ActivityItem({ activity }: { activity: ActivityData }) {
6+
const getActivityDetails = () => {
7+
switch (activity.type) {
8+
case "donation":
9+
return {
10+
icon: <Heart className="h-4 w-4 text-rose-400" />,
11+
bg: "bg-rose-500/10 text-rose-500",
12+
text: (
13+
<>
14+
<span className="font-medium text-slate-200">{activity.user}</span> donated{" "}
15+
<span className="font-semibold text-emerald-400">{activity.amount}</span> to{" "}
16+
<span className="font-medium text-indigo-300">{activity.poolName}</span>
17+
</>
18+
),
19+
};
20+
case "pool_created":
21+
return {
22+
icon: <Plus className="h-4 w-4 text-indigo-400" />,
23+
bg: "bg-indigo-500/10 text-indigo-500",
24+
text: (
25+
<>
26+
<span className="font-medium text-slate-200">{activity.user}</span> created a new pool:{" "}
27+
<span className="font-medium text-indigo-300">{activity.poolName}</span>
28+
</>
29+
),
30+
};
31+
case "reward":
32+
return {
33+
icon: <Trophy className="h-4 w-4 text-amber-400" />,
34+
bg: "bg-amber-500/10 text-amber-500",
35+
text: (
36+
<>
37+
<span className="font-medium text-slate-200">{activity.user}</span> earned a reward of{" "}
38+
<span className="font-semibold text-amber-400">{activity.amount}</span>
39+
</>
40+
),
41+
};
42+
}
43+
};
44+
45+
const details = getActivityDetails();
46+
47+
return (
48+
<div className="flex items-start gap-4 p-4 transition-colors hover:bg-slate-800/30 rounded-lg">
49+
<div className={`mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-full ${details.bg}`}>
50+
{details.icon}
51+
</div>
52+
<div className="flex-1 space-y-1">
53+
<p className="text-sm text-slate-300">{details.text}</p>
54+
<p className="text-xs text-slate-500">{activity.timestamp}</p>
55+
</div>
56+
</div>
57+
);
58+
}
59+
60+
export function ActivityItemSkeleton() {
61+
return (
62+
<div className="flex items-start gap-4 p-4 animate-pulse">
63+
<div className="h-8 w-8 shrink-0 rounded-full bg-slate-700/50"></div>
64+
<div className="flex-1 space-y-2 py-1">
65+
<div className="h-4 w-3/4 rounded bg-slate-700/50"></div>
66+
<div className="h-3 w-20 rounded bg-slate-700/50"></div>
67+
</div>
68+
</div>
69+
);
70+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"use client";
2+
3+
import React, { createContext, useContext, useState, useEffect } from "react";
4+
5+
export interface StatData {
6+
title: string;
7+
value: string;
8+
iconName: string;
9+
trend?: string;
10+
}
11+
12+
export interface ActivityData {
13+
id: string;
14+
type: "donation" | "pool_created" | "reward";
15+
user: string;
16+
amount?: string;
17+
poolName?: string;
18+
timestamp: string;
19+
}
20+
21+
interface DashboardContextType {
22+
stats: StatData[];
23+
activities: ActivityData[];
24+
isLoading: boolean;
25+
}
26+
27+
const DashboardContext = createContext<DashboardContextType | undefined>(undefined);
28+
29+
export function DashboardProvider({ children }: { children: React.ReactNode }) {
30+
const [stats, setStats] = useState<StatData[]>([]);
31+
const [activities, setActivities] = useState<ActivityData[]>([]);
32+
const [isLoading, setIsLoading] = useState(true);
33+
34+
useEffect(() => {
35+
// Simulate network delay for fetching dashboard data
36+
const timer = setTimeout(() => {
37+
setStats([
38+
{ title: "Total Donated", value: "$45,231.89", iconName: "Banknotes", trend: "+12.5%" },
39+
{ title: "Active Pools", value: "24", iconName: "Droplets", trend: "+2" },
40+
{ title: "Impact Score", value: "89.2", iconName: "Activity", trend: "+4.1" },
41+
]);
42+
setActivities([
43+
{ id: "1", type: "donation", user: "Alice", amount: "$500", poolName: "Ocean Cleanup", timestamp: "10 mins ago" },
44+
{ id: "2", type: "pool_created", user: "Bob", poolName: "Reforestation Initiative", timestamp: "2 hours ago" },
45+
{ id: "3", type: "reward", user: "Charlie", amount: "50 NEVO", timestamp: "5 hours ago" },
46+
{ id: "4", type: "donation", user: "Diana", amount: "$150", poolName: "Local Shelter", timestamp: "1 day ago" },
47+
]);
48+
setIsLoading(false);
49+
}, 1500);
50+
51+
return () => clearTimeout(timer);
52+
}, []);
53+
54+
return (
55+
<DashboardContext.Provider value={{ stats, activities, isLoading }}>
56+
{children}
57+
</DashboardContext.Provider>
58+
);
59+
}
60+
61+
export function useDashboard() {
62+
const context = useContext(DashboardContext);
63+
if (context === undefined) {
64+
throw new Error("useDashboard must be used within a DashboardProvider");
65+
}
66+
return context;
67+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"use client";
2+
3+
import React from "react";
4+
import { useDashboard } from "./DashboardContext";
5+
import { StatCard, StatCardSkeleton } from "./StatCard";
6+
7+
export function DashboardStats() {
8+
const { stats, isLoading } = useDashboard();
9+
10+
if (isLoading) {
11+
return (
12+
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
13+
{[1, 2, 3].map((i) => (
14+
<StatCardSkeleton key={i} />
15+
))}
16+
</div>
17+
);
18+
}
19+
20+
return (
21+
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
22+
{stats.map((stat, i) => (
23+
<StatCard key={i} {...stat} />
24+
))}
25+
</div>
26+
);
27+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import React from "react";
2+
import { Banknote, Droplets, Activity, type LucideIcon } from "lucide-react";
3+
4+
const iconMap: Record<string, LucideIcon> = {
5+
Banknotes: Banknote,
6+
Droplets: Droplets,
7+
Activity: Activity,
8+
};
9+
10+
interface StatCardProps {
11+
title: string;
12+
value: string;
13+
iconName: string;
14+
trend?: string;
15+
}
16+
17+
export function StatCard({ title, value, iconName, trend }: StatCardProps) {
18+
const Icon = iconMap[iconName] || Activity;
19+
20+
return (
21+
<div className="rounded-xl border border-slate-800/80 bg-slate-900/50 p-6 backdrop-blur-sm transition-all hover:bg-slate-800/50">
22+
<div className="flex items-center justify-between">
23+
<h3 className="text-sm font-medium text-slate-400">{title}</h3>
24+
<div className="rounded-md bg-indigo-500/10 p-2">
25+
<Icon className="h-5 w-5 text-indigo-400" />
26+
</div>
27+
</div>
28+
<div className="mt-4 flex items-baseline gap-2">
29+
<span className="text-2xl font-bold tracking-tight text-white">{value}</span>
30+
{trend && (
31+
<span className={`text-xs font-medium ${trend.startsWith("+") ? "text-emerald-400" : "text-rose-400"}`}>
32+
{trend}
33+
</span>
34+
)}
35+
</div>
36+
</div>
37+
);
38+
}
39+
40+
export function StatCardSkeleton() {
41+
return (
42+
<div className="rounded-xl border border-slate-800/50 bg-slate-900/20 p-6 backdrop-blur-sm animate-pulse">
43+
<div className="flex items-center justify-between">
44+
<div className="h-4 w-24 rounded bg-slate-700/50"></div>
45+
<div className="h-9 w-9 rounded-md bg-slate-700/50"></div>
46+
</div>
47+
<div className="mt-4 flex items-baseline gap-2">
48+
<div className="h-8 w-32 rounded bg-slate-700/50"></div>
49+
<div className="h-4 w-12 rounded bg-slate-700/50"></div>
50+
</div>
51+
</div>
52+
);
53+
}

0 commit comments

Comments
 (0)