Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

149 changes: 149 additions & 0 deletions src/app/roadmaps/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { Metadata } from 'next';
import SkillTreeVisualizer from '@/components/features/SkillTreeVisualizer';
import ComingSoonRoadmap from '@/components/features/ComingSoonRoadmap';
import React from 'react';
import Link from 'next/link';
import { ArrowLeft } from 'lucide-react';

type Props = {
params: Promise<{ id: string }>;
};

// Disable dynamic routes since this is a static build
export const dynamicParams = false;

interface RoadmapConfig {
title: string;
description: string;
techDetails: string;
isAvailable: boolean;
visualizerPath?: 'Frontend' | 'Backend';
}

const ROADMAPS_CONFIG: Record<string, RoadmapConfig> = {
frontend: {
title: 'Frontend Developer Roadmap',
description: 'Master modern frontend development with our curated step-by-step Frontend developer roadmap. Learn HTML/CSS, JavaScript, React, and Next.js.',
techDetails: 'HTML/CSS, JavaScript, React, Next.js',
isAvailable: true,
visualizerPath: 'Frontend',
},
backend: {
title: 'Backend Developer Roadmap',
description: 'Learn backend engineering with our curated backend roadmap. Master databases, Node.js, and API development.',
techDetails: 'Databases, Node.js, API design',
isAvailable: true,
visualizerPath: 'Backend',
},
devops: {
title: 'DevOps Mastery Roadmap',
description: 'DevOps learning path: Docker, Kubernetes, CI/CD, and AWS infrastructure.',
techDetails: 'Docker & Kubernetes, CI/CD Pipelines, AWS Infrastructure',
isAvailable: false,
},
'python-ai': {
title: 'Python for AI Roadmap',
description: 'Learn AI and machine learning: PyTorch, Neural Networks, and LLM Integration.',
techDetails: 'PyTorch Fundamentals, Neural Networks, LLM Integration',
isAvailable: false,
},
'full-stack-react': {
title: 'Full Stack React Roadmap',
description: 'Master Next.js App Router, Server Actions, PostgreSQL, and Prisma.',
techDetails: 'Next.js App Router, Server Actions, PostgreSQL & Prisma',
isAvailable: false,
},
'web3-development': {
title: 'Web3 Development Roadmap',
description: 'Step into Web3: Solidity Smart Contracts, Ethers.js, and DApp Architecture.',
techDetails: 'Solidity Smart Contracts, Ethers.js, DApp Architecture',
isAvailable: false,
},
};

export async function generateStaticParams() {
return Object.keys(ROADMAPS_CONFIG).map((id) => ({
id,
}));
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
const resolvedParams = await params;
const config = ROADMAPS_CONFIG[resolvedParams.id];

if (!config) {
return {
title: 'Roadmap Not Found | DevPath',
description: 'The requested roadmap could not be found.',
};
}

const fullTitle = `${config.title} | DevPath`;

return {
title: fullTitle,
description: config.description,
openGraph: {
title: fullTitle,
description: config.description,
url: `https://devpath.community/roadmaps/${resolvedParams.id}`,
siteName: 'DevPath',
images: [
{
url: 'https://devpath.community/og-roadmaps.png',
width: 1200,
height: 630,
alt: `${config.title} illustration on DevPath`,
},
],
type: 'website',
},
twitter: {
card: 'summary_large_image',
title: fullTitle,
description: config.description,
images: ['https://devpath.community/og-roadmaps.png'],
},
};
}

export default async function RoadmapPage({ params }: Props) {
const resolvedParams = await params;
const config = ROADMAPS_CONFIG[resolvedParams.id];

if (!config) {
return (
<div className="min-h-screen bg-[#0b0c10] text-slate-100 flex flex-col items-center justify-center p-6 text-center">
<h1 className="text-3xl font-extrabold text-white mb-2">Roadmap Not Found</h1>
<p className="text-slate-400 text-sm mb-4">The requested roadmap does not exist.</p>
<Link href="/paths" className="text-primary hover:underline text-sm">
Return to Learning Paths
</Link>
</div>
);
}

if (!config.isAvailable) {
return <ComingSoonRoadmap title={config.title} techDetails={config.techDetails} />;
}

return (
<main className="min-h-screen bg-[#0b0c10] pt-24 pb-12 px-4 md:px-8">
<div className="max-w-7xl mx-auto space-y-8">
<div className="flex flex-col gap-2">
<Link
href="/paths"
className="inline-flex items-center gap-2 text-sm text-slate-400 hover:text-white transition-colors group w-fit"
>
<ArrowLeft size={16} className="group-hover:-translate-x-1 transition-transform" />
<span>Back to Paths</span>
</Link>
<h1 className="text-3xl font-extrabold text-white mt-2">{config.title}</h1>
<p className="text-sm text-slate-400 max-w-2xl">{config.description}</p>
</div>

<SkillTreeVisualizer initialPath={config.visualizerPath} />
</div>
</main>
);
}
116 changes: 116 additions & 0 deletions src/components/features/ComingSoonRoadmap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
'use client';

