diff --git a/client/src/App.jsx b/client/src/App.jsx index 7f861c0..c93d346 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -15,6 +15,7 @@ import GitHubInsights from './pages/GitHubInsights' import Showcase from './pages/Showcase' import PublicProfile from './pages/PublicProfile' import ResumeBuilder from './pages/ResumeBuilder' +import Roadmap from './pages/Roadmap' import { preferencesApi } from './services/api' import useHeartbeat from './hooks/useHeartbeat' import Lenis from 'lenis' @@ -131,6 +132,7 @@ function App() { }> } /> } /> + } /> } /> } /> } /> diff --git a/client/src/components/layout/Navbar.jsx b/client/src/components/layout/Navbar.jsx index 585c22f..24e137d 100644 --- a/client/src/components/layout/Navbar.jsx +++ b/client/src/components/layout/Navbar.jsx @@ -1,9 +1,17 @@ import { Link, useLocation } from 'react-router-dom' import { UserButton } from '@clerk/clerk-react' -import { motion } from 'framer-motion' +import { motion, AnimatePresence } from 'framer-motion' import { useState, useEffect, useRef } from 'react' import NotificationSettings from '../settings/NotificationSettings' -import { BookOpen, Info, Trophy, FileText } from 'lucide-react' +import { + BookOpen, + Trophy, + FileText, + Map, + Code2, + Rocket, + Grid, +} from 'lucide-react' // SVG Icon Components const DashboardIcon = ({ className = "w-5 h-5" }) => ( @@ -39,22 +47,52 @@ const GithubOutlineIcon = ({ className = "w-5 h-5" }) => ( ) - - - -const navItems = [ - { name: 'Dashboard', path: '/dashboard', icon: DashboardIcon }, - { name: 'Learning', path: '/learning', icon: BookOpen }, - { name: 'Projects', path: '/projects', icon: WindowsTerminalIcon }, - { name: 'Showcase', path: '/showcase', icon: Trophy }, - { name: 'AI Chat', path: '/chat', icon: GeminiIcon }, - { name: 'GitHub Insights', path: '/github-insights', icon: GithubOutlineIcon }, - { name: 'Resume Builder', path: '/resume', icon: FileText }, +// Define the groups +const navGroups = [ + { + id: 'home', + name: 'Home', + type: 'single', + path: '/dashboard', + icon: DashboardIcon + }, + { + id: 'dev', + name: 'Dev', + type: 'group', + icon: Code2, + items: [ + { name: 'Roadmap', path: '/roadmap', icon: Map }, + { name: 'Projects', path: '/projects', icon: WindowsTerminalIcon }, + { name: 'GitHub', path: '/github-insights', icon: GithubOutlineIcon }, + ] + }, + { + id: 'growth', + name: 'Growth', + type: 'group', + icon: Rocket, + items: [ + { name: 'Learning', path: '/learning', icon: BookOpen }, + { name: 'Showcase', path: '/showcase', icon: Trophy }, + ] + }, + { + id: 'tools', + name: 'Tools', + type: 'group', + icon: Grid, + items: [ + { name: 'AI Chat', path: '/chat', icon: GeminiIcon }, + { name: 'Resume', path: '/resume', icon: FileText }, + ] + } ] -// Sidebar icon button -function SidebarIcon({ item, isActive }) { +// Single Sidebar Item +function SidebarItem({ item, isActive }) { const IconComponent = item.icon + return ( + {/* Tooltip */}
{item.name}
+ {/* Active indicator */} {isActive && ( location.pathname.startsWith(item.path)) + + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + + + + + {/* Active indicator for group */} + {hasActiveChild && ( + + )} + + {/* Flyout Menu */} + + {isHovered && ( + +
+
+ {group.name} +
+ {group.items.map((item) => { + const ItemIcon = item.icon + const isItemActive = location.pathname.startsWith(item.path) + return ( + + + + {item.name} + + + ) + })} +
+
+ )} +
+
+ ) +} + // Settings Icon Button function SettingsButton({ onClick }) { return ( @@ -138,13 +252,20 @@ function Sidebar({ onOpenSettings }) {
{/* Navigation */} -
+ + {/* Mobile Cards - Horizontal scroll on small screens */} +
+ {/* Activity + Skills Row */} +
+ {/* Top Skills - Flexible height */} +
+ +
+ {/* LeetCode Full Width - Flexible height */} +
+ +
+
+
diff --git a/client/src/pages/Roadmap.jsx b/client/src/pages/Roadmap.jsx new file mode 100644 index 0000000..513069f --- /dev/null +++ b/client/src/pages/Roadmap.jsx @@ -0,0 +1,492 @@ +import { useState, useEffect } from 'react' +import { motion, AnimatePresence } from 'framer-motion' +import { useAuth } from '@clerk/clerk-react' +import { goalsApi } from '../services/api' +import PixelTransition from '../components/ui/PixelTransition' +import Button from '../components/ui/Button' +import { Plus, Target, Calendar, CheckCircle, Clock, Trash2, Edit2, AlertTriangle, ChevronRight, Check } from 'lucide-react' +import { createPortal } from 'react-dom' +import DatePicker from '../components/ui/DatePicker' + +// Format date helper +const formatDate = (dateString, includeYear = false) => { + if (!dateString) return 'No Date' + const date = new Date(dateString) + // Check if valid date + if (isNaN(date.getTime())) return 'Invalid Date' + + // Check if it's a Firestore timestamp (seconds) + if (typeof dateString === 'object' && dateString._seconds) { + return new Date(dateString._seconds * 1000).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: includeYear ? 'numeric' : undefined + }) + } + + return date.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: includeYear ? 'numeric' : undefined + }) +} + +// Modal Component +function Modal({ isOpen, onClose, title, children }) { + if (!isOpen) return null + + return createPortal( + + + e.stopPropagation()} + > +
+

