Skip to content

Commit 499038b

Browse files
authored
Merge pull request #804 from dhruv-jani-0808/feature/roadmap-checklist-tracker
feat: add interactive roadmap checklist tracker with localStorage persistence
2 parents 26f492c + ebe922e commit 499038b

4 files changed

Lines changed: 507 additions & 84 deletions

File tree

src/app/roadmaps/[id]/page.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Metadata } from 'next';
22
import SkillTreeVisualizer from '@/components/features/SkillTreeVisualizer';
33
import ComingSoonRoadmap from '@/components/features/ComingSoonRoadmap';
4+
import RoadmapChecklist from '@/components/features/RoadmapChecklist';
45
import React from 'react';
56
import Link from 'next/link';
67
import { ArrowLeft } from 'lucide-react';
@@ -67,6 +68,24 @@ const ROADMAPS_CONFIG: Record<string, RoadmapConfig> = {
6768
},
6869
};
6970

71+
// ── Checklist nodes data (mirrors pathsData in SkillTreeVisualizer) ────────
72+
type ChecklistNodeEntry = { id: string; label: string; desc: string };
73+
74+
const ROADMAP_CHECKLIST_NODES: Partial<Record<string, ChecklistNodeEntry[]>> = {
75+
frontend: [
76+
{ id: '1', label: 'HTML/CSS', desc: 'Master the building blocks of the web — semantic HTML5 elements and modern CSS layouts with Flexbox and Grid.' },
77+
{ id: '2', label: 'JavaScript', desc: 'Learn core programming concepts, DOM manipulation, events, async/await, and the Fetch API.' },
78+
{ id: '3', label: 'Version Control (Git)', desc: 'Understand Git fundamentals — commits, branches, merges, pull requests, and working with GitHub.' },
79+
{ id: '4', label: 'React', desc: 'Build interactive UIs with React components, hooks (useState, useEffect), and the component lifecycle.' },
80+
{ id: '5', label: 'Next.js', desc: 'Ship production-ready React apps with file-based routing, SSR, SSG, Server Actions, and API routes.' },
81+
],
82+
backend: [
83+
{ id: '1', label: 'Databases', desc: 'Understand relational (SQL) vs non-relational (NoSQL) databases, schema design, indexing, and queries.' },
84+
{ id: '2', label: 'Node.js', desc: 'Learn server-side JavaScript — the event loop, modules, npm, streams, and building HTTP servers.' },
85+
{ id: '3', label: 'APIs', desc: 'Design and build REST and GraphQL APIs, handle authentication, and follow best practices for API security.' },
86+
],
87+
};
88+
7089
export async function generateStaticParams() {
7190
return Object.keys(ROADMAPS_CONFIG).map((id) => ({
7291
id,
@@ -165,6 +184,24 @@ export default async function RoadmapPage({ params }: Props) {
165184
</div>
166185

167186
<SkillTreeVisualizer initialPath={config.visualizerPath} />
187+
188+
{/* Roadmap Checklist Tracker — available to all users (localStorage for guests) */}
189+
{ROADMAP_CHECKLIST_NODES[resolvedParams.id] && (
190+
<div className="flex flex-col items-center gap-3 pt-4">
191+
<div className="w-full max-w-[800px] flex items-center gap-3">
192+
<div className="flex-1 h-px bg-gradient-to-r from-transparent via-slate-700/60 to-transparent" />
193+
<span className="text-xs font-semibold uppercase tracking-widest text-slate-600">
194+
Topic Checklist
195+
</span>
196+
<div className="flex-1 h-px bg-gradient-to-r from-transparent via-slate-700/60 to-transparent" />
197+
</div>
198+
<RoadmapChecklist
199+
pathId={config.visualizerPath ?? resolvedParams.id}
200+
title={config.title}
201+
nodes={ROADMAP_CHECKLIST_NODES[resolvedParams.id]!}
202+
/>
203+
</div>
204+
)}
168205
</div>
169206
</main>
170207
);
Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
'use client';
2+
3+
import React, { useState } from 'react';
4+
import { motion, AnimatePresence } from 'framer-motion';
5+
import {
6+
CheckCircle2,
7+
Circle,
8+
ChevronDown,
9+
ChevronUp,
10+
RotateCcw,
11+
Trophy,
12+
Flame,
13+
Zap,
14+
} from 'lucide-react';
15+
import { useLearningProgress } from '@/hooks/useLearningProgress';
16+
import { useAuth } from '@/context/AuthContext';
17+
18+
// ── Types ──────────────────────────────────────────────────────────────────
19+
20+
interface ChecklistNode {
21+
id: string;
22+
label: string;
23+
desc: string;
24+
}
25+
26+
interface RoadmapChecklistProps {
27+
/** The path identifier, e.g. "Frontend" or "Backend" */
28+
pathId: string;
29+
/** Human-readable title for this roadmap, e.g. "Frontend Developer Roadmap" */
30+
title: string;
31+
/** All nodes in this roadmap */
32+
nodes: ChecklistNode[];
33+
}
34+
35+
// ── Helper: motivational label based on progress ──────────────────────────
36+
37+
function getMotivationalLabel(percent: number): { text: string; emoji: string } {
38+
if (percent === 0) return { text: 'Ready to begin your journey?', emoji: '🚀' };
39+
if (percent < 25) return { text: 'Great start! Keep going!', emoji: '🌱' };
40+
if (percent < 50) return { text: "You're building momentum!", emoji: '⚡' };
41+
if (percent < 75) return { text: 'More than halfway there!', emoji: '🔥' };
42+
if (percent < 100) return { text: 'Almost mastered! Push through!', emoji: '🏆' };
43+
return { text: 'Path mastered! Legendary!', emoji: '🎉' };
44+
}
45+
46+
// ── Sub-component: individual checklist row ───────────────────────────────
47+
48+
function ChecklistRow({
49+
node,
50+
pathId,
51+
index,
52+
}: {
53+
node: ChecklistNode;
54+
pathId: string;
55+
index: number;
56+
}) {
57+
const { isNodeCompleted, toggleNode } = useLearningProgress();
58+
const isCompleted = isNodeCompleted(pathId, node.id);
59+
const [isExpanded, setIsExpanded] = useState(false);
60+
61+
return (
62+
<motion.div
63+
initial={{ opacity: 0, x: -16 }}
64+
animate={{ opacity: 1, x: 0 }}
65+
transition={{ duration: 0.3, delay: index * 0.06 }}
66+
className={`
67+
group relative flex flex-col rounded-xl border transition-all duration-300 overflow-hidden
68+
${
69+
isCompleted
70+
? 'border-emerald-500/40 bg-emerald-950/20'
71+
: 'border-slate-700/50 bg-slate-900/30 hover:border-slate-600/70'
72+
}
73+
`}
74+
>
75+
<div className="flex items-center gap-4 p-4">
76+
{/* Checkbox button */}
77+
<button
78+
id={`checklist-node-${pathId}-${node.id}`}
79+
aria-label={`Mark "${node.label}" as ${isCompleted ? 'incomplete' : 'complete'}`}
80+
onClick={() => toggleNode(pathId, node.id)}
81+
className="flex-shrink-0 focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500 rounded-full transition-transform hover:scale-110 active:scale-95"
82+
>
83+
<AnimatePresence mode="wait">
84+
{isCompleted ? (
85+
<motion.span
86+
key="checked"
87+
initial={{ scale: 0.5, opacity: 0 }}
88+
animate={{ scale: 1, opacity: 1 }}
89+
exit={{ scale: 0.5, opacity: 0 }}
90+
transition={{ duration: 0.18 }}
91+
>
92+
<CheckCircle2 size={26} className="text-emerald-500 drop-shadow-[0_0_6px_rgba(52,211,153,0.6)]" />
93+
</motion.span>
94+
) : (
95+
<motion.span
96+
key="unchecked"
97+
initial={{ scale: 0.5, opacity: 0 }}
98+
animate={{ scale: 1, opacity: 1 }}
99+
exit={{ scale: 0.5, opacity: 0 }}
100+
transition={{ duration: 0.18 }}
101+
>
102+
<Circle size={26} className="text-slate-600 group-hover:text-slate-400 transition-colors" />
103+
</motion.span>
104+
)}
105+
</AnimatePresence>
106+
</button>
107+
108+
{/* Node step number badge */}
109+
<span
110+
className={`
111+
flex-shrink-0 text-[10px] font-bold w-6 h-6 rounded-full flex items-center justify-center
112+
${isCompleted ? 'bg-emerald-500/20 text-emerald-400' : 'bg-slate-800 text-slate-500'}
113+
`}
114+
>
115+
{index + 1}
116+
</span>
117+
118+
{/* Label */}
119+
<span
120+
className={`flex-1 text-sm font-semibold tracking-wide transition-colors duration-200 ${
121+
isCompleted ? 'text-emerald-400 line-through decoration-emerald-600/50' : 'text-slate-200'
122+
}`}
123+
>
124+
{node.label}
125+
</span>
126+
127+
{/* Expand toggle */}
128+
<button
129+
aria-label={isExpanded ? 'Collapse description' : 'Expand description'}
130+
onClick={() => setIsExpanded((v) => !v)}
131+
className="flex-shrink-0 text-slate-500 hover:text-slate-300 transition-colors p-1 rounded-lg hover:bg-slate-800/60"
132+
>
133+
{isExpanded ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
134+
</button>
135+
</div>
136+
137+
{/* Expandable description */}
138+
<AnimatePresence>
139+
{isExpanded && (
140+
<motion.div
141+
key="desc"
142+
initial={{ height: 0, opacity: 0 }}
143+
animate={{ height: 'auto', opacity: 1 }}
144+
exit={{ height: 0, opacity: 0 }}
145+
transition={{ duration: 0.22 }}
146+
className="overflow-hidden"
147+
>
148+
<div className="px-14 pb-4 text-xs text-slate-400 leading-relaxed border-t border-slate-800/60 pt-3">
149+
{node.desc}
150+
</div>
151+
</motion.div>
152+
)}
153+
</AnimatePresence>
154+
155+
{/* Completed shimmer accent */}
156+
{isCompleted && (
157+
<motion.div
158+
initial={{ width: 0 }}
159+
animate={{ width: '100%' }}
160+
transition={{ duration: 0.4 }}
161+
className="absolute bottom-0 left-0 h-[2px] bg-gradient-to-r from-emerald-500 to-teal-400 rounded-full"
162+
/>
163+
)}
164+
</motion.div>
165+
);
166+
}
167+
168+
// ── Main Component ─────────────────────────────────────────────────────────
169+
170+
export default function RoadmapChecklist({ pathId, title, nodes }: RoadmapChecklistProps) {
171+
const { user } = useAuth();
172+
const { isNodeCompleted, resetProgress } = useLearningProgress();
173+
const [showResetConfirm, setShowResetConfirm] = useState(false);
174+
175+
const completedCount = nodes.filter((n) => isNodeCompleted(pathId, n.id)).length;
176+
const totalCount = nodes.length;
177+
const progressPercent = totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0;
178+
const { text: motivationalText, emoji } = getMotivationalLabel(progressPercent);
179+
180+
const handleReset = async () => {
181+
await resetProgress(pathId);
182+
setShowResetConfirm(false);
183+
};
184+
185+
return (
186+
<section
187+
id={`roadmap-checklist-${pathId.toLowerCase()}`}
188+
aria-label={`${title} checklist tracker`}
189+
className="w-full max-w-[800px] mx-auto mt-2"
190+
>
191+
{/* ── Header Card ── */}
192+
<div className="relative overflow-hidden rounded-2xl border border-slate-700/60 bg-gradient-to-br from-slate-900 via-[#0f1115] to-slate-950 shadow-2xl mb-4">
193+
{/* Decorative glow orbs */}
194+
<div className="pointer-events-none absolute -top-8 -right-8 w-40 h-40 rounded-full bg-emerald-500/8 blur-3xl" />
195+
<div className="pointer-events-none absolute -bottom-8 -left-8 w-32 h-32 rounded-full bg-teal-500/6 blur-3xl" />
196+
197+
<div className="relative p-6">
198+
{/* Title row */}
199+
<div className="flex flex-col sm:flex-row sm:items-start justify-between gap-4 mb-5">
200+
<div className="space-y-1">
201+
<div className="flex items-center gap-2 mb-1">
202+
<Flame size={18} className="text-orange-500 animate-pulse" />
203+
<span className="text-xs font-semibold uppercase tracking-widest text-slate-500">
204+
Checklist Tracker
205+
</span>
206+
</div>
207+
<h2 className="text-xl font-extrabold text-white leading-tight">{title}</h2>
208+
<p className="text-xs text-slate-400 max-w-xs">
209+
{user
210+
? 'Progress synced to your account in real-time.'
211+
: 'Progress saved locally in your browser. Sign in to sync across devices.'}
212+
</p>
213+
</div>
214+
215+
{/* Stats bubble */}
216+
<div className="flex flex-col items-start sm:items-end gap-1 flex-shrink-0">
217+
<div className="flex items-center gap-2 bg-slate-800/60 border border-slate-700/60 rounded-xl px-4 py-2">
218+
<Zap size={14} className={progressPercent > 0 ? 'text-yellow-400' : 'text-slate-600'} />
219+
<span className="text-2xl font-black text-white font-mono">{progressPercent}%</span>
220+
</div>
221+
<span className="text-xs text-slate-500 font-mono">
222+
{completedCount} / {totalCount} completed
223+
</span>
224+
</div>
225+
</div>
226+
227+
{/* Progress bar */}
228+
<div className="space-y-2">
229+
<div className="h-3 w-full bg-slate-800/80 rounded-full overflow-hidden border border-slate-700/40">
230+
<motion.div
231+
className="h-full rounded-full bg-gradient-to-r from-emerald-600 via-emerald-500 to-teal-400 relative"
232+
initial={{ width: 0 }}
233+
animate={{ width: `${progressPercent}%` }}
234+
transition={{ duration: 0.6, ease: 'easeOut' }}
235+
>
236+
{progressPercent > 10 && (
237+
<div className="absolute inset-0 bg-gradient-to-r from-white/10 via-transparent to-transparent rounded-full" />
238+
)}
239+
</motion.div>
240+
</div>
241+
242+
{/* Motivational label */}
243+
<div className="flex items-center justify-between">
244+
<span className="text-xs text-slate-400">
245+
{emoji} {motivationalText}
246+
</span>
247+
{progressPercent === 100 && (
248+
<span className="inline-flex items-center gap-1 text-xs font-semibold text-yellow-400 bg-yellow-400/10 border border-yellow-400/20 rounded-full px-3 py-0.5">
249+
<Trophy size={12} />
250+
Path Mastered!
251+
</span>
252+
)}
253+
</div>
254+
</div>
255+
256+
{/* Reset button row */}
257+
<div className="flex items-center justify-end mt-4 pt-4 border-t border-slate-800/60">
258+
<AnimatePresence mode="wait">
259+
{!showResetConfirm ? (
260+
<motion.button
261+
key="reset-btn"
262+
initial={{ opacity: 0 }}
263+
animate={{ opacity: 1 }}
264+
exit={{ opacity: 0 }}
265+
onClick={() => setShowResetConfirm(true)}
266+
disabled={completedCount === 0}
267+
className="inline-flex items-center gap-1.5 text-xs text-slate-500 hover:text-red-400 disabled:opacity-30 disabled:cursor-not-allowed transition-colors duration-200 py-1 px-2 rounded-lg hover:bg-red-950/30"
268+
>
269+
<RotateCcw size={13} />
270+
Reset Progress
271+
</motion.button>
272+
) : (
273+
<motion.div
274+
key="confirm-row"
275+
initial={{ opacity: 0, scale: 0.95 }}
276+
animate={{ opacity: 1, scale: 1 }}
277+
exit={{ opacity: 0, scale: 0.95 }}
278+
className="flex items-center gap-3"
279+
>
280+
<span className="text-xs text-red-400">Reset all progress for this path?</span>
281+
<button
282+
onClick={handleReset}
283+
className="text-xs font-semibold text-red-400 border border-red-500/40 rounded-lg px-3 py-1 hover:bg-red-950/40 transition-colors"
284+
>
285+
Yes, reset
286+
</button>
287+
<button
288+
onClick={() => setShowResetConfirm(false)}
289+
className="text-xs text-slate-400 border border-slate-700/50 rounded-lg px-3 py-1 hover:bg-slate-800/60 transition-colors"
290+
>
291+
Cancel
292+
</button>
293+
</motion.div>
294+
)}
295+
</AnimatePresence>
296+
</div>
297+
</div>
298+
</div>
299+
300+
{/* ── Checklist Rows ── */}
301+
<div className="space-y-2.5" role="list" aria-label="Roadmap topics checklist">
302+
{nodes.map((node, index) => (
303+
<ChecklistRow
304+
key={node.id}
305+
node={node}
306+
pathId={pathId}
307+
index={index}
308+
/>
309+
))}
310+
</div>
311+
312+
{/* ── Guest CTA ── */}
313+
{!user && (
314+
<motion.div
315+
initial={{ opacity: 0, y: 8 }}
316+
animate={{ opacity: 1, y: 0 }}
317+
transition={{ delay: 0.4 }}
318+
className="mt-6 rounded-xl border border-slate-700/40 bg-slate-900/30 px-5 py-4 text-center"
319+
>
320+
<p className="text-xs text-slate-500 leading-relaxed">
321+
🔒 Your progress is saved in this browser only.{' '}
322+
<a
323+
href="/login"
324+
className="text-emerald-400 hover:text-emerald-300 underline underline-offset-2 transition-colors font-medium"
325+
>
326+
Sign in
327+
</a>{' '}
328+
to sync your progress across all your devices.
329+
</p>
330+
</motion.div>
331+
)}
332+
</section>
333+
);
334+
}

0 commit comments

Comments
 (0)