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>
pull/6728/head
Arik Chakma 2 months ago committed by GitHub
parent c67a7d195d
commit 2007167fa9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 23
      src/components/Navigation/Navigation.astro
  2. 42
      src/components/NavigationDropdown.tsx
  3. 93
      src/components/RoadmapDropdownMenu/RoadmapDropdownMenu.tsx
  4. 3
      src/stores/page.ts

@ -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'-->
<!--&gt;-->
<!-- <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" />

@ -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);
Loading…
Cancel
Save