diff --git a/package-lock.json b/package-lock.json
index ddc5ad65..581fdefd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18823,7 +18823,6 @@
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
- "dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
diff --git a/src/app/opportunities/page.tsx b/src/app/opportunities/page.tsx
new file mode 100644
index 00000000..4a95a97c
--- /dev/null
+++ b/src/app/opportunities/page.tsx
@@ -0,0 +1,29 @@
+import React from 'react';
+import OpportunityDashboard from '@/components/features/OpportunityDashboard';
+
+export const metadata = {
+ title: 'Opportunities Hub | DevPath',
+ description: 'Explore career opportunities, developer internships, and hackathons with deadline countdowns and dynamic bookmarking.',
+};
+
+export default function OpportunitiesPage() {
+ return (
+
+
+ {/* Page Heading banner */}
+
+
+ Career & Developer Opportunities
+
+
+ Apply to top-tier internship openings, hackathons, and fellowship cohorts.
+ Pin key dates and visualize live deadline countdowns below.
+
+
+
+ {/* Dashboard Component */}
+
+
+
+ );
+}
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 */}
+
+
+ ) : (
+
+
+ ✨
+
+
+
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.
+
+
+
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
+
+
+ )}
+
+
+
+ );
+}
diff --git a/src/components/features/OpportunityDashboard.tsx b/src/components/features/OpportunityDashboard.tsx
new file mode 100644
index 00000000..7cf09249
--- /dev/null
+++ b/src/components/features/OpportunityDashboard.tsx
@@ -0,0 +1,427 @@
+'use client';
+
+import React, { useState, useEffect, useMemo } from 'react';
+import { motion, AnimatePresence } from 'framer-motion';
+import {
+ Bookmark,
+ Calendar,
+ Grid,
+ List,
+ Search,
+ SlidersHorizontal,
+ Clock,
+ Building,
+ Check,
+ AlertTriangle,
+ ArrowUpDown,
+} from 'lucide-react';
+import { useBookmarks, BookmarkItem } from '@/hooks/useBookmarks';
+
+const MOCK_OPPORTUNITIES: BookmarkItem[] = [
+ {
+ id: 'opp-1',
+ title: 'Open Source Contributor',
+ description: 'GSoC is a global program focused on bringing student developers into open source software development.',
+ type: 'opportunity',
+ company: 'Google',
+ deadline: '2026-06-25T23:59:59Z',
+ tags: ['Open Source', 'Remote', 'Stipend'],
+ color: 'linear-gradient(135deg, #ea4335, #c5221f)',
+ },
+ {
+ id: 'opp-2',
+ title: 'Software Engineering Intern',
+ description: "Join Meta's product teams to build technologies that help people connect, find communities, and grow businesses.",
+ type: 'opportunity',
+ company: 'Meta',
+ deadline: '2026-06-20T23:59:59Z',
+ tags: ['Internship', 'Frontend', 'Backend'],
+ color: 'linear-gradient(135deg, #0080ff, #0055b3)',
+ },
+ {
+ id: 'opp-3',
+ title: 'Hackathon Participant',
+ description: 'Our annual 48-hour hackathon to build open-source projects for community welfare. Compete for cash prizes and mentorship.',
+ type: 'opportunity',
+ company: 'DevPath',
+ deadline: '2026-06-18T18:00:00Z',
+ tags: ['Hackathon', 'Community', 'Prizes'],
+ color: 'linear-gradient(135deg, #10b981, #047857)',
+ },
+ {
+ id: 'opp-4',
+ title: 'GitHub Octernship Fellow',
+ description: 'The GitHub Octernships program connects students with industry partners for paid internship opportunities.',
+ type: 'opportunity',
+ company: 'GitHub',
+ deadline: '2026-07-15T23:59:59Z',
+ tags: ['Fellowship', 'Remote', 'Paid'],
+ color: 'linear-gradient(135deg, #24292e, #1a1e22)',
+ },
+ {
+ id: 'opp-5',
+ title: 'Developer Participant',
+ description: 'Build next-generation payment integrations and financial tools using Stripe API.',
+ type: 'opportunity',
+ company: 'Stripe',
+ deadline: '2026-06-01T23:59:59Z',
+ tags: ['Hackathon', 'API', 'Payments'],
+ color: 'linear-gradient(135deg, #635bff, #4339ca)',
+ },
+];
+
+interface DeadlineStatus {
+ text: string;
+ status: 'expired' | 'closing-today' | 'closing-tomorrow' | 'upcoming';
+ daysLeft: number;
+}
+
+const calculateDeadlineStatus = (deadlineStr: string): DeadlineStatus => {
+ const deadline = new Date(deadlineStr).getTime();
+ const now = Date.now();
+ const diff = deadline - now;
+
+ if (diff <= 0) {
+ return { text: 'Expired', status: 'expired', daysLeft: -1 };
+ }
+
+ const oneDay = 24 * 60 * 60 * 1000;
+ const daysLeft = diff / oneDay;
+
+ if (daysLeft <= 1) {
+ return { text: 'Closing today', status: 'closing-today', daysLeft };
+ } else if (daysLeft <= 2) {
+ return { text: 'Closing tomorrow', status: 'closing-tomorrow', daysLeft };
+ } else {
+ return { text: `${Math.ceil(daysLeft)} days left`, status: 'upcoming', daysLeft };
+ }
+};
+
+export default function OpportunityDashboard() {
+ const { bookmarks, toggleBookmark, isBookmarked } = useBookmarks();
+ const [activeTab, setActiveTab] = useState<'explore' | 'bookmarked'>('explore');
+ const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
+ const [searchQuery, setSearchQuery] = useState('');
+ const [sortBy, setSortBy] = useState<'deadline' | 'recent' | 'alpha'>('deadline');
+ const [mounted, setMounted] = useState(false);
+
+ useEffect(() => {
+ setMounted(true);
+ }, []);
+
+ // Filter bookmarked opportunities to match our schema
+ const bookmarkedOpportunities = useMemo(() => {
+ return bookmarks.filter((b) => b.type === 'opportunity');
+ }, [bookmarks]);
+
+ // Combine mock data with bookmark updates to display correct saved states
+ const opportunitiesSource = useMemo(() => {
+ if (activeTab === 'bookmarked') {
+ return bookmarkedOpportunities;
+ }
+ return MOCK_OPPORTUNITIES;
+ }, [activeTab, bookmarkedOpportunities]);
+
+ // Handle Search & Filter logic
+ const processedOpportunities = useMemo(() => {
+ let result = [...opportunitiesSource];
+
+ // Search query match
+ if (searchQuery.trim()) {
+ const query = searchQuery.toLowerCase();
+ result = result.filter(
+ (opp) =>
+ opp.title.toLowerCase().includes(query) ||
+ opp.company?.toLowerCase().includes(query) ||
+ opp.tags?.some((t) => t.toLowerCase().includes(query))
+ );
+ }
+
+ // Sort matching configuration
+ result.sort((a, b) => {
+ if (sortBy === 'deadline') {
+ const timeA = a.deadline ? new Date(a.deadline).getTime() : Infinity;
+ const timeB = b.deadline ? new Date(b.deadline).getTime() : Infinity;
+ return timeA - timeB;
+ }
+ if (sortBy === 'recent') {
+ const bookmarkedA = a.bookmarkedAt ?? 0;
+ const bookmarkedB = b.bookmarkedAt ?? 0;
+ // Most recent first
+ return bookmarkedB - bookmarkedA;
+ }
+ if (sortBy === 'alpha') {
+ return a.title.localeCompare(b.title);
+ }
+ return 0;
+ });
+
+ return result;
+ }, [opportunitiesSource, searchQuery, sortBy]);
+
+ const handleBookmarkToggle = (opp: BookmarkItem) => {
+ const isSaved = isBookmarked(opp.id);
+ if (isSaved) {
+ toggleBookmark(opp);
+ } else {
+ toggleBookmark({
+ ...opp,
+ bookmarkedAt: Date.now(),
+ });
+ }
+ };
+
+ return (
+
+ {/* Header and Controls */}
+
+
+
+
+
+ Opportunity Hub
+
+
+ Track applications, countdown deadlines, and save high-value career opportunities.
+
+
+
+ {/* View and Sorting Actions */}
+
+ {/* Search Bar */}
+
+
+ setSearchQuery(e.target.value)}
+ className="w-full pl-10 pr-4 py-2 bg-slate-950/80 border border-slate-800 focus:border-primary/50 focus:outline-none rounded-xl text-xs text-white placeholder:text-slate-600 transition-colors"
+ />
+
+
+ {/* Sort Dropdown */}
+
+
+
setSortBy(e.target.value as any)}
+ className="bg-transparent text-xs text-slate-300 focus:outline-none cursor-pointer"
+ >
+ Closest Deadline
+ Recently Bookmarked
+ Alphabetical
+
+
+
+ {/* Grid/List toggler */}
+
+ setViewMode('grid')}
+ className={`p-1.5 rounded-lg transition-colors ${viewMode === 'grid' ? 'bg-primary text-white' : 'text-slate-500 hover:text-slate-300'}`}
+ title="Grid View"
+ aria-label="Switch to grid view"
+ >
+
+
+ setViewMode('list')}
+ className={`p-1.5 rounded-lg transition-colors ${viewMode === 'list' ? 'bg-primary text-white' : 'text-slate-500 hover:text-slate-300'}`}
+ title="List View"
+ aria-label="Switch to list view"
+ >
+
+
+
+
+
+
+ {/* Tabs */}
+
+ setActiveTab('explore')}
+ className={`pb-3 px-4 text-sm font-bold relative transition-colors ${
+ activeTab === 'explore' ? 'text-white' : 'text-slate-500 hover:text-slate-300'
+ }`}
+ >
+ Explore
+ {activeTab === 'explore' && (
+
+ )}
+
+ setActiveTab('bookmarked')}
+ className={`pb-3 px-4 text-sm font-bold relative transition-colors flex items-center gap-1.5 ${
+ activeTab === 'bookmarked' ? 'text-white' : 'text-slate-500 hover:text-slate-300'
+ }`}
+ >
+ My Bookmarks
+ {mounted && bookmarkedOpportunities.length > 0 && (
+
+ {bookmarkedOpportunities.length}
+
+ )}
+ {activeTab === 'bookmarked' && (
+
+ )}
+
+
+
+ {/* Dashboard Grid/List */}
+
+ {processedOpportunities.length === 0 ? (
+
+
+
+
+ No Opportunities Found
+
+ {activeTab === 'bookmarked'
+ ? 'Save opportunities from the Explore tab to track deadlines here.'
+ : 'No opportunities match your current filter query.'}
+
+
+ ) : (
+
+ {processedOpportunities.map((opp) => {
+ const saved = isBookmarked(opp.id);
+ const status = opp.deadline ? calculateDeadlineStatus(opp.deadline) : null;
+
+ // Deadline badge styling configuration
+ let badgeColor = 'bg-slate-900 text-slate-400 border-slate-800';
+ if (status) {
+ if (status.status === 'expired') {
+ badgeColor = 'bg-red-500/10 text-red-400 border-red-500/20';
+ } else if (status.status === 'closing-today') {
+ badgeColor = 'bg-orange-500/10 text-orange-400 border-orange-500/20 animate-pulse';
+ } else if (status.status === 'closing-tomorrow') {
+ badgeColor = 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20';
+ } else {
+ badgeColor = 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20';
+ }
+ }
+
+ return (
+
+ {/* Accent Side Line */}
+
+
+ {/* Body Content */}
+
+ {/* Header Row: Company and Title */}
+
+
+
+ {opp.company}
+
+
+ {opp.title}
+
+
+
+
+ {opp.description}
+
+
+ {/* Tags */}
+
+ {opp.tags?.map((tag) => (
+
+ {tag}
+
+ ))}
+
+
+
+ {/* Action Footer (List layout alignment) */}
+
+ {/* Deadline & Countdown */}
+ {opp.deadline && (
+
+
+
+
+ {new Date(opp.deadline).toLocaleDateString(undefined, {
+ month: 'short',
+ day: 'numeric',
+ year: 'numeric',
+ })}
+
+
+ {mounted ? (
+
+
+ {status?.text}
+
+ ) : (
+
+ )}
+
+ )}
+
+ {/* Bookmark Toggle Button */}
+
handleBookmarkToggle(opp)}
+ className={`flex items-center gap-1.5 px-3.5 py-2 rounded-xl text-xs font-bold transition-all duration-300 w-full sm:w-auto justify-center ${
+ saved
+ ? 'bg-red-500/10 hover:bg-red-500/20 text-red-400 border border-red-500/20'
+ : 'bg-primary hover:bg-primary/95 text-white shadow-lg shadow-primary/10'
+ }`}
+ title={saved ? 'Remove from bookmarks' : 'Add to bookmarks'}
+ >
+ {saved ? (
+ <>
+
+ Unsave
+ >
+ ) : (
+ <>
+
+ Bookmark
+ >
+ )}
+
+
+
+ );
+ })}
+
+ )}
+
+
+ );
+}
diff --git a/src/components/features/SkillTreeVisualizer.tsx b/src/components/features/SkillTreeVisualizer.tsx
index eea6014c..35e20a88 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;
@@ -87,18 +88,50 @@ 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;
+ // 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/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 ?? 'Anonymous'}
diff --git a/src/components/layout/Navbar.tsx b/src/components/layout/Navbar.tsx
index 88d716bf..822701d2 100644
--- a/src/components/layout/Navbar.tsx
+++ b/src/components/layout/Navbar.tsx
@@ -1,10 +1,10 @@
'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';
-import { motion, AnimatePresence } from 'framer-motion';
+import { motion, AnimatePresence, useScroll, useSpring } from 'framer-motion';
import {
Flame,
Github,
@@ -34,6 +34,7 @@ const navLinks = [
{ href: '/community', label: 'Community' },
{ href: '/resources', label: 'Resources' },
{ href: '/events', label: 'Events' },
+ { href: '/opportunities', label: 'Opportunities' },
{ href: '/opensource', label: 'Open Source' },
{ href: '/team', label: 'Team' },
];
@@ -49,11 +50,27 @@ export default function Navbar() {
const pathname = usePathname();
+ useEffect(() => {
+ const handleCloseAll = () => {
+ setMobileMenuOpen(false);
+ setBookmarkDrawerOpen(false);
+ };
+ window.addEventListener('close-all-overlays', handleCloseAll);
+ return () => window.removeEventListener('close-all-overlays', handleCloseAll);
+ }, []);
+
const { currentStreak } = useMemo(
() => calculateStreak(user?.loginDates),
[user?.loginDates]
);
+ const { scrollYProgress } = useScroll();
+ const scaleX = useSpring(scrollYProgress, {
+ stiffness: 100,
+ damping: 30,
+ restDelta: 0.001,
+ });
+
if (pathname === '/ap') return null;
const toggleMobileMenu = () => {
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/components/profile/DevCard.tsx b/src/components/profile/DevCard.tsx
index ebc228d0..1f1a35ce 100644
--- a/src/components/profile/DevCard.tsx
+++ b/src/components/profile/DevCard.tsx
@@ -360,7 +360,7 @@ export default function DevCard({ user }: { user: any }) {
const a = document.createElement('a');
a.href = dataUrl;
- 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.');
diff --git a/src/hooks/useBookmarks.ts b/src/hooks/useBookmarks.ts
index 0ba63671..5fa57b22 100644
--- a/src/hooks/useBookmarks.ts
+++ b/src/hooks/useBookmarks.ts
@@ -4,9 +4,13 @@ export interface BookmarkItem {
id: string;
title: string;
description: string;
- type: 'roadmap' | 'project';
+ type: 'roadmap' | 'project' | 'opportunity';
color?: string;
path?: string;
+ company?: string;
+ deadline?: string;
+ tags?: string[];
+ bookmarkedAt?: number;
}
const listeners = new Set<() => void>();
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]);
+}