From def1840a650d5440a645a37d300653c08fa64259 Mon Sep 17 00:00:00 2001 From: shivani11jadhav Date: Fri, 12 Jun 2026 18:11:11 +0530 Subject: [PATCH 1/4] fix(profile): resolve real-time state desync between profile update form and DevCard using onSnapshot --- package-lock.json | 17 ++++++ src/components/profile/DevCard.tsx | 90 ++++++++++++++++++++---------- 2 files changed, 78 insertions(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index e4fb7598..343f1db3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,6 +60,7 @@ "jest-environment-jsdom": "^30.4.1", "postcss": "^8.5.10", "postcss-nesting": "^13.0.2", + "prettier": "^3.2.5", "tailwind-merge": "^3.4.0", "tailwindcss": "^3.4.17", "tailwindcss-animate": "^1.0.7", @@ -19444,6 +19445,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-format": { "version": "30.4.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", diff --git a/src/components/profile/DevCard.tsx b/src/components/profile/DevCard.tsx index 8d288af0..19141dde 100644 --- a/src/components/profile/DevCard.tsx +++ b/src/components/profile/DevCard.tsx @@ -11,7 +11,7 @@ import { } from 'lucide-react'; import { calculateLevel } from '@/lib/points'; import { copyToClipboard } from '@/lib/clipboard'; -import { collection, query, where, getCountFromServer } from 'firebase/firestore'; +import { collection, query, where, getCountFromServer, doc, onSnapshot } from 'firebase/firestore'; import { db } from '@/lib/firebase'; import { useNotificationActions } from '@/stores/ui-store'; import styles from './DevCard.module.css'; @@ -117,24 +117,56 @@ export default function DevCard({ user }: { user: any }) { const [avatarLoadFailed, setAvatarLoadFailed] = useState(false); const { showSuccess, showError } = useNotificationActions(); + const [realTimeUser, setRealTimeUser] = useState(user); + + useEffect(() => { + setRealTimeUser(user); + }, [user]); + + useEffect(() => { + if (!user?.uid) return; + + const collectionName = user.role === 'admin' ? 'admins' : 'members'; + const docId = user.role === 'admin' ? user.email?.toLowerCase() : user.uid; + + if (!docId) return; + + const docRef = doc(db, collectionName, docId); + const unsubscribe = onSnapshot(docRef, (docSnap) => { + if (docSnap.exists()) { + const data = docSnap.data(); + setRealTimeUser((prev: any) => ({ + ...prev, + ...data, + uid: user.uid, + role: user.role, + })); + } + }, (error) => { + console.error("Error listening to profile changes in DevCard:", error); + }); + + return () => unsubscribe(); + }, [user?.uid, user?.role, user?.email]); + useEffect(() => { const fetch = async () => { - if (!user?.points) { setRankLoading(false); return; } + if (!realTimeUser?.points) { setRankLoading(false); return; } try { const snap = await getCountFromServer( - query(collection(db, 'leaderboard'), where('points', '>', user.points)) + query(collection(db, 'leaderboard'), where('points', '>', realTimeUser.points)) ); setRank(snap.data().count + 1); } catch { /* silent */ } finally { setRankLoading(false); } }; fetch(); - }, [user?.points]); + }, [realTimeUser?.points]); useEffect(() => { setShowSkeleton(true); const timer = setTimeout(() => setShowSkeleton(false), 650); return () => clearTimeout(timer); - }, [user?.uid]); + }, [realTimeUser?.uid]); useEffect(() => { const t = setTimeout(() => setLangMounted(true), 500); @@ -143,28 +175,28 @@ export default function DevCard({ user }: { user: any }) { useEffect(() => { setAvatarLoadFailed(false); - }, [user?.photoURL]); + }, [realTimeUser?.photoURL]); - const levelInfo = calculateLevel(user?.points ?? 0); + const levelInfo = calculateLevel(realTimeUser?.points ?? 0); const level = levelInfo.currentLevel; const levelColor = resolveLevelColor(level.color); const levelBg = resolveLevelBg(level.bg); - const earnedBadges = (user?.achievements ?? []) + const earnedBadges = (realTimeUser?.achievements ?? []) .filter((id: string) => BADGE_REGISTRY[id]) .map((id: string) => ({ id, ...BADGE_REGISTRY[id] })); const topBadges = earnedBadges.slice(0, 4); const extraCount = Math.max(0, earnedBadges.length - 4); - const topLangs = ((user?.githubStats?.topLanguages ?? []) as { language: string; count: number }[]).slice(0, 4); + const topLangs = ((realTimeUser?.githubStats?.topLanguages ?? []) as { language: string; count: number }[]).slice(0, 4); const totalLang = topLangs.reduce((s, l) => s + l.count, 0); - const animXP = useAnimatedCount(user?.points ?? 0); - const animStreak = useAnimatedCount(user?.streak ?? 0, 900); + const animXP = useAnimatedCount(realTimeUser?.points ?? 0); + const animStreak = useAnimatedCount(realTimeUser?.streak ?? 0, 900); const profileUrl = typeof window !== 'undefined' - ? `${window.location.origin}/u/${user?.uid}` - : `devpath.in/u/${user?.uid}`; + ? `${window.location.origin}/u/${realTimeUser?.uid}` + : `devpath.in/u/${realTimeUser?.uid}`; const waitForCardImages = async (root: HTMLElement) => { const imgs = Array.from(root.querySelectorAll('img')); @@ -224,7 +256,7 @@ export default function DevCard({ user }: { user: any }) { const url = canvas.toDataURL('image/png'); const a = document.createElement('a'); a.href = url; - a.download = `devcard-${user?.name?.replace(/\s+/g, '-').toLowerCase() ?? 'devcard'}.png`; + a.download = `devcard-${realTimeUser?.name?.replace(/\s+/g, '-').toLowerCase() ?? 'devcard'}.png`; a.click(); showSuccess('DevCard downloaded successfully.'); } catch { @@ -315,10 +347,10 @@ export default function DevCard({ user }: { user: any }) {
- {user?.photoURL && !avatarLoadFailed ? ( + {realTimeUser?.photoURL && !avatarLoadFailed ? ( {user?.name setAvatarLoadFailed(true)} /> ) : ( -
{user?.name?.charAt(0)?.toUpperCase() ?? '?'}
+
{realTimeUser?.name?.charAt(0)?.toUpperCase() ?? '?'}
)} - {user?.name ?? 'Developer'} + {realTimeUser?.name ?? 'Developer'} {level.name} - {(user?.city || user?.state) && ( - {[user.city, user.state].filter(Boolean).join(', ')} + {(realTimeUser?.city || realTimeUser?.state) && ( + {[realTimeUser.city, realTimeUser.state].filter(Boolean).join(', ')} )} - Joined {fmtDate(user?.createdAt)} - {user?.githubStats?.username && {user.githubStats.username}} - {user?.linkedin && ( - + Joined {fmtDate(realTimeUser?.createdAt)} + {realTimeUser?.githubStats?.username && {realTimeUser.githubStats.username}} + {realTimeUser?.linkedin && ( + LinkedIn )} - {user?.instagram && ( - + {realTimeUser?.instagram && ( + Instagram )} @@ -367,8 +399,8 @@ export default function DevCard({ user }: { user: any }) {
{rankLoading ? '—' : rank ? `#${rank}` : '—'}Global Rank
{animStreak}dStreak
{earnedBadges.length}Badges
-
{user?.completedQuizzes?.length ?? 0}Quizzes
-
{user?.githubStats?.connected ? : }
{user?.githubStats?.connected ? fmtPoints(user.githubStats.totalStars ?? user.githubStats.stars ?? 0) : (user?.followers?.length ?? 0)}{user?.githubStats?.connected ? 'GH Stars' : 'Followers'}
+
{realTimeUser?.completedQuizzes?.length ?? 0}Quizzes
+
{realTimeUser?.githubStats?.connected ? : }
{realTimeUser?.githubStats?.connected ? fmtPoints(realTimeUser.githubStats.totalStars ?? realTimeUser.githubStats.stars ?? 0) : (realTimeUser?.followers?.length ?? 0)}{realTimeUser?.githubStats?.connected ? 'GH Stars' : 'Followers'}
{topBadges.length > 0 && ( From ff92285c0caa53c474c1eb1a5292b779b7b46bc5 Mon Sep 17 00:00:00 2001 From: shivani11jadhav Date: Mon, 15 Jun 2026 06:27:28 +0530 Subject: [PATCH 2/4] feat(hotkeys): implement global keyboard navigation shortcuts and shortcut legend UI (#604) --- .../features/SkillTreeVisualizer.tsx | 28 ++- src/components/layout/Navbar.tsx | 11 +- src/components/layout/RouteAwareChrome.tsx | 11 ++ src/components/layout/ShortcutLegend.tsx | 170 ++++++++++++++++++ src/hooks/useKeyboardShortcuts.ts | 129 +++++++++++++ 5 files changed, 347 insertions(+), 2 deletions(-) create mode 100644 src/components/layout/ShortcutLegend.tsx create mode 100644 src/hooks/useKeyboardShortcuts.ts diff --git a/src/components/features/SkillTreeVisualizer.tsx b/src/components/features/SkillTreeVisualizer.tsx index eea6014c..97f44b2f 100644 --- a/src/components/features/SkillTreeVisualizer.tsx +++ b/src/components/features/SkillTreeVisualizer.tsx @@ -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; @@ -99,6 +100,31 @@ export default function SkillTreeVisualizer() { 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 (
diff --git a/src/components/layout/Navbar.tsx b/src/components/layout/Navbar.tsx index b1504687..1f8fbf3e 100644 --- a/src/components/layout/Navbar.tsx +++ b/src/components/layout/Navbar.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, useMemo } from 'react'; +import { useState, useMemo, useEffect } from 'react'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; import Image from 'next/image'; @@ -46,6 +46,15 @@ export default function Navbar() { const setSearchOpen = useSetSearchOpen(); console.log('Navbar Render: isMaintenanceMode =', isMaintenanceMode); + useEffect(() => { + const handleCloseAll = () => { + setMobileMenuOpen(false); + setBookmarkDrawerOpen(false); + }; + window.addEventListener('close-all-overlays', handleCloseAll); + return () => window.removeEventListener('close-all-overlays', handleCloseAll); + }, []); + const toggleMobileMenu = () => { if (!isMaintenanceMode) { setMobileMenuOpen(!mobileMenuOpen); diff --git a/src/components/layout/RouteAwareChrome.tsx b/src/components/layout/RouteAwareChrome.tsx index b7b34d14..cd155706 100644 --- a/src/components/layout/RouteAwareChrome.tsx +++ b/src/components/layout/RouteAwareChrome.tsx @@ -1,5 +1,6 @@ 'use client'; +import { useState } from 'react'; import { usePathname } from 'next/navigation'; import MaintenanceBlocker from '@/components/layout/MaintenanceBlocker'; @@ -11,6 +12,8 @@ import PageWrapper from '@/components/layout/PageWrapper'; import { FloatingAssistant } from '@/components/assistant/floating-assistant'; import { ToastContainer } from '@/components/ui/ToastContainer'; import SearchModal from '@/components/layout/SearchModal'; +import ShortcutLegend from '@/components/layout/ShortcutLegend'; +import { useKeyboardShortcuts } from '@/hooks/useKeyboardShortcuts'; export default function RouteAwareChrome({ children, @@ -19,6 +22,13 @@ export default function RouteAwareChrome({ }) { const pathname = usePathname(); const isAuthRoute = pathname === '/login' || pathname === '/signup'; + const [isLegendOpen, setLegendOpen] = useState(false); + + // Bind global navigation shortcuts + useKeyboardShortcuts({ + '?': () => setLegendOpen((prev) => !prev), + 'escape': () => setLegendOpen(false), + }); return ( <> @@ -34,6 +44,7 @@ export default function RouteAwareChrome({ {!isAuthRoute && } + setLegendOpen(false)} /> ); } diff --git a/src/components/layout/ShortcutLegend.tsx b/src/components/layout/ShortcutLegend.tsx new file mode 100644 index 00000000..a920dce3 --- /dev/null +++ b/src/components/layout/ShortcutLegend.tsx @@ -0,0 +1,170 @@ +'use client'; + +import { motion, AnimatePresence } from 'framer-motion'; +import { X, Keyboard, Search, CornerDownLeft, ArrowLeftRight, HelpCircle } from 'lucide-react'; + +interface ShortcutLegendProps { + isOpen: boolean; + onClose: () => void; +} + +interface ShortcutItem { + keys: string[]; + description: string; + icon?: any; +} + +interface ShortcutSection { + title: string; + items: ShortcutItem[]; +} + +const SHORTCUT_SECTIONS: ShortcutSection[] = [ + { + title: 'Global Shortcuts', + items: [ + { + keys: ['Ctrl', 'K'], + description: 'Toggle Global Search Input', + icon: Search, + }, + { + keys: ['Cmd', 'K'], + description: 'Toggle Global Search Input (macOS)', + icon: Search, + }, + { + keys: ['?'], + description: 'Toggle Keyboard Shortcut Legend', + icon: HelpCircle, + }, + { + keys: ['Esc'], + description: 'Close Open Modals / Side Drawers / Search', + icon: X, + }, + ], + }, + { + title: 'Roadmap Navigation', + items: [ + { + keys: ['→'], + description: 'Cycle focused roadmap selection forward', + icon: ArrowLeftRight, + }, + { + keys: ['←'], + description: 'Cycle focused roadmap selection backward', + icon: ArrowLeftRight, + }, + ], + }, +]; + +export default function ShortcutLegend({ isOpen, onClose }: ShortcutLegendProps) { + return ( + + {isOpen && ( + <> + {/* Backdrop with premium blur */} + + + {/* Modal Container */} +
+ + {/* Top accent glow line */} +
+ + {/* Header */} +
+
+
+ +
+
+

Keyboard Shortcuts

+

Accessibility & navigation hotkeys

+
+
+ +
+ + {/* Content body */} +
+ {SHORTCUT_SECTIONS.map((section) => ( +
+

+ {section.title} +

+
+ {section.items.map((item, idx) => { + const Icon = item.icon; + return ( +
+
+ {Icon && ( + + )} + + {item.description} + +
+
+ {item.keys.map((key, kIdx) => ( + + {kIdx > 0 && +} + + {key} + + + ))} +
+
+ ); + })} +
+
+ ))} +
+ + {/* Footer */} +
+ Press ? to toggle + + + Master Mode Active + +
+
+
+ + )} +
+ ); +} diff --git a/src/hooks/useKeyboardShortcuts.ts b/src/hooks/useKeyboardShortcuts.ts new file mode 100644 index 00000000..31bc2179 --- /dev/null +++ b/src/hooks/useKeyboardShortcuts.ts @@ -0,0 +1,129 @@ +'use client'; + +import { useEffect, useRef } from 'react'; +import { useSearchOpen, useSetSearchOpen } from '@/stores/ui-store'; + +type ShortcutHandler = (e: KeyboardEvent) => void; + +interface ShortcutConfig { + [key: string]: ShortcutHandler; +} + +/** + * Checks if the event target is inside an input, textarea, or contenteditable element. + */ +const isInputElement = (el: HTMLElement | null): boolean => { + if (!el) return false; + const tagName = el.tagName.toUpperCase(); + return tagName === 'INPUT' || tagName === 'TEXTAREA' || el.isContentEditable; +}; + +/** + * Parses and matches a keyboard event against a shortcut key combo string. + * Example combos: 'ctrl+k', 'meta+k', 'cmd+k', 'escape', 'ArrowLeft', '?', 'shift+/' + */ +const matchesCombo = (combo: string, e: KeyboardEvent): boolean => { + const parts = combo.toLowerCase().split('+'); + const targetKey = parts[parts.length - 1]; + + const wantsCtrl = parts.includes('ctrl'); + const wantsMeta = parts.includes('meta') || parts.includes('cmd'); + const wantsAlt = parts.includes('alt'); + const wantsShift = parts.includes('shift'); + + const hasCtrl = e.ctrlKey; + const hasMeta = e.metaKey; + const hasAlt = e.altKey; + const hasShift = e.shiftKey; + + // Normalize key name matches + const eventKey = e.key.toLowerCase(); + let matchesKey = false; + + if (targetKey === 'cmd') { + // If 'cmd' is specified as the key itself rather than a modifier + matchesKey = eventKey === 'meta' || eventKey === 'os'; + } else if (targetKey === 'ctrl') { + matchesKey = eventKey === 'control'; + } else { + matchesKey = eventKey === targetKey; + } + + return ( + matchesKey && + wantsCtrl === hasCtrl && + wantsMeta === hasMeta && + wantsAlt === hasAlt && + wantsShift === hasShift + ); +}; + +export function useKeyboardShortcuts(customShortcuts?: ShortcutConfig) { + const isSearchOpen = useSearchOpen(); + const setSearchOpen = useSetSearchOpen(); + + // Store handlers in a ref so the listener doesn't need to re-bind when customShortcuts change + const shortcutsRef = useRef(customShortcuts); + useEffect(() => { + shortcutsRef.current = customShortcuts; + }, [customShortcuts]); + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + const activeEl = document.activeElement as HTMLElement | null; + const isTyping = isInputElement(activeEl); + + // Input field isolation check + // Escape key is permitted to bubble to allow closing active modal overlay even when typing in search input + if (isTyping && e.key !== 'Escape') { + return; + } + + // 1. Process custom callbacks passed into the hook first (takes precedence) + if (shortcutsRef.current) { + for (const [combo, handler] of Object.entries(shortcutsRef.current)) { + if (matchesCombo(combo, e)) { + e.preventDefault(); + handler(e); + return; + } + } + } + + // 2. Default Global Mappings (runs if no custom handler matched the combo) + + // Ctrl+K / Cmd+K -> Toggle Search + if (matchesCombo('ctrl+k', e) || matchesCombo('meta+k', e) || matchesCombo('cmd+k', e)) { + e.preventDefault(); + setSearchOpen(!isSearchOpen); + return; + } + + // Escape -> Close all open modal overlays/search/drawers + if (matchesCombo('escape', e)) { + // Close search if open + if (isSearchOpen) { + e.preventDefault(); + setSearchOpen(false); + } + + // Dispatch global broadcast event for side drawers / menus + window.dispatchEvent(new CustomEvent('close-all-overlays')); + + // Programmatically close any DOM elements matching close queries as a robust fallback + const closeButtons = document.querySelectorAll( + 'button[aria-label*="Close" i], button[class*="close" i], button[id*="close" i], [class*="modal" i] button[onClick]' + ); + closeButtons.forEach((btn) => { + (btn as HTMLButtonElement).click(); + }); + return; + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => { + window.removeEventListener('keydown', handleKeyDown); + }; + }, [isSearchOpen, setSearchOpen]); +} From 61e35f07541a6f140f404e6d625f87c94f3bf60f Mon Sep 17 00:00:00 2001 From: shivani11jadhav Date: Mon, 15 Jun 2026 07:49:42 +0530 Subject: [PATCH 3/4] feat(seo): implement dynamic metadata and opengraph engine for roadmap pages (#607) --- src/app/roadmaps/[id]/page.tsx | 149 ++++++++++++++++++ src/components/features/ComingSoonRoadmap.tsx | 116 ++++++++++++++ .../features/SkillTreeVisualizer.tsx | 11 +- 3 files changed, 274 insertions(+), 2 deletions(-) create mode 100644 src/app/roadmaps/[id]/page.tsx create mode 100644 src/components/features/ComingSoonRoadmap.tsx diff --git a/src/app/roadmaps/[id]/page.tsx b/src/app/roadmaps/[id]/page.tsx new file mode 100644 index 00000000..0eff0611 --- /dev/null +++ b/src/app/roadmaps/[id]/page.tsx @@ -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 = { + 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 { + 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 ( +
+

Roadmap Not Found

+

The requested roadmap does not exist.

+ + Return to Learning Paths + +
+ ); + } + + if (!config.isAvailable) { + return ; + } + + return ( +
+
+
+ + + Back to Paths + +

{config.title}

+

{config.description}

+
+ + +
+
+ ); +} diff --git a/src/components/features/ComingSoonRoadmap.tsx b/src/components/features/ComingSoonRoadmap.tsx new file mode 100644 index 00000000..653730ff --- /dev/null +++ b/src/components/features/ComingSoonRoadmap.tsx @@ -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 ( +
+ {/* Background glow effects */} +
+
+ +
+ {/* Back Link */} + + + Back to Learning Paths + + + {/* Card */} +
+
+ + {!isSubmitted ? ( +
+
+
+ + Roadmap Under Construction +
+

+ {title} +

+

+ We are currently crafting a comprehensive curriculum for {title}. This roadmap will outline step-by-step topics, milestones, and guided project recommendations. +

+
+ + {/* Stack Preview */} +
+ Curriculum Highlights +

{techDetails}

+
+ + {/* Form */} +
+
+ +
+ + 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" + /> +
+
+ +
+
+ ) : ( +
+
+ ✨ +
+
+

You're on the list!

+

+ Thank you! We have registered your email {email}. We will reach out as soon as the {title} learning path is published. +

+
+ +
+ )} +
+
+
+ ); +} diff --git a/src/components/features/SkillTreeVisualizer.tsx b/src/components/features/SkillTreeVisualizer.tsx index 97f44b2f..35e20a88 100644 --- a/src/components/features/SkillTreeVisualizer.tsx +++ b/src/components/features/SkillTreeVisualizer.tsx @@ -88,14 +88,21 @@ const pathsData: Record = { ], }; -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(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; From 7718c8fc2a1dbc41076bbc6d0c9f41c039a529a1 Mon Sep 17 00:00:00 2001 From: shivani11jadhav Date: Tue, 16 Jun 2026 22:53:39 +0530 Subject: [PATCH 4/4] perf(assets): refactor legacy image tags to next/image components and fix leaderboard linting --- src/components/gamification/Leaderboard.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/gamification/Leaderboard.tsx b/src/components/gamification/Leaderboard.tsx index 913e2248..764b1311 100644 --- a/src/components/gamification/Leaderboard.tsx +++ b/src/components/gamification/Leaderboard.tsx @@ -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([]); @@ -48,9 +50,12 @@ export function Leaderboard() { #{i + 1} - {u.displayName {u.displayName ?? 'Anonymous'}