Skip to content

Commit

Permalink
Refactor link and menu configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
alon710 committed Oct 7, 2024
1 parent 4878a57 commit c925687
Show file tree
Hide file tree
Showing 7 changed files with 247 additions and 157 deletions.
173 changes: 16 additions & 157 deletions app/dashboard/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,26 @@
"use client";

import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { CircleUser, Search, Menu, X } from "lucide-react";
import { Drawer, DrawerOverlay, DrawerContent } from "@/components/ui/drawer";
import MobileDrawer from "@/components/layout/MobileDrawer";
import Header from "@/components/layout/Header";
import MainContent from "@/components/layout/MainContent";
import { SiteFooter } from "@/components/ui/footer";
import Image from "next/image";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { Card, CardDescription, CardFooter } from "@/components/ui/card";
import { links } from "@/config/menu";

export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
const [isHydrated, setIsHydrated] = useState(false);
const currentPath = usePathname();
const router = useRouter();
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);

const getLinkClassName = (href: string) => {
return currentPath?.startsWith(href)
? "text-foreground transition-colors hover:text-foreground font-semibold"
: "text-muted-foreground transition-colors hover:text-foreground";
};
useEffect(() => {
setIsHydrated(true);
}, []);

const getCurrentSideLinks = () => {
return Object.entries(links).find(
Expand All @@ -52,149 +39,21 @@ export default function DashboardLayout({
}
}, [currentPath, router]);

useEffect(() => {
if (isMobileMenuOpen) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "auto";
}
}, [isMobileMenuOpen]);

return (
<div className="flex min-h-screen w-full flex-col">
<header className="sticky top-0 flex h-16 items-center gap-4 border-b bg-background px-4 md:px-6">
<Button
variant="ghost"
size="icon"
className="md:hidden"
onClick={() => setIsMobileMenuOpen(true)}
>
<Menu className="h-6 w-6" />
<span className="sr-only">Toggle mobile menu</span>
</Button>
<nav className="hidden md:flex flex-col gap-6 text-lg font-medium md:flex-row md:items-center md:gap-5 md:text-sm lg:gap-6">
<Image src="/logo/512px.png" alt="Divex" width={24} height={24} />
{Object.entries(links).map(([href, { label }]) => (
<Link key={href} href={href} className={getLinkClassName(href)}>
{label}
</Link>
))}
</nav>
<div className="flex w-full items-center gap-4 md:ml-auto md:gap-2 lg:gap-4">
<form className="ml-auto flex-1 sm:flex-initial">
<div className="relative">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="search"
placeholder="Search..."
className="pl-8 sm:w-[300px] md:w-[200px] lg:w-[300px]"
/>
</div>
</form>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="secondary" size="icon" className="rounded-full">
<CircleUser className="h-5 w-5" />
<span className="sr-only">Toggle user menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>Settings</DropdownMenuItem>
<DropdownMenuItem>Support</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onSelect={() => document.body.classList.toggle("dark")}
>
Change Theme
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</header>

<Drawer
open={isMobileMenuOpen}
<Header onMobileMenuOpen={() => setIsMobileMenuOpen(true)} />
<MobileDrawer
isOpen={isMobileMenuOpen}
onClose={() => setIsMobileMenuOpen(false)}
>
<DrawerOverlay onClick={() => setIsMobileMenuOpen(false)} />
<DrawerContent>
<div className="flex items-center justify-between p-4 border-b">
<Link
href="/dashboard"
className="flex items-center gap-2 text-lg font-semibold md:text-base"
onClick={() => setIsMobileMenuOpen(false)}
>
<Image
src="/logo/512px.png"
alt="Divex"
width={24}
height={24}
className="rounded-full"
/>
<span className="sr-only">Divex</span>
</Link>
<Button
variant="ghost"
size="icon"
onClick={() => setIsMobileMenuOpen(false)}
>
<X className="h-6 w-6" />
<span className="sr-only">Close menu</span>
</Button>
</div>
<nav className="flex flex-col gap-6 text-lg font-medium p-4">
{Object.entries(links).map(([href, { label }]) => (
<Link
key={href}
href={href}
className={getLinkClassName(href)}
onClick={() => setIsMobileMenuOpen(false)}
>
{label}
</Link>
))}
</nav>
</DrawerContent>
</Drawer>
/>

{/* Main Content */}
<main className="flex min-h-[calc(100vh_-_theme(spacing.16))] flex-1 flex-col gap-4 bg-muted/40 p-4 md:gap-8 md:p-10">
<div className="mx-auto grid w-full max-w-6xl gap-2">
<h1 className="text-3xl font-semibold">
{links[currentPath?.split("/").slice(0, 3).join("/")]?.label}
</h1>
</div>
<div className="mx-auto grid w-full max-w-6xl items-start gap-6 md:grid-cols-[180px_1fr] lg:grid-cols-[250px_1fr]">
{/* Side Navigation Bar */}
{sideLinks && (
<nav className="grid gap-4 text-sm text-muted-foreground">
{Object.entries(sideLinks).map(([href, label]) => (
<Link key={href} href={href} className={getLinkClassName(href)}>
{label}
</Link>
))}
</nav>
)}

{/* Main Content Area */}
<div className="grid gap-6">
<Card>
<div className="p-6">
<CardDescription>{children}</CardDescription>
</div>
<CardFooter className="border-t px-6 py-4">
<div className="text-sm text-muted-foreground">
{links?.[currentPath?.split("/").slice(0, 3).join("/")]
?.label +
" > " +
sideLinks?.[currentPath]}
</div>
</CardFooter>
</Card>
</div>
</div>
{isHydrated ? (
<MainContent currentPath={currentPath} sideLinks={sideLinks}>
{children}
</MainContent>
) : null}
<SiteFooter />
</main>
</div>
Expand Down
55 changes: 55 additions & 0 deletions components/layout/Header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"use client";

import { usePathname } from "next/navigation";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import Image from "next/image";
import Link from "next/link";
import { Menu } from "lucide-react";
import UserMenu from "@/components/layout/UserMenu";
import { links } from "@/config/menu";
import { getLinkClassName } from "@/utils/link";

interface HeaderProps {
onMobileMenuOpen: () => void;
}

export default function Header({ onMobileMenuOpen }: HeaderProps) {
const [isHydrated, setIsHydrated] = useState(false);
const currentPath = usePathname();

useEffect(() => {
setIsHydrated(true);
}, []);

if (!isHydrated) {
return null;
}

return (
<header className="sticky top-0 flex h-16 items-center gap-4 border-b bg-background px-4 md:px-6">
<Button
variant="ghost"
size="icon"
className="md:hidden"
onClick={onMobileMenuOpen}
>
<Menu className="h-6 w-6" />
<span className="sr-only">Toggle mobile menu</span>
</Button>
<nav className="hidden md:flex flex-col gap-6 text-lg font-medium md:flex-row md:items-center md:gap-5 md:text-sm lg:gap-6">
<Image src="/logo/512px.png" alt="Divex" width={24} height={24} />
{Object.entries(links).map(([href, { label }]) => (
<Link
key={href}
href={href}
className={getLinkClassName(href, currentPath)}
>
{label}
</Link>
))}
</nav>
<UserMenu />
</header>
);
}
37 changes: 37 additions & 0 deletions components/layout/MainContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Card, CardDescription, CardFooter } from "@/components/ui/card";
import SideLinks from "@/components/layout/SideLinks";
import { links } from "@/config/menu";

