refactor: implement responsive side navigation with new portal sidebar configuration

This commit is contained in:
poyrazavsever
2026-06-07 07:40:57 +03:00
parent 855723ffd1
commit 47b9f728e6
3 changed files with 225 additions and 56 deletions
+1 -2
View File
@@ -1,6 +1,5 @@
import { createClient } from "@/lib/supabase/server"; import { createClient } from "@/lib/supabase/server";
import { Card, CardContent } from "poyraz-ui/atoms"; import { Card, CardContent, Badge } from "poyraz-ui/atoms";
import { Badge } from "poyraz-ui/molecules";
import { FolderKanban, CheckCircle2, Clock, ArrowRight } from "lucide-react"; import { FolderKanban, CheckCircle2, Clock, ArrowRight } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { format } from "date-fns"; import { format } from "date-fns";
+188 -43
View File
@@ -10,10 +10,26 @@ import {
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "poyraz-ui/molecules"; } from "poyraz-ui/molecules";
import { LogOut, User } from "lucide-react"; import {
Sidebar,
SidebarBranding,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarMenu,
SidebarMenuItem,
SidebarSection,
SidebarSeparator,
SidebarTrigger,
SidebarUserProfile,
} from "poyraz-ui/organisms";
import { portalSidebarData } from "@/config/portal-sidebar";
import { cn } from "@/lib/utils";
import { ChevronUp, LogOut, Menu, Settings } from "lucide-react";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { useState } from "react";
type PortalShellProps = { type PortalShellProps = {
children: React.ReactNode; children: React.ReactNode;
@@ -27,11 +43,35 @@ type PortalShellProps = {
export function PortalShell({ children, user }: PortalShellProps) { export function PortalShell({ children, user }: PortalShellProps) {
const pathname = usePathname(); const pathname = usePathname();
const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false);
return ( return (
<div className="min-h-screen bg-background text-foreground flex flex-col"> <div className="min-h-screen bg-background text-foreground">
<header className="sticky top-0 z-30 flex h-16 items-center justify-between border-b border-border bg-background/95 px-4 md:px-8 backdrop-blur"> <div className="flex min-h-screen">
<div className="flex items-center gap-6"> <aside className="sticky top-0 hidden h-dvh shrink-0 self-start border-r border-border bg-background lg:block">
<AppSidebar pathname={pathname} user={user} />
</aside>
{isMobileSidebarOpen ? (
<div className="fixed inset-0 z-50 lg:hidden">
<button
type="button"
aria-label="Menuyu kapat"
className="absolute inset-0 bg-overlay"
onClick={() => setIsMobileSidebarOpen(false)}
/>
<aside className="relative h-dvh border-r border-border bg-background">
<AppSidebar
pathname={pathname}
user={user}
onNavigate={() => setIsMobileSidebarOpen(false)}
/>
</aside>
</div>
) : null}
<div className="flex flex-1 flex-col min-w-0">
<header className="sticky top-0 z-30 flex h-14 items-center justify-between border-b border-border bg-background/95 px-4 backdrop-blur lg:hidden">
<Link href="/portal" className="flex items-center gap-2 font-semibold"> <Link href="/portal" className="flex items-center gap-2 font-semibold">
<Image <Image
src="/logo/LogoWithBg.png" src="/logo/LogoWithBg.png"
@@ -43,49 +83,154 @@ export function PortalShell({ children, user }: PortalShellProps) {
/> />
Cognis Portal Cognis Portal
</Link> </Link>
<nav className="hidden md:flex items-center gap-4 text-sm font-medium"> <Button
<Link type="button"
href="/portal" variant="outline"
className={`transition-colors hover:text-primary ${pathname === '/portal' ? 'text-primary' : 'text-muted-foreground'}`} className="h-9 w-9 p-0"
onClick={() => setIsMobileSidebarOpen(true)}
> >
Dashboard <Menu className="h-4 w-4" />
</Link>
</nav>
</div>
<div className="flex items-center gap-4">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-9 w-9 rounded-full p-0 border border-border overflow-hidden">
{user.avatarUrl ? (
<Image src={user.avatarUrl} alt={user.displayName} width={36} height={36} className="h-full w-full object-cover" />
) : (
<span className="text-xs font-medium">{user.shortName}</span>
)}
</Button> </Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuLabel className="font-normal">
<div className="flex flex-col space-y-1">
<p className="text-sm font-medium leading-none">{user.displayName}</p>
<p className="text-xs leading-none text-muted-foreground">
{user.email}
</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem className="text-rose-500" onClick={() => signOut()}>
<LogOut className="mr-2 h-4 w-4" />
Çıkış Yap
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</header> </header>
<main className="flex-1 w-full max-w-7xl mx-auto p-4 md:p-8"> <main className="flex-1 p-4 lg:p-8 min-w-0">{children}</main>
{children} </div>
</main> </div>
</div> </div>
); );
} }
function AppSidebar({
pathname,
user,
onNavigate,
}: {
pathname: string;
user: PortalShellProps["user"];
onNavigate?: () => void;
}) {
return (
<Sidebar
variant="bordered"
className="flex h-dvh max-h-dvh flex-col overflow-hidden rounded-none border-0"
>
<SidebarHeader className="shrink-0 border-b-0 px-6 py-6">
<SidebarBranding
title="Cognis"
subtitle="Client Portal"
logo={
<Image
src="/logo/LogoWithBg.png"
alt="Cognis"
width={36}
height={36}
className="h-9 w-9 rounded-sm object-cover"
priority
/>
}
/>
</SidebarHeader>
<SidebarSeparator className="mx-0 my-0 w-full" />
<SidebarContent className="tiny-scrollbar min-h-0 flex-1 overflow-y-auto px-6 py-6">
{portalSidebarData.map((group, groupIndex) => (
<div key={group.title}>
<SidebarSection title={group.title} defaultOpen className="mb-0">
<SidebarMenu>
{group.items.map((item) => {
const isActive =
item.href === "/portal"
? pathname === "/portal"
: item.href
? pathname === item.href || pathname.startsWith(item.href + "/")
: false;
const Icon = item.icon;
return (
<SidebarMenuItem
key={item.href || item.title}
active={isActive}
icon={Icon ? <Icon className="h-4 w-4" /> : undefined}
className={cn(isActive && "font-semibold")}
>
<Link
href={item.href || "#"}
onClick={onNavigate}
className="block w-full"
>
{item.title}
</Link>
</SidebarMenuItem>
);
})}
</SidebarMenu>
</SidebarSection>
{groupIndex < portalSidebarData.length - 1 ? (
<SidebarSeparator className="-mx-6 my-5 w-[calc(100%+3rem)]" />
) : null}
</div>
))}
</SidebarContent>
<SidebarSeparator className="mx-0 my-0 w-full" />
<SidebarFooter className="shrink-0 border-t-0 px-6 py-5">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
className="w-full rounded-sm text-left outline-none transition-colors hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring"
>
<div className="flex items-center gap-2">
<SidebarUserProfile
name={user.displayName}
role={user.email}
avatarUrl={user.avatarUrl || undefined}
initials={user.shortName}
className="min-w-0 flex-1"
/>
<ChevronUp className="mr-3 h-4 w-4 shrink-0 text-muted-foreground" />
</div>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
side="right"
sideOffset={8}
className="w-64"
>
<DropdownMenuLabel className="font-normal">
<div className="flex flex-col gap-1">
<span className="truncate text-sm font-medium text-foreground">
{user.displayName}
</span>
<span className="truncate text-xs text-muted-foreground">
{user.email}
</span>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link href="/portal/settings" onClick={onNavigate} className="gap-2">
<Settings className="h-4 w-4" />
Ayarlar
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<form action={signOut}>
<DropdownMenuItem asChild>
<button type="submit" className="w-full gap-2">
<LogOut className="h-4 w-4" />
Çıkış yap
</button>
</DropdownMenuItem>
</form>
</DropdownMenuContent>
</DropdownMenu>
</SidebarFooter>
<SidebarTrigger className="hidden" />
</Sidebar>
);
}
+25
View File
@@ -0,0 +1,25 @@
import {
FolderKanban,
Sparkles,
} from "lucide-react";
export type PortalSidebarNavItem = {
title: string;
href?: string;
icon?: any;
items?: PortalSidebarNavItem[];
};
export type PortalSidebarNavGroup = {
title: string;
items: PortalSidebarNavItem[];
};
export const portalSidebarData: PortalSidebarNavGroup[] = [
{
title: "GENEL BAKIŞ",
items: [
{ title: "Dashboard", href: "/portal", icon: Sparkles },
],
},
];