import React, { useState } from 'react';
import { Bell, ArrowLeft, Mail, Sparkles } from 'lucide-react';
import Link from 'next/link';

interface ComingSoonRoadmapProps {
title: string;
techDetails: string;
}

export default function ComingSoonRoadmap({ title, techDetails }: ComingSoonRoadmapProps) {
const [email, setEmail] = useState('');
const [isSubmitted, setIsSubmitted] = useState(false);

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (email.trim()) {
setIsSubmitted(true);
}
};

return (
<div className="min-h-screen bg-[#0b0c10] text-slate-100 flex flex-col items-center justify-center p-6 relative overflow-hidden">
{/* Background glow effects */}
<div className="absolute top-1/4 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[500px] h-[500px] bg-primary/5 rounded-full blur-3xl pointer-events-none"></div>
<div className="absolute bottom-1/4 left-1/3 w-[300px] h-[300px] bg-emerald-500/5 rounded-full blur-3xl pointer-events-none"></div>

<div className="w-full max-w-xl z-10 space-y-8">
{/* Back Link */}
<Link
href="/paths"
className="inline-flex items-center gap-2 text-sm text-slate-400 hover:text-white transition-colors group"
>
<ArrowLeft size={16} className="group-hover:-translate-x-1 transition-transform" />
<span>Back to Learning Paths</span>
</Link>

{/* Card */}
<div className="bg-slate-950/40 border border-slate-900 rounded-2xl p-8 md:p-10 shadow-2xl relative overflow-hidden backdrop-blur-xl">
<div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-orange-500 via-primary to-purple-500"></div>

{!isSubmitted ? (
<div className="space-y-6">
<div className="space-y-2">
<div className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider bg-orange-500/10 text-orange-400 border border-orange-500/20 mb-2">
<Sparkles size={12} />
Roadmap Under Construction
</div>
<h1 className="text-3xl md:text-4xl font-extrabold tracking-tight text-white">
{title}
</h1>
<p className="text-sm text-slate-400 leading-relaxed">
We are currently crafting a comprehensive curriculum for <strong className="text-slate-200">{title}</strong>. This roadmap will outline step-by-step topics, milestones, and guided project recommendations.
</p>
</div>

{/* Stack Preview */}
<div className="p-4 bg-slate-900/30 border border-slate-900 rounded-xl space-y-1.5">
<span className="text-xs font-bold text-slate-400 uppercase tracking-widest font-mono">Curriculum Highlights</span>
<p className="text-sm text-emerald-400 font-semibold">{techDetails}</p>
</div>

{/* Form */}
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-1">
<label htmlFor="email" className="text-xs font-semibold text-slate-400">
Get notified when this roadmap launches
</label>
<div className="relative">
<Mail size={18} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-slate-500" />
<input
id="email"
type="email"
required
placeholder="Enter your email address"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full pl-11 pr-4 py-3 bg-slate-950 border border-slate-800 rounded-xl text-sm focus:border-primary focus:outline-none transition-colors text-white placeholder:text-slate-600"
/>
</div>
</div>
<button
type="submit"
className="w-full py-3 bg-primary hover:bg-primary/95 text-white font-bold rounded-xl transition-all duration-300 shadow-lg shadow-primary/20 flex items-center justify-center gap-2 hover:scale-[1.01] active:scale-[0.99]"
>
<Bell size={16} />
<span>Notify Me</span>
</button>
</form>
</div>
) : (
<div className="text-center py-6 space-y-6">
<div className="w-16 h-16 bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 rounded-2xl flex items-center justify-center text-3xl mx-auto animate-bounce">
</div>
<div className="space-y-2">
<h3 className="text-2xl font-bold text-white">You&apos;re on the list!</h3>
<p className="text-sm text-slate-400 max-w-sm mx-auto">
Thank you! We have registered your email <strong className="text-slate-200">{email}</strong>. We will reach out as soon as the <strong className="text-slate-200">{title}</strong> learning path is published.
</p>
</div>
<button
type="button"
onClick={() => setIsSubmitted(false)}
className="px-6 py-2 border border-slate-800 hover:border-slate-700 bg-slate-900/40 hover:bg-slate-900/60 rounded-xl text-xs font-semibold transition-colors"
>
Change Email
</button>
</div>
)}
</div>
</div>
</div>
);
}
39 changes: 36 additions & 3 deletions src/components/features/SkillTreeVisualizer.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
'use client';

