This commit is contained in:
2026-05-16 18:57:42 +07:00
commit 01c00093d5
128 changed files with 23193 additions and 0 deletions
+226
View File
@@ -0,0 +1,226 @@
import { useAuth } from "@/hooks/useAuth";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useIsMobile } from "@/hooks/use-mobile";
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { useState, type ReactNode } from "react";
import { useLocation, useNavigate } from "react-router";
import {
LayoutDashboard,
Map,
Users,
Church,
ClipboardCheck,
Settings,
Shield,
LogOut,
Menu,
ChevronLeft,
ChevronRight,
MapPinHouse,
} from "lucide-react";
const menuItems = [
{ icon: LayoutDashboard, label: "Dashboard", path: "/" },
{ icon: Map, label: "Peta GIS", path: "/map" },
{ icon: Users, label: "Penerima Bantuan", path: "/recipients" },
{ icon: MapPinHouse, label: "Rumah Ibadah", path: "/places" },
{ icon: ClipboardCheck, label: "Verifikasi", path: "/verification" },
{ icon: Shield, label: "Pengguna", path: "/users", adminOnly: true },
{ icon: Settings, label: "Pengaturan", path: "/settings" },
];
export default function AppLayout({ children }: { children: ReactNode }) {
const { user, logout } = useAuth();
const location = useLocation();
const navigate = useNavigate();
const isMobile = useIsMobile();
const [collapsed, setCollapsed] = useState(false);
const [mobileOpen, setMobileOpen] = useState(false);
const isAdmin = user?.role === "admin";
const activeItem = menuItems.find((item) => item.path === location.pathname);
const filteredItems = menuItems.filter(
(item) => !item.adminOnly || isAdmin
);
const sidebarWidth = collapsed ? 72 : 260;
const renderSidebar = () => (
<div
className="flex flex-col h-full bg-sidebar border-r border-sidebar-border"
style={{ width: sidebarWidth, transition: "width 0.2s ease" }}
>
{/* Header */}
<div className="flex items-center h-16 px-4 border-b border-sidebar-border shrink-0">
<Church className="h-6 w-6 text-primary shrink-0" />
{!collapsed && (
<div className="ml-3 overflow-hidden">
<h1 className="text-sm font-bold truncate text-sidebar-foreground">
WebGIS Poverty
</h1>
<p className="text-[10px] text-muted-foreground truncate">
Mapping System
</p>
</div>
)}
</div>
{/* Navigation */}
<nav className="flex-1 overflow-y-auto py-3 px-2 space-y-1">
{filteredItems.map((item) => {
const isActive = location.pathname === item.path;
return (
<button
key={item.path}
onClick={() => {
navigate(item.path);
setMobileOpen(false);
}}
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-all ${
isActive
? "bg-primary/10 text-primary"
: "text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
}`}
title={collapsed ? item.label : undefined}
>
<item.icon className="h-[18px] w-[18px] shrink-0" />
{!collapsed && <span className="truncate">{item.label}</span>}
{!collapsed && item.adminOnly && (
<Badge variant="outline" className="ml-auto text-[9px] px-1 py-0 h-4">
Admin
</Badge>
)}
</button>
);
})}
</nav>
{/* Footer - User */}
<div className="border-t border-sidebar-border p-3 shrink-0">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="w-full flex items-center gap-3 rounded-xl hover:bg-sidebar-accent transition-colors p-2">
<Avatar className="h-8 w-8 shrink-0">
<AvatarFallback className="text-xs bg-primary/10 text-primary">
{user?.name?.charAt(0).toUpperCase() || "U"}
</AvatarFallback>
</Avatar>
{!collapsed && (
<div className="flex-1 min-w-0 text-left">
<p className="text-sm font-medium truncate text-sidebar-foreground">
{user?.name || "User"}
</p>
<p className="text-xs text-muted-foreground capitalize">
{user?.role || "user"}
</p>
</div>
)}
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-52">
<DropdownMenuItem disabled className="flex-col items-start">
<span className="font-medium">{user?.name}</span>
<span className="text-xs text-muted-foreground">{user?.email}</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => navigate("/settings")}>
<Settings className="mr-2 h-4 w-4" />
Pengaturan
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={logout} className="text-destructive">
<LogOut className="mr-2 h-4 w-4" />
Keluar
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{/* Collapse toggle */}
{!isMobile && (
<button
onClick={() => setCollapsed(!collapsed)}
className="w-full flex items-center justify-center mt-2 py-1.5 rounded-lg hover:bg-sidebar-accent transition-colors"
>
{collapsed ? (
<ChevronRight className="h-4 w-4 text-muted-foreground" />
) : (
<ChevronLeft className="h-4 w-4 text-muted-foreground" />
)}
</button>
)}
</div>
</div>
);
return (
<div className="flex h-screen w-screen overflow-hidden bg-background">
{/* Sidebar - Desktop */}
{!isMobile && (
<div
className="shrink-0 h-full"
style={{ width: sidebarWidth, transition: "width 0.2s ease" }}
>
{renderSidebar()}
</div>
)}
{/* Sidebar - Mobile */}
{isMobile && (
<Sheet open={mobileOpen} onOpenChange={setMobileOpen}>
<SheetContent side="left" className="p-0 w-[260px]">
<div className="h-full" style={{ width: 260 }}>
{renderSidebar()}
</div>
</SheetContent>
</Sheet>
)}
{/* Main Content */}
<div className="flex-1 flex flex-col min-w-0 overflow-hidden">
{/* Mobile Header */}
{isMobile && (
<header className="h-14 shrink-0 flex items-center gap-3 px-4 border-b bg-background/95 backdrop-blur z-40">
<Sheet>
<SheetTrigger asChild>
<Button variant="ghost" size="icon" onClick={() => setMobileOpen(true)}>
<Menu className="h-5 w-5" />
</Button>
</SheetTrigger>
</Sheet>
<Church className="h-5 w-5 text-primary" />
<span className="font-semibold text-sm">{activeItem?.label || "WebGIS"}</span>
</header>
)}
{/* Desktop Header */}
{!isMobile && (
<header className="h-14 shrink-0 flex items-center justify-between px-6 border-b bg-background/95 backdrop-blur z-40">
<div>
<h1 className="text-lg font-semibold">{activeItem?.label || ""}</h1>
</div>
<div className="flex items-center gap-2">
<Badge variant="secondary" className="capitalize text-xs">
{user?.role}
</Badge>
</div>
</header>
)}
{/* Page Content */}
<main className="flex-1 overflow-y-auto p-4 md:p-6">
{children}
</main>
</div>
</div>
);
}