diff --git a/package-lock.json b/package-lock.json index 66fb7c1c..2b167f6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23037,7 +23037,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/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 */} +
+ + +
+ + {/* Grid/List toggler */} +
+ + +
+
+
+ + {/* Tabs */} +
+ + +
+ + {/* 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 */} + +
+
+ ); + })} +
+ )} +
+
+ ); +} diff --git a/src/components/features/SkillTreeVisualizer.tsx b/src/components/features/SkillTreeVisualizer.tsx index ae308085..e0d39b53 100644 --- a/src/components/features/SkillTreeVisualizer.tsx +++ b/src/components/features/SkillTreeVisualizer.tsx @@ -149,6 +149,31 @@ export default function SkillTreeVisualizer({ window.removeEventListener('close-all-overlays', handleCloseAll); }, []); + // 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 (
([]); @@ -48,9 +50,12 @@ export function Leaderboard() { #{i + 1} - {u.displayName {u.displayName ?? 'Anonymous'} diff --git a/src/components/layout/Navbar.tsx b/src/components/layout/Navbar.tsx index d009cf30..1715e135 100644 --- a/src/components/layout/Navbar.tsx +++ b/src/components/layout/Navbar.tsx @@ -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' }, ]; 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>();