Skip to content

Commit 787a098

Browse files
committed
new roadmap ui
1 parent d4be342 commit 787a098

5 files changed

Lines changed: 858 additions & 4 deletions

File tree

Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
"use client";
2+
3+
import { useEffect, useState } from "react";
4+
import { Box } from "@/components/shared/dashboard";
5+
import { Badge } from "@codevs/ui/badge";
6+
import { Card, CardContent } from "@/components/ui/card";
7+
import { Skeleton } from "@/components/ui/skeleton/skeleton";
8+
import { useUserStore } from "@/store/codev-store";
9+
import { CheckCircle2, Circle } from "lucide-react";
10+
11+
interface RoadmapStep {
12+
id: string;
13+
step: string;
14+
}
15+
16+
interface RoadmapPhase {
17+
id: string;
18+
phase: string;
19+
title: string;
20+
pointRange: string;
21+
steps: RoadmapStep[];
22+
}
23+
24+
const roadmapData: RoadmapPhase[] = [
25+
{
26+
id: "1",
27+
phase: "Phase 1",
28+
title: "Intern",
29+
pointRange: "0-100 pts",
30+
steps: [
31+
{ id: "1", step: "Learn The Basics" },
32+
{ id: "2", step: "Hands-On Practice" },
33+
{ id: "3", step: "Version Control" },
34+
],
35+
},
36+
{
37+
id: "2",
38+
phase: "Phase 2",
39+
title: "Codev",
40+
pointRange: "100-200 pts",
41+
steps: [
42+
{ id: "1", step: "Deepen Language Proficiency" },
43+
{ id: "2", step: "Explore Frameworks and Libraries" },
44+
{ id: "3", step: "Work On Projects" },
45+
{ id: "4", step: "Development Practices" },
46+
],
47+
},
48+
{
49+
id: "3",
50+
phase: "Phase 3",
51+
title: "Mentor",
52+
pointRange: "200+ pts",
53+
steps: [
54+
{ id: "1", step: "Specialize" },
55+
{ id: "2", step: "Advanced Concepts" },
56+
{ id: "3", step: "Collaborate" },
57+
],
58+
},
59+
];
60+
61+
export default function DashboardProgressRoadmap() {
62+
const { user } = useUserStore();
63+
const [userPoints, setUserPoints] = useState(0);
64+
const [isLoading, setIsLoading] = useState(true);
65+
const [activePhaseIndex, setActivePhaseIndex] = useState(0);
66+
67+
useEffect(() => {
68+
if (!user?.id) return;
69+
70+
const fetchUserPoints = async () => {
71+
try {
72+
// Fetch total points from codev_points table
73+
const supabase = (await import("@/utils/supabase/client"))
74+
.createClientClientComponent();
75+
76+
if (!supabase) {
77+
console.error("Failed to initialize Supabase client.");
78+
setIsLoading(false);
79+
return;
80+
}
81+
82+
const { data: pointsData, error: pointsError } = await supabase
83+
.from("codev_points")
84+
.select("points")
85+
.eq("codev_id", user.id);
86+
87+
if (pointsError) throw pointsError;
88+
89+
// Calculate total skill points
90+
const totalSkillPoints =
91+
pointsData?.reduce((sum, record) => sum + (record.points || 0), 0) ||
92+
0;
93+
94+
// Fetch attendance points
95+
const { data: attendanceData } = await supabase
96+
.from("attendance_points")
97+
.select("points")
98+
.eq("codev_id", user.id)
99+
.single();
100+
101+
const attendancePoints = attendanceData?.points || 0;
102+
103+
// Fetch profile points
104+
let profilePoints = 0;
105+
try {
106+
const res = await fetch(`/api/profile-points/${user.id}`);
107+
if (res.ok) {
108+
const data = (await res.json()) as {
109+
success?: boolean;
110+
totalPoints?: number;
111+
};
112+
profilePoints = data?.totalPoints || 0;
113+
}
114+
} catch (error) {
115+
console.error("Failed to fetch profile points:", error);
116+
}
117+
118+
// Fetch social points
119+
const { data: socialData } = await supabase.rpc(
120+
"calculate_social_points",
121+
{ codev_id: user.id },
122+
);
123+
const socialPoints = socialData || 0;
124+
125+
// Calculate total points
126+
const totalPoints =
127+
totalSkillPoints + attendancePoints + profilePoints + socialPoints;
128+
129+
setUserPoints(totalPoints);
130+
131+
// Determine active phase based on total points
132+
if (totalPoints < 100) {
133+
setActivePhaseIndex(0);
134+
} else if (totalPoints < 200) {
135+
setActivePhaseIndex(1);
136+
} else {
137+
setActivePhaseIndex(2);
138+
}
139+
} catch (error) {
140+
console.error("Error fetching user points:", error);
141+
} finally {
142+
setIsLoading(false);
143+
}
144+
};
145+
146+
fetchUserPoints();
147+
}, [user]);
148+
149+
const getPhaseStatus = (index: number) => {
150+
if (index < activePhaseIndex) return "completed";
151+
if (index === activePhaseIndex) return "active";
152+
return "upcoming";
153+
};
154+
155+
const getPhaseColor = (status: string) => {
156+
switch (status) {
157+
case "completed":
158+
return "border-green-500 bg-green-50 dark:bg-green-950/30";
159+
case "active":
160+
return "border-blue-500 bg-blue-50 dark:bg-blue-950/30";
161+
default:
162+
return "border-gray-300 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/30";
163+
}
164+
};
165+
166+
return (
167+
<Box className="flex w-full flex-1 flex-col gap-4">
168+
<div className="flex items-center justify-between">
169+
<p className="text-2xl font-semibold">Your Learning Roadmap</p>
170+
{!isLoading && (
171+
<Badge variant="info" className="text-sm">
172+
{userPoints} points
173+
</Badge>
174+
)}
175+
</div>
176+
177+
{isLoading ? (
178+
<div className="flex flex-col gap-4">
179+
<Skeleton className="h-32 w-full rounded-lg" />
180+
<Skeleton className="h-32 w-full rounded-lg" />
181+
<Skeleton className="h-32 w-full rounded-lg" />
182+
</div>
183+
) : (
184+
<div className="relative">
185+
{/* Connection Line for Desktop */}
186+
<div className="absolute left-6 top-0 hidden h-full w-0.5 bg-gradient-to-b from-green-500 via-blue-500 to-gray-300 dark:to-gray-700 md:block" />
187+
188+
<div className="flex flex-col gap-6">
189+
{roadmapData.map((phase, index) => {
190+
const status = getPhaseStatus(index);
191+
const isCompleted = status === "completed";
192+
const isActive = status === "active";
193+
194+
return (
195+
<div key={phase.id} className="relative">
196+
{/* Status Indicator Circle */}
197+
<div className="absolute -left-1 top-6 z-10 hidden md:block">
198+
{isCompleted ? (
199+
<CheckCircle2 className="h-8 w-8 rounded-full bg-white text-green-500 dark:bg-gray-950" />
200+
) : isActive ? (
201+
<div className="h-8 w-8 rounded-full border-4 border-blue-500 bg-white dark:bg-gray-950">
202+
<div className="h-full w-full animate-pulse rounded-full bg-blue-500" />
203+
</div>
204+
) : (
205+
<Circle className="h-8 w-8 rounded-full bg-white text-gray-400 dark:bg-gray-950" />
206+
)}
207+
</div>
208+
209+
{/* Phase Card */}
210+
<Card
211+
className={`ml-0 md:ml-16 transition-all duration-300 ${getPhaseColor(status)} ${
212+
isActive ? "shadow-lg scale-[1.02]" : ""
213+
}`}
214+
>
215+
<CardContent className="p-6">
216+
<div className="flex flex-col gap-4">
217+
{/* Phase Header */}
218+
<div className="flex items-start justify-between gap-4">
219+
<div className="flex-1">
220+
<div className="flex items-center gap-2 mb-1">
221+
<Badge
222+
variant={
223+
isCompleted
224+
? "success"
225+
: isActive
226+
? "default"
227+
: "outline"
228+
}
229+
className="text-xs"
230+
>
231+
{phase.phase}
232+
</Badge>
233+
<span className="text-sm text-muted-foreground">
234+
{phase.pointRange}
235+
</span>
236+
</div>
237+
<h3 className="text-xl font-bold text-foreground">
238+
{phase.title}
239+
</h3>
240+
</div>
241+
{/* Mobile Status Indicator */}
242+
<div className="md:hidden">
243+
{isCompleted ? (
244+
<CheckCircle2 className="h-6 w-6 text-green-500" />
245+
) : isActive ? (
246+
<div className="h-6 w-6 rounded-full border-2 border-blue-500 bg-blue-500/20" />
247+
) : (
248+
<Circle className="h-6 w-6 text-gray-400" />
249+
)}
250+
</div>
251+
</div>
252+
253+
{/* Steps */}
254+
<div className="grid gap-2 sm:grid-cols-2">
255+
{phase.steps.map((step) => (
256+
<div
257+
key={step.id}
258+
className="flex items-center gap-2 rounded-lg bg-background/50 p-3 transition-colors hover:bg-background"
259+
>
260+
<div
261+
className={`flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full text-xs font-semibold ${
262+
isCompleted
263+
? "bg-green-500 text-white"
264+
: isActive
265+
? "bg-blue-500 text-white"
266+
: "bg-gray-300 text-gray-600 dark:bg-gray-700 dark:text-gray-400"
267+
}`}
268+
>
269+
{step.id}
270+
</div>
271+
<span
272+
className={`text-sm ${
273+
isCompleted || isActive
274+
? "font-medium text-foreground"
275+
: "text-muted-foreground"
276+
}`}
277+
>
278+
{step.step}
279+
</span>
280+
</div>
281+
))}
282+
</div>
283+
284+
{/* Progress Message */}
285+
{isActive && (
286+
<div className="mt-2 rounded-md bg-blue-100 p-3 dark:bg-blue-900/30">
287+
<p className="text-sm font-medium text-blue-900 dark:text-blue-100">
288+
🎯 You're currently on this phase! Keep up the great work.
289+
</p>
290+
</div>
291+
)}
292+
{isCompleted && (
293+
<div className="mt-2 rounded-md bg-green-100 p-3 dark:bg-green-900/30">
294+
<p className="text-sm font-medium text-green-900 dark:text-green-100">
295+
✅ Completed! You've mastered this phase.
296+
</p>
297+
</div>
298+
)}
299+
</div>
300+
</CardContent>
301+
</Card>
302+
</div>
303+
);
304+
})}
305+
</div>
306+
</div>
307+
)}
308+
</Box>
309+
);
310+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { BaseEdge, EdgeProps, getBezierPath } from "reactflow";
2+
3+
export const CustomRoadmapEdge = ({
4+
id,
5+
sourceX,
6+
sourceY,
7+
targetX,
8+
targetY,
9+
sourcePosition,
10+
targetPosition,
11+
data,
12+
}: EdgeProps) => {
13+
const [edgePath] = getBezierPath({
14+
sourceX,
15+
sourceY,
16+
sourcePosition,
17+
targetX,
18+
targetY,
19+
targetPosition,
20+
});
21+
22+
const isActive = data?.isActive ?? false;
23+
24+
return (
25+
<>
26+
{/* Background path for glow effect */}
27+
{isActive && (
28+
<path
29+
d={edgePath}
30+
fill="none"
31+
stroke="url(#gradient-glow)"
32+
strokeWidth={6}
33+
className="opacity-40"
34+
/>
35+
)}
36+
37+
{/* Main path */}
38+
<BaseEdge
39+
id={id}
40+
path={edgePath}
41+
style={{
42+
stroke: isActive ? "url(#gradient-edge)" : "#9ca3af",
43+
strokeWidth: isActive ? 4 : 2,
44+
strokeDasharray: isActive ? undefined : "5,5",
45+
}}
46+
/>
47+
48+
{/* Animated dots along the path when active */}
49+
{isActive && (
50+
<circle r="4" fill="#8b5cf6" className="animate-pulse">
51+
<animateMotion dur="2s" repeatCount="indefinite" path={edgePath} />
52+
</circle>
53+
)}
54+
55+
{/* Define gradients for edges */}
56+
<defs>
57+
<linearGradient id="gradient-edge" gradientUnits="userSpaceOnUse">
58+
<stop offset="0%" stopColor="#3b82f6" />
59+
<stop offset="50%" stopColor="#8b5cf6" />
60+
<stop offset="100%" stopColor="#a855f7" />
61+
</linearGradient>
62+
<linearGradient id="gradient-glow" gradientUnits="userSpaceOnUse">
63+
<stop offset="0%" stopColor="#60a5fa" />
64+
<stop offset="50%" stopColor="#a78bfa" />
65+
<stop offset="100%" stopColor="#c084fc" />
66+
</linearGradient>
67+
</defs>
68+
</>
69+
);
70+
};

0 commit comments

Comments
 (0)