{title}

+ +
+
+ {children} +
+
+
+
, + document.body + ) +} + +function GoalCard({ goal, onEdit, onDelete, onUpdate }) { + const progress = goal.progress || 0 + // Safely verify milestones exists and is an array + const milestones = Array.isArray(goal.milestones) ? goal.milestones : [] + const nextMilestone = milestones.find(m => !m.isCompleted) + + // Calculate days remaining + const getDaysRemaining = () => { + if (!goal.targetDate) return null + const target = new Date(goal.targetDate._seconds ? goal.targetDate._seconds * 1000 : goal.targetDate) + const now = new Date() + const diffTime = target - now + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + return diffDays + } + + const daysRemaining = getDaysRemaining() + const isOverdue = daysRemaining !== null && daysRemaining < 0 && goal.status !== 'Completed' + + return ( + +
+
+
+ + {goal.category} + + {isOverdue && ( + + Overdue + + )} +
+

{goal.title}

+
+ +
+ + +
+
+ +

{goal.description}

+ + {/* Progress Bar */} +
+
+ Progress + {progress}% +
+
+ 50 ? 'bg-purple-500' : 'bg-blue-500' + }`} + /> +
+
+ + {/* Next Milestone or Completed Status */} +
+
+ {progress === 100 ? ( + + Goal Completed! + + ) : nextMilestone ? ( + + + Next: {nextMilestone.title} + + ) : ( + + No active milestones + + )} +
+ +
+ + {daysRemaining !== null ? ( + daysRemaining < 0 ? `${Math.abs(daysRemaining)}d overdue` : `${daysRemaining} days left` + ) : 'No deadline'} +
+
+
+ ) +} + +export default function Roadmap() { + const { isLoaded, isSignedIn } = useAuth() + const [loading, setLoading] = useState(true) + const [goals, setGoals] = useState([]) + const [showModal, setShowModal] = useState(false) + const [editingGoal, setEditingGoal] = useState(null) + const [deleteConfirm, setDeleteConfirm] = useState(null) + const [error, setError] = useState(null) + const [refreshTrigger, setRefreshTrigger] = useState(0) + + // Form State + const defaultForm = { + title: '', + description: '', + category: 'Personal', + startDate: new Date().toISOString().split('T')[0], + targetDate: '', + milestones: [] + } + const [formData, setFormData] = useState(defaultForm) + const [newMilestone, setNewMilestone] = useState('') + + useEffect(() => { + if (isLoaded && isSignedIn) { + fetchGoals() + } + }, [isLoaded, isSignedIn, refreshTrigger]) + + const fetchGoals = async () => { + try { + // Keep loading true on initial load + if (goals.length === 0) setLoading(true) + const res = await goalsApi.getAll() + setGoals(res.data?.data?.goals || []) + } catch (err) { + console.error('Failed to fetch goals:', err) + setError('Could not load your roadmap.') + } finally { + setLoading(false) + } + } + + const handleOpenModal = (goal = null) => { + if (goal) { + setEditingGoal(goal) + setFormData({ + title: goal.title, + description: goal.description, + category: goal.category, + startDate: goal.startDate ? (goal.startDate._seconds ? new Date(goal.startDate._seconds * 1000).toISOString().split('T')[0] : goal.startDate.split('T')[0]) : '', + targetDate: goal.targetDate ? (goal.targetDate._seconds ? new Date(goal.targetDate._seconds * 1000).toISOString().split('T')[0] : goal.targetDate.split('T')[0]) : '', + milestones: Array.isArray(goal.milestones) ? goal.milestones : [] + }) + } else { + setEditingGoal(null) + setFormData(defaultForm) + } + setShowModal(true) + } + + const handleAddMilestone = () => { + if (!newMilestone.trim()) return + setFormData(prev => ({ + ...prev, + milestones: [ + ...prev.milestones, + { id: crypto.randomUUID(), title: newMilestone.trim(), isCompleted: false } + ] + })) + setNewMilestone('') + } + + const removeMilestone = (id) => { + setFormData(prev => ({ + ...prev, + milestones: prev.milestones.filter(m => m.id !== id) + })) + } + + const toggleMilestoneInForm = (id) => { + setFormData(prev => ({ + ...prev, + milestones: prev.milestones.map(m => + m.id === id ? { ...m, isCompleted: !m.isCompleted } : m + ) + })) + } + + const handleSubmit = async (e) => { + e.preventDefault() + try { + if (editingGoal) { + await goalsApi.update(editingGoal.id, formData) + } else { + await goalsApi.create(formData) + } + setShowModal(false) + setRefreshTrigger(prev => prev + 1) + } catch (err) { + console.error('Error saving goal:', err) + alert('Failed to save goal') + } + } + + const handleDelete = async (id) => { + try { + await goalsApi.delete(id) + setDeleteConfirm(null) + setRefreshTrigger(prev => prev + 1) + } catch (err) { + console.error('Error deleting goal:', err) + alert('Failed to delete goal') + } + } + + return ( + +
+ {/* Header */} +
+
+

My Roadmap

+

Set goals, track milestones, and visualize your developer journey.

+
+ +
+ + {/* Goals Grid */} + {!loading && goals.length === 0 ? ( +
+
+ +
+

No Goals Set Yet

+

Define what you want to achieve next. A clear roadmap is the key to success!

+ +
+ ) : ( +
+ {goals.map(goal => ( + setDeleteConfirm(id)} + /> + ))} +
+ )} + + {/* Modals */} + setShowModal(false)} + title={editingGoal ? "Edit Goal" : "Create New Goal"} + > +
+
+
+ + setFormData({ ...formData, title: e.target.value })} + className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-purple-500 focus:outline-none transition-colors" + placeholder="e.g. Master React Performance" + required + /> +
+
+ +
+ {['Personal', 'Career', 'Learning', 'Project'].map(cat => ( + + ))} +
+
+
+ +