mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-09-09 08:40:40 +02:00
feat: add roadmap dropdown menu (#6692)
* feat: add roadmap dropdown menu * fix: typo official roadmaps * fix: add role attribute * Update projects dropdown * Update roadmaps dropdown --------- Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import Icon from '../AstroIcon.astro';
|
|||||||
import { NavigationDropdown } from '../NavigationDropdown';
|
import { NavigationDropdown } from '../NavigationDropdown';
|
||||||
import { AccountDropdown } from './AccountDropdown';
|
import { AccountDropdown } from './AccountDropdown';
|
||||||
import NewIndicator from './NewIndicator.astro';
|
import NewIndicator from './NewIndicator.astro';
|
||||||
|
import { RoadmapDropdownMenu } from '../RoadmapDropdownMenu/RoadmapDropdownMenu';
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class='bg-slate-900 py-5 text-white sm:py-8'>
|
<div class='bg-slate-900 py-5 text-white sm:py-8'>
|
||||||
@@ -19,7 +20,7 @@ import NewIndicator from './NewIndicator.astro';
|
|||||||
|
|
||||||
<a
|
<a
|
||||||
href='/teams'
|
href='/teams'
|
||||||
class='group relative !mr-2 inline text-blue-300 hover:text-white sm:hidden'
|
class='group relative inline text-blue-300 hover:text-white sm:hidden'
|
||||||
>
|
>
|
||||||
Teams
|
Teams
|
||||||
|
|
||||||
@@ -35,32 +36,18 @@ import NewIndicator from './NewIndicator.astro';
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- Desktop navigation items -->
|
<!-- Desktop navigation items -->
|
||||||
<div class='hidden space-x-5 sm:flex sm:items-center'>
|
<div class='hidden gap-5 sm:flex sm:items-center'>
|
||||||
<NavigationDropdown client:load />
|
<NavigationDropdown client:load />
|
||||||
<a href='/get-started' class='text-gray-400 hover:text-white'>
|
<a href='/get-started' class='text-gray-400 hover:text-white'>
|
||||||
Start Here
|
Start Here
|
||||||
</a>
|
</a>
|
||||||
|
<RoadmapDropdownMenu client:load />
|
||||||
<a
|
<a
|
||||||
href='/teams'
|
href='/teams'
|
||||||
class='group relative text-gray-400 hover:text-white'
|
class='group relative !mr-5 text-gray-400 hover:text-white'
|
||||||
>
|
>
|
||||||
Teams
|
Teams
|
||||||
</a>
|
</a>
|
||||||
<a href='/ai' class='text-gray-400 hover:text-white'> AI</a>
|
|
||||||
<a
|
|
||||||
href='/community'
|
|
||||||
class='group relative !mr-2 text-blue-300 hover:text-white'
|
|
||||||
>
|
|
||||||
Community
|
|
||||||
<NewIndicator />
|
|
||||||
</a>
|
|
||||||
<!--<button-->
|
|
||||||
<!-- data-command-menu-->
|
|
||||||
<!-- class='hidden items-center rounded-md border border-gray-800 px-2.5 py-1.5 text-sm text-gray-400 hover:cursor-pointer hover:bg-gray-800 md:flex'-->
|
|
||||||
<!-->-->
|
|
||||||
<!-- <Icon icon='search' class='h-3 w-3' />-->
|
|
||||||
<!-- <span class='ml-2'>Search</span>-->
|
|
||||||
<!--</button>-->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@@ -2,22 +2,34 @@ import {
|
|||||||
BookOpenText,
|
BookOpenText,
|
||||||
CheckSquare,
|
CheckSquare,
|
||||||
FileQuestion,
|
FileQuestion,
|
||||||
|
FolderKanban,
|
||||||
Menu,
|
Menu,
|
||||||
Shirt,
|
Shirt,
|
||||||
Video,
|
Video,
|
||||||
Waypoints,
|
Waypoints,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { cn } from '../lib/classname.ts';
|
import { cn } from '../lib/classname.ts';
|
||||||
import { useOutsideClick } from '../hooks/use-outside-click.ts';
|
import { useOutsideClick } from '../hooks/use-outside-click.ts';
|
||||||
|
import {
|
||||||
|
navigationDropdownOpen,
|
||||||
|
roadmapsDropdownOpen,
|
||||||
|
} from '../stores/page.ts';
|
||||||
|
import { useStore } from '@nanostores/react';
|
||||||
|
|
||||||
const links = [
|
const links = [
|
||||||
{
|
{
|
||||||
link: '/roadmaps',
|
link: '/roadmaps',
|
||||||
label: 'Roadmaps',
|
label: 'Official Roadmaps',
|
||||||
description: 'Step by step learning paths',
|
description: 'Made by subject matter experts',
|
||||||
Icon: Waypoints,
|
Icon: Waypoints,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
link: '/backend/projects',
|
||||||
|
label: 'Projects',
|
||||||
|
description: 'Skill-up with real-world projects',
|
||||||
|
Icon: FolderKanban,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
link: '/best-practices',
|
link: '/best-practices',
|
||||||
label: 'Best Practices',
|
label: 'Best Practices',
|
||||||
@@ -54,21 +66,30 @@ const links = [
|
|||||||
|
|
||||||
export function NavigationDropdown() {
|
export function NavigationDropdown() {
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
|
const $navigationDropdownOpen = useStore(navigationDropdownOpen);
|
||||||
|
const $roadmapsDropdownOpen = useStore(roadmapsDropdownOpen);
|
||||||
|
|
||||||
useOutsideClick(dropdownRef, () => {
|
useOutsideClick(dropdownRef, () => {
|
||||||
setIsOpen(false);
|
navigationDropdownOpen.set(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if ($roadmapsDropdownOpen) {
|
||||||
|
navigationDropdownOpen.set(false);
|
||||||
|
}
|
||||||
|
}, [$roadmapsDropdownOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex items-center" ref={dropdownRef}>
|
<div className="relative flex items-center" ref={dropdownRef}>
|
||||||
<button
|
<button
|
||||||
className={cn('text-gray-400 hover:text-white', {
|
className={cn('text-gray-400 hover:text-white', {
|
||||||
'text-white': isOpen,
|
'text-white': $navigationDropdownOpen,
|
||||||
})}
|
})}
|
||||||
onClick={() => setIsOpen(true)}
|
onClick={() => navigationDropdownOpen.set(true)}
|
||||||
onMouseOver={() => setIsOpen(true)}
|
onMouseOver={() => navigationDropdownOpen.set(true)}
|
||||||
aria-label="Open Navigation Dropdown"
|
aria-label="Open Navigation Dropdown"
|
||||||
|
aria-expanded={$navigationDropdownOpen}
|
||||||
>
|
>
|
||||||
<Menu className="h-5 w-5" />
|
<Menu className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
@@ -76,9 +97,11 @@ export function NavigationDropdown() {
|
|||||||
className={cn(
|
className={cn(
|
||||||
'pointer-events-none invisible absolute left-0 top-full z-[999] mt-2 w-48 min-w-[320px] -translate-y-1 rounded-lg bg-slate-800 py-2 opacity-0 shadow-xl transition-all duration-100',
|
'pointer-events-none invisible absolute left-0 top-full z-[999] mt-2 w-48 min-w-[320px] -translate-y-1 rounded-lg bg-slate-800 py-2 opacity-0 shadow-xl transition-all duration-100',
|
||||||
{
|
{
|
||||||
'pointer-events-auto visible translate-y-2.5 opacity-100': isOpen,
|
'pointer-events-auto visible translate-y-2.5 opacity-100':
|
||||||
|
$navigationDropdownOpen,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
|
role="menu"
|
||||||
>
|
>
|
||||||
{links.map((link) => (
|
{links.map((link) => (
|
||||||
<a
|
<a
|
||||||
@@ -87,6 +110,7 @@ export function NavigationDropdown() {
|
|||||||
rel={link.isExternal ? 'noopener noreferrer' : undefined}
|
rel={link.isExternal ? 'noopener noreferrer' : undefined}
|
||||||
key={link.link}
|
key={link.link}
|
||||||
className="group flex items-center gap-3 px-4 py-2.5 text-gray-400 transition-colors hover:bg-slate-700"
|
className="group flex items-center gap-3 px-4 py-2.5 text-gray-400 transition-colors hover:bg-slate-700"
|
||||||
|
role="menuitem"
|
||||||
>
|
>
|
||||||
<span className="flex h-[40px] w-[40px] items-center justify-center rounded-full bg-slate-600 transition-colors group-hover:bg-slate-500 group-hover:text-slate-100">
|
<span className="flex h-[40px] w-[40px] items-center justify-center rounded-full bg-slate-600 transition-colors group-hover:bg-slate-500 group-hover:text-slate-100">
|
||||||
<link.Icon className="inline-block h-5 w-5" />
|
<link.Icon className="inline-block h-5 w-5" />
|
||||||
|
93
src/components/RoadmapDropdownMenu/RoadmapDropdownMenu.tsx
Normal file
93
src/components/RoadmapDropdownMenu/RoadmapDropdownMenu.tsx
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { ChevronDown, Globe, Menu, Sparkles, Waypoints } from 'lucide-react';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { useOutsideClick } from '../../hooks/use-outside-click';
|
||||||
|
import { cn } from '../../lib/classname';
|
||||||
|
import {
|
||||||
|
navigationDropdownOpen,
|
||||||
|
roadmapsDropdownOpen,
|
||||||
|
} from '../../stores/page.ts';
|
||||||
|
import { useStore } from '@nanostores/react';
|
||||||
|
|
||||||
|
const links = [
|
||||||
|
{
|
||||||
|
link: '/roadmaps',
|
||||||
|
label: 'Official Roadmaps',
|
||||||
|
description: 'Made by subject matter experts',
|
||||||
|
Icon: Waypoints,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: '/ai/explore',
|
||||||
|
label: 'AI Roadmaps',
|
||||||
|
description: 'Generate roadmaps with AI',
|
||||||
|
Icon: Sparkles,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: '/community',
|
||||||
|
label: 'Community Roadmaps',
|
||||||
|
description: 'Made by community members',
|
||||||
|
Icon: Globe,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function RoadmapDropdownMenu() {
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const $roadmapsDropdownOpen = useStore(roadmapsDropdownOpen);
|
||||||
|
const $navigationDropdownOpen = useStore(navigationDropdownOpen);
|
||||||
|
|
||||||
|
useOutsideClick(dropdownRef, () => {
|
||||||
|
roadmapsDropdownOpen.set(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if ($navigationDropdownOpen) {
|
||||||
|
roadmapsDropdownOpen.set(false);
|
||||||
|
}
|
||||||
|
}, [$navigationDropdownOpen]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative flex items-center" ref={dropdownRef}>
|
||||||
|
<button
|
||||||
|
className={cn('text-gray-400 hover:text-white', {
|
||||||
|
'text-white': $roadmapsDropdownOpen,
|
||||||
|
})}
|
||||||
|
onClick={() => roadmapsDropdownOpen.set(true)}
|
||||||
|
onMouseOver={() => roadmapsDropdownOpen.set(true)}
|
||||||
|
aria-label="Open Navigation Dropdown"
|
||||||
|
aria-expanded={$roadmapsDropdownOpen}
|
||||||
|
>
|
||||||
|
Roadmaps{' '}
|
||||||
|
<ChevronDown className="inline-block h-3 w-3" strokeWidth={4} />
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'pointer-events-none invisible absolute left-0 top-full z-[999] mt-2 w-48 min-w-[320px] -translate-y-1 rounded-lg bg-slate-800 py-2 opacity-0 shadow-2xl transition-all duration-100',
|
||||||
|
{
|
||||||
|
'pointer-events-auto visible translate-y-2.5 opacity-100':
|
||||||
|
$roadmapsDropdownOpen,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
role="menu"
|
||||||
|
>
|
||||||
|
{links.map((link) => (
|
||||||
|
<a
|
||||||
|
href={link.link}
|
||||||
|
key={link.link}
|
||||||
|
className="group flex items-center gap-3 px-4 py-2.5 text-gray-400 transition-colors hover:bg-slate-700"
|
||||||
|
role="menuitem"
|
||||||
|
>
|
||||||
|
<span className="flex h-[40px] w-[40px] items-center justify-center rounded-full bg-slate-600 transition-colors group-hover:bg-slate-500 group-hover:text-slate-100">
|
||||||
|
<link.Icon className="inline-block h-5 w-5" />
|
||||||
|
</span>
|
||||||
|
<span className="flex flex-col">
|
||||||
|
<span className="font-medium text-slate-300 transition-colors group-hover:text-slate-100">
|
||||||
|
{link.label}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm">{link.description}</span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@@ -2,3 +2,6 @@ import { atom } from 'nanostores';
|
|||||||
|
|
||||||
export const pageProgressMessage = atom<string | undefined>(undefined);
|
export const pageProgressMessage = atom<string | undefined>(undefined);
|
||||||
export const sponsorHidden = atom(false);
|
export const sponsorHidden = atom(false);
|
||||||
|
|
||||||
|
export const roadmapsDropdownOpen = atom(false);
|
||||||
|
export const navigationDropdownOpen = atom(false);
|
Reference in New Issue
Block a user