import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import styles from "./SkillTreeVisualizer.module.css";
import { useLearningProgress } from "@/hooks/useLearningProgress";
import { useAuth } from "@/context/AuthContext";
import { CheckSquare, Square, Flame, Target } from "lucide-react";
import { motion } from "framer-motion";
import { useKeyboardShortcuts } from "@/hooks/useKeyboardShortcuts";

type SkillNode = {
id: string;
Expand Down Expand Up @@ -87,18 +88,50 @@ const pathsData: Record<string, SkillNode[]> = {
],
};

export default function SkillTreeVisualizer() {
export default function SkillTreeVisualizer({ initialPath }: { initialPath?: "Frontend" | "Backend" } = {}) {
const { user } = useAuth();
const { completedNodes, loading, toggleNode, isNodeCompleted } = useLearningProgress();
const [activePath, setActivePath] = useState<"Frontend" | "Backend">("Frontend");
const [activePath, setActivePath] = useState<"Frontend" | "Backend">(initialPath || "Frontend");
const [selectedNode, setSelectedNode] = useState<SkillNode | null>(null);

const nodes = pathsData[activePath];

// Sync active path with prop updates
useEffect(() => {
if (initialPath) {
setActivePath(initialPath);
}
}, [initialPath]);

// Dynamic progress calculation
const completedCount = nodes.filter(node => isNodeCompleted(activePath, node.id)).length;
const progressPercent = nodes.length > 0 ? Math.round((completedCount / nodes.length) * 100) : 0;

// Bind local arrow key shortcuts for node selection cycling
useKeyboardShortcuts({
arrowright: () => {
if (nodes.length === 0) return;
const currentIndex = selectedNode ? nodes.findIndex((n) => n.id === selectedNode.id) : -1;
const nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % nodes.length;
setSelectedNode(nodes[nextIndex]);
},
arrowleft: () => {
if (nodes.length === 0) return;
const currentIndex = selectedNode ? nodes.findIndex((n) => n.id === selectedNode.id) : -1;
const prevIndex = currentIndex === -1 ? nodes.length - 1 : (currentIndex - 1 + nodes.length) % nodes.length;
setSelectedNode(nodes[prevIndex]);
},
});

// Listen for the escape close-all-overlays event to close the side drawer
useEffect(() => {
const handleCloseAll = () => {
setSelectedNode(null);
};
window.addEventListener('close-all-overlays', handleCloseAll);
return () => window.removeEventListener('close-all-overlays', handleCloseAll);
}, []);

return (
<div className={`${styles.container} w-full flex flex-col items-center bg-[#0f1115] p-6 rounded-xl border border-slate-800`}>

Expand Down
9 changes: 7 additions & 2 deletions src/components/gamification/Leaderboard.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client';
import { useEffect, useState } from 'react';
import { collection, query, orderBy, limit, getDocs } from 'firebase/firestore';
import { db } from '@/lib/firebase';
import { getTier } from '@/lib/gamification';
import Image from 'next/image';

export function Leaderboard() {
const [users, setUsers] = useState<any[]>([]);
Expand Down Expand Up @@ -48,9 +50,12 @@ export function Leaderboard() {
<span className="text-gray-400 w-6 text-center font-mono">
#{i + 1}
</span>
<img
<Image
src={u.photoURL ?? '/default-avatar.png'}
className="w-8 h-8 rounded-full"
alt={u.displayName ?? 'User avatar'}
width={32}
height={32}
className="rounded-full"
/>
<span className="flex-1 font-medium">
{u.displayName ?? 'Anonymous'}
Expand Down
Loading
Loading