interface MainContentProps {
children: React.ReactNode;
currentPath: string;
sideLinks?: { [href: string]: string };
}

export default function main_content({
children,
currentPath,
sideLinks,
}: MainContentProps) {
return (
<div className="mx-auto grid w-full max-w-6xl items-start gap-6 md:grid-cols-[180px_1fr] lg:grid-cols-[250px_1fr]">
{sideLinks && (
<SideLinks sideLinks={sideLinks} currentPath={currentPath} />
)}
<div className="grid gap-6">
<Card>
<div className="p-6">
<CardDescription>{children}</CardDescription>
</div>
<CardFooter className="border-t px-6 py-4">
<div className="text-sm text-muted-foreground">
{links?.[currentPath?.split("/").slice(0, 3).join("/")]?.label +
" > " +
sideLinks?.[currentPath]}
</div>
</CardFooter>
</Card>
</div>
</div>
);
}
69 changes: 69 additions & 0 deletions components/layout/MobileDrawer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"use client";

import { usePathname } from "next/navigation";
import { useEffect, useState } from "react";
import { Drawer, DrawerOverlay, DrawerContent } from "@/components/ui/drawer";
import Image from "next/image";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { X } from "lucide-react";
import { links } from "@/config/menu";
import { getLinkClassName } from "@/utils/link";

interface MobileDrawerProps {
isOpen: boolean;
onClose: () => void;
}

export default function MobileDrawer({ isOpen, onClose }: MobileDrawerProps) {
const [isHydrated, setIsHydrated] = useState(false);
const currentPath = usePathname();

useEffect(() => {
setIsHydrated(true);
}, []);

if (!isHydrated) {
return null;
}

return (
<Drawer open={isOpen} onClose={onClose}>
<DrawerOverlay onClick={onClose} />
<DrawerContent>
<div className="flex items-center justify-between p-4 border-b">
<Link
href="/dashboard"
className="flex items-center gap-2 text-lg font-semibold md:text-base"
onClick={onClose}
>
<Image
src="/logo/512px.png"
alt="Divex"
width={24}
height={24}
className="rounded-full"
/>
<span className="sr-only">Divex</span>
</Link>
<Button variant="ghost" size="icon" onClick={onClose}>
<X className="h-6 w-6" />
<span className="sr-only">Close menu</span>
</Button>
</div>
<nav className="flex flex-col gap-6 text-lg font-medium p-4">
{Object.entries(links).map(([href, { label }]) => (
<Link
key={href}
href={href}
className={getLinkClassName(href, currentPath)}
onClick={onClose}
>
{label}
</Link>
))}
</nav>
</DrawerContent>
</Drawer>
);
}
23 changes: 23 additions & 0 deletions components/layout/SideLinks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Link from "next/link";
import { getLinkClassName } from "@/utils/link";

interface SideLinksProps {
sideLinks: { [href: string]: string };
currentPath: string;
}

export default function side_links({ sideLinks, currentPath }: SideLinksProps) {
return (
<nav className="grid gap-4 text-sm text-muted-foreground">
{Object.entries(sideLinks).map(([href, label]) => (
<Link
key={href}
href={href}
className={getLinkClassName(href, currentPath)}
>
{label}
</Link>
))}
</nav>
);
}
Loading

0 comments on commit c925687

Please sign in to comment.