feat: authorization admin panel
This commit is contained in:
parent
fd740a99c3
commit
c35b1c85df
|
@ -1,6 +1,5 @@
|
||||||
// app/components/layoutadmin/header.tsx
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Form } from "@remix-run/react";
|
import { Form, useNavigation } from "@remix-run/react";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { Input } from "~/components/ui/input";
|
import { Input } from "~/components/ui/input";
|
||||||
|
@ -14,6 +13,16 @@ import {
|
||||||
DropdownMenuTrigger
|
DropdownMenuTrigger
|
||||||
} from "~/components/ui/dropdown-menu";
|
} from "~/components/ui/dropdown-menu";
|
||||||
import { Sheet, SheetContent, SheetTrigger } from "~/components/ui/sheet";
|
import { Sheet, SheetContent, SheetTrigger } from "~/components/ui/sheet";
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle
|
||||||
|
} from "~/components/ui/alert-dialog";
|
||||||
import {
|
import {
|
||||||
Menu,
|
Menu,
|
||||||
Search,
|
Search,
|
||||||
|
@ -26,221 +35,258 @@ import {
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
MoreHorizontal,
|
MoreHorizontal,
|
||||||
PanelLeftClose,
|
PanelLeftClose,
|
||||||
PanelLeft
|
PanelLeft,
|
||||||
|
Loader2,
|
||||||
|
Shield
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { SessionData } from "~/sessions.server";
|
||||||
|
|
||||||
interface AdminHeaderProps {
|
interface AdminHeaderProps {
|
||||||
onMenuClick: () => void;
|
onMenuClick: () => void;
|
||||||
sidebarCollapsed: boolean;
|
sidebarCollapsed: boolean;
|
||||||
isMobile: boolean;
|
isMobile: boolean;
|
||||||
|
user: SessionData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AdminHeader({
|
export function AdminHeader({
|
||||||
onMenuClick,
|
onMenuClick,
|
||||||
sidebarCollapsed,
|
sidebarCollapsed,
|
||||||
isMobile
|
isMobile,
|
||||||
|
user
|
||||||
}: AdminHeaderProps) {
|
}: AdminHeaderProps) {
|
||||||
const [isDark, setIsDark] = useState(false);
|
const [showLogoutDialog, setShowLogoutDialog] = useState(false);
|
||||||
|
const navigation = useNavigation();
|
||||||
|
const isLoggingOut = navigation.formAction === "/action/logout";
|
||||||
|
|
||||||
|
const getUserInitials = (email: string) => {
|
||||||
|
if (email) {
|
||||||
|
return email.substring(0, 2).toUpperCase();
|
||||||
|
}
|
||||||
|
return "AD";
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="sticky top-0 flex w-full bg-white border-b border-gray-200 z-40 dark:border-gray-700 dark:bg-gray-800">
|
<>
|
||||||
<div className="flex flex-col items-center justify-between grow lg:flex-row lg:px-6">
|
<header className="sticky top-0 flex w-full bg-white border-b border-gray-200 z-40 dark:border-gray-700 dark:bg-gray-800">
|
||||||
{/* Mobile header */}
|
<div className="flex flex-col items-center justify-between grow lg:flex-row lg:px-6">
|
||||||
<div className="flex items-center justify-between w-full gap-2 px-3 py-3 border-b border-gray-200 dark: sm:gap-4 lg:justify-normal lg:border-b-0 lg:px-0 lg:py-4">
|
{/* Mobile header */}
|
||||||
{/* Hamburger menu button */}
|
<div className="flex items-center justify-between w-full gap-2 px-3 py-3 border-b border-gray-200 dark: sm:gap-4 lg:justify-normal lg:border-b-0 lg:px-0 lg:py-4">
|
||||||
<Button
|
{/* Hamburger menu button */}
|
||||||
variant="outline"
|
<Button
|
||||||
size="icon"
|
variant="outline"
|
||||||
onClick={onMenuClick}
|
size="icon"
|
||||||
className="hover:bg-gray-100 dark:hover:bg-gray-700 border-gray-300 dark:border-gray-600 dark:bg-gray-800"
|
onClick={onMenuClick}
|
||||||
>
|
className="hover:bg-gray-100 dark:hover:bg-gray-700 border-gray-300 dark:border-gray-600 dark:bg-gray-800"
|
||||||
{isMobile ? (
|
>
|
||||||
<Menu className="h-5 w-5" />
|
{isMobile ? (
|
||||||
) : sidebarCollapsed ? (
|
<Menu className="h-5 w-5" />
|
||||||
<PanelLeft className="h-5 w-5" />
|
) : sidebarCollapsed ? (
|
||||||
) : (
|
<PanelLeft className="h-5 w-5" />
|
||||||
<PanelLeftClose className="h-5 w-5" />
|
) : (
|
||||||
)}
|
<PanelLeftClose className="h-5 w-5" />
|
||||||
</Button>
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
{/* Mobile logo */}
|
{/* Mobile logo */}
|
||||||
<div className="lg:hidden">
|
<div className="lg:hidden">
|
||||||
<div className="w-24 h-6 bg-gray-800 dark:bg-white rounded flex items-center justify-center text-white dark:text-gray-900 text-xs font-bold">
|
<div className="flex items-center space-x-2">
|
||||||
LOGO
|
<Shield className="h-5 w-5 text-blue-600" />
|
||||||
</div>
|
<div className="text-sm font-bold text-gray-900 dark:text-white">
|
||||||
</div>
|
Admin Panel
|
||||||
|
</div>
|
||||||
{/* Mobile more menu */}
|
|
||||||
<Sheet>
|
|
||||||
<SheetTrigger asChild>
|
|
||||||
<Button variant="ghost" size="icon" className="lg:hidden">
|
|
||||||
<MoreHorizontal className="h-5 w-5" />
|
|
||||||
</Button>
|
|
||||||
</SheetTrigger>
|
|
||||||
<SheetContent side="right" className="w-80">
|
|
||||||
<div className="mt-6 space-y-4">
|
|
||||||
<Button className="w-full" variant="outline">
|
|
||||||
<Bell className="mr-2 h-4 w-4" />
|
|
||||||
Notifications
|
|
||||||
<Badge variant="destructive" className="ml-auto">
|
|
||||||
3
|
|
||||||
</Badge>
|
|
||||||
</Button>
|
|
||||||
<ModeToggle />
|
|
||||||
</div>
|
|
||||||
</SheetContent>
|
|
||||||
</Sheet>
|
|
||||||
|
|
||||||
{/* Desktop search - Commented out for now */}
|
|
||||||
{/* <div className="hidden lg:block">
|
|
||||||
<div className="relative">
|
|
||||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-500 dark:text-gray-400" />
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
placeholder="Search or type command..."
|
|
||||||
className="w-96 pl-10 pr-14 bg-gray-50 dark:bg-gray-800/50 border-gray-200 dark:border-gray-700"
|
|
||||||
/>
|
|
||||||
<div className="absolute right-2 top-1/2 -translate-y-1/2">
|
|
||||||
<kbd className="inline-flex items-center gap-1 rounded border border-gray-200 bg-white px-1.5 py-0.5 text-xs text-gray-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400">
|
|
||||||
⌘K
|
|
||||||
</kbd>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div> */}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Desktop header actions */}
|
{/* Mobile more menu */}
|
||||||
<div className="hidden items-center justify-between w-full gap-4 px-5 py-4 lg:flex lg:justify-end lg:px-0">
|
<Sheet>
|
||||||
<div className="flex items-center gap-3">
|
<SheetTrigger asChild>
|
||||||
{/* Theme toggle */}
|
<Button variant="ghost" size="icon" className="lg:hidden">
|
||||||
<ModeToggle />
|
<MoreHorizontal className="h-5 w-5" />
|
||||||
|
|
||||||
{/* Notifications */}
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
className="relative hover:bg-gray-100 dark:hover:bg-gray-700 border-gray-300 dark:border-gray-600 dark:bg-gray-800"
|
|
||||||
>
|
|
||||||
<Bell className="h-4 w-4" />
|
|
||||||
<Badge
|
|
||||||
variant="destructive"
|
|
||||||
className="absolute -top-1 -right-1 h-5 w-5 rounded-full p-0 text-xs flex items-center justify-center animate-pulse"
|
|
||||||
>
|
|
||||||
3
|
|
||||||
</Badge>
|
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</SheetTrigger>
|
||||||
<DropdownMenuContent
|
<SheetContent side="right" className="w-80">
|
||||||
align="end"
|
<div className="mt-6 space-y-4">
|
||||||
className="w-80 bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700"
|
<Button className="w-full" variant="outline">
|
||||||
>
|
<Bell className="mr-2 h-4 w-4" />
|
||||||
<div className="p-4">
|
|
||||||
<h4 className="font-semibold mb-2 flex items-center justify-between text-gray-900 dark:text-gray-100">
|
|
||||||
Notifications
|
Notifications
|
||||||
<Badge variant="secondary" className="text-xs">
|
<Badge variant="destructive" className="ml-auto">
|
||||||
3 new
|
5
|
||||||
</Badge>
|
</Badge>
|
||||||
</h4>
|
</Button>
|
||||||
<div className="space-y-2">
|
<ModeToggle />
|
||||||
<div className="text-sm text-gray-600 dark:text-gray-200 p-3 rounded-lg bg-gray-50 dark:bg-gray-700 border-l-4 border-blue-500">
|
|
||||||
<p className="font-medium text-gray-900 dark:text-gray-100">
|
{/* Mobile Logout */}
|
||||||
New user registered
|
<Button
|
||||||
</p>
|
variant="outline"
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-300 mt-1">
|
className="w-full text-red-600 border-red-200 hover:bg-red-50"
|
||||||
2 minutes ago
|
onClick={() => setShowLogoutDialog(true)}
|
||||||
</p>
|
>
|
||||||
</div>
|
<LogOut className="mr-2 h-4 w-4" />
|
||||||
<div className="text-sm text-gray-600 dark:text-gray-200 p-3 rounded-lg bg-gray-50 dark:bg-gray-700 border-l-4 border-green-500">
|
Logout
|
||||||
<p className="font-medium text-gray-900 dark:text-gray-100">
|
|
||||||
System update available
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-300 mt-1">
|
|
||||||
1 hour ago
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-gray-600 dark:text-gray-200 p-3 rounded-lg bg-gray-50 dark:bg-gray-700 border-l-4 border-orange-500">
|
|
||||||
<p className="font-medium text-gray-900 dark:text-gray-100">
|
|
||||||
New message received
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-300 mt-1">
|
|
||||||
3 hours ago
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button variant="outline" className="w-full mt-3 text-xs">
|
|
||||||
View all notifications
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuContent>
|
</SheetContent>
|
||||||
</DropdownMenu>
|
</Sheet>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* User menu */}
|
{/* Desktop header actions */}
|
||||||
<DropdownMenu>
|
<div className="hidden items-center justify-between w-full gap-4 px-5 py-4 lg:flex lg:justify-end lg:px-0">
|
||||||
<DropdownMenuTrigger asChild>
|
<div className="flex items-center gap-3">
|
||||||
<Button
|
{/* Theme toggle */}
|
||||||
variant="ghost"
|
<ModeToggle />
|
||||||
className="h-auto p-2 hover:bg-gray-100 dark:hover:bg-gray-700"
|
|
||||||
|
{/* Notifications */}
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="relative hover:bg-gray-100 dark:hover:bg-gray-700 border-gray-300 dark:border-gray-600 dark:bg-gray-800"
|
||||||
|
>
|
||||||
|
<Bell className="h-4 w-4" />
|
||||||
|
<Badge
|
||||||
|
variant="destructive"
|
||||||
|
className="absolute -top-1 -right-1 h-5 w-5 rounded-full p-0 text-xs flex items-center justify-center animate-pulse"
|
||||||
|
>
|
||||||
|
5
|
||||||
|
</Badge>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
align="end"
|
||||||
|
className="w-80 bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="p-4">
|
||||||
<Avatar className="h-8 w-8">
|
<h4 className="font-semibold mb-2 flex items-center justify-between text-gray-900 dark:text-gray-100">
|
||||||
<AvatarImage
|
Admin Notifications
|
||||||
src="https://github.com/shadcn.png"
|
<Badge variant="secondary" className="text-xs">
|
||||||
alt="User"
|
5 new
|
||||||
/>
|
</Badge>
|
||||||
<AvatarFallback className="bg-blue-600 text-white">
|
</h4>
|
||||||
MU
|
<div className="space-y-2">
|
||||||
</AvatarFallback>
|
<div className="text-sm text-gray-600 dark:text-gray-200 p-3 rounded-lg bg-gray-50 dark:bg-gray-700 border-l-4 border-red-500">
|
||||||
</Avatar>
|
<p className="font-medium text-gray-900 dark:text-gray-100">
|
||||||
<div className="hidden sm:block text-left">
|
New user verification required
|
||||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
</p>
|
||||||
Musharof
|
<p className="text-xs text-gray-500 dark:text-gray-300 mt-1">
|
||||||
|
2 minutes ago
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-500 dark:text-gray-300">
|
<div className="text-sm text-gray-600 dark:text-gray-200 p-3 rounded-lg bg-gray-50 dark:bg-gray-700 border-l-4 border-blue-500">
|
||||||
Administrator
|
<p className="font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
New pengelola registration
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-300 mt-1">
|
||||||
|
15 minutes ago
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-600 dark:text-gray-200 p-3 rounded-lg bg-gray-50 dark:bg-gray-700 border-l-4 border-green-500">
|
||||||
|
<p className="font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
System backup completed
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-300 mt-1">
|
||||||
|
1 hour ago
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ChevronDown className="h-4 w-4 text-gray-500" />
|
<Button variant="outline" className="w-full mt-3 text-xs">
|
||||||
|
View all notifications
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</DropdownMenuContent>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenu>
|
||||||
<DropdownMenuContent
|
|
||||||
align="end"
|
{/* User menu */}
|
||||||
className="w-56 bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700"
|
<DropdownMenu>
|
||||||
>
|
<DropdownMenuTrigger asChild>
|
||||||
<div className="px-2 py-1.5 text-sm text-gray-700 dark:text-gray-200">
|
<Button
|
||||||
<div className="font-medium">Musharof</div>
|
variant="ghost"
|
||||||
<div className="text-xs text-gray-500 dark:text-gray-300">
|
className="h-auto p-2 hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||||
admin@example.com
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Avatar className="h-8 w-8">
|
||||||
|
<AvatarImage src="" alt="Admin" />
|
||||||
|
<AvatarFallback className="bg-blue-600 text-white">
|
||||||
|
{getUserInitials(user.email || "Admin")}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div className="hidden sm:block text-left">
|
||||||
|
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
Administrator
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500 dark:text-gray-300">
|
||||||
|
{user.email || "admin@example.com"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ChevronDown className="h-4 w-4 text-gray-500" />
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
align="end"
|
||||||
|
className="w-56 bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700"
|
||||||
|
>
|
||||||
|
<div className="px-2 py-1.5 text-sm text-gray-700 dark:text-gray-200">
|
||||||
|
<div className="font-medium">Administrator</div>
|
||||||
|
<div className="text-xs text-gray-500 dark:text-gray-300">
|
||||||
|
{user.email || "admin@example.com"}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuItem className="cursor-pointer">
|
||||||
<DropdownMenuItem className="cursor-pointer">
|
<User className="mr-2 h-4 w-4" />
|
||||||
<User className="mr-2 h-4 w-4" />
|
Admin Profile
|
||||||
Profile
|
</DropdownMenuItem>
|
||||||
</DropdownMenuItem>
|
<DropdownMenuItem className="cursor-pointer">
|
||||||
<DropdownMenuItem className="cursor-pointer">
|
<Settings className="mr-2 h-4 w-4" />
|
||||||
<Settings className="mr-2 h-4 w-4" />
|
System Settings
|
||||||
Settings
|
</DropdownMenuItem>
|
||||||
</DropdownMenuItem>
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuItem
|
||||||
<DropdownMenuItem asChild>
|
className="cursor-pointer text-red-600 dark:text-red-400"
|
||||||
<Form action="/logout" method="post" className="w-full">
|
onClick={() => setShowLogoutDialog(true)}
|
||||||
<button
|
>
|
||||||
type="submit"
|
<LogOut className="mr-2 h-4 w-4" />
|
||||||
className="flex w-full items-center text-red-600 dark:text-red-400 cursor-pointer"
|
Sign out
|
||||||
>
|
</DropdownMenuItem>
|
||||||
<LogOut className="mr-2 h-4 w-4" />
|
</DropdownMenuContent>
|
||||||
Sign out
|
</DropdownMenu>
|
||||||
</button>
|
</div>
|
||||||
</Form>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</header>
|
||||||
</header>
|
|
||||||
|
{/* Logout Confirmation Dialog */}
|
||||||
|
<AlertDialog open={showLogoutDialog} onOpenChange={setShowLogoutDialog}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Konfirmasi Logout Admin</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
Apakah Anda yakin ingin keluar dari admin panel? Anda perlu login
|
||||||
|
kembali untuk mengakses sistem administrasi.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel disabled={isLoggingOut}>Batal</AlertDialogCancel>
|
||||||
|
<Form method="post" action="/action/logout">
|
||||||
|
<AlertDialogAction
|
||||||
|
type="submit"
|
||||||
|
className="bg-red-600 hover:bg-red-700"
|
||||||
|
disabled={isLoggingOut}
|
||||||
|
>
|
||||||
|
{isLoggingOut ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Logging out...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Ya, Logout"
|
||||||
|
)}
|
||||||
|
</AlertDialogAction>
|
||||||
|
</Form>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
// app/components/layoutadmin/layout-wrapper.tsx
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { AdminSidebar } from "./sidebar";
|
import { AdminSidebar } from "./sidebar";
|
||||||
import { AdminHeader } from "./header";
|
import { AdminHeader } from "./header";
|
||||||
|
import { SessionData } from "~/sessions.server";
|
||||||
|
|
||||||
interface AdminLayoutWrapperProps {
|
interface AdminLayoutWrapperProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
user: SessionData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AdminLayoutWrapper({ children }: AdminLayoutWrapperProps) {
|
export function AdminLayoutWrapper({
|
||||||
|
children,
|
||||||
|
user
|
||||||
|
}: AdminLayoutWrapperProps) {
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
@ -69,6 +73,7 @@ export function AdminLayoutWrapper({ children }: AdminLayoutWrapperProps) {
|
||||||
onMenuClick={handleToggleSidebar}
|
onMenuClick={handleToggleSidebar}
|
||||||
sidebarCollapsed={sidebarCollapsed}
|
sidebarCollapsed={sidebarCollapsed}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
|
user={user}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Page content */}
|
{/* Page content */}
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { Link, useLocation } from "@remix-run/react";
|
||||||
import { Badge } from "~/components/ui/badge";
|
import { Badge } from "~/components/ui/badge";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { ScrollArea } from "~/components/ui/scroll-area";
|
import { ScrollArea } from "~/components/ui/scroll-area";
|
||||||
import { Separator } from "~/components/ui/separator";
|
|
||||||
import {
|
import {
|
||||||
Collapsible,
|
Collapsible,
|
||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
|
@ -19,21 +18,12 @@ import {
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
Recycle,
|
Recycle,
|
||||||
Users,
|
Users,
|
||||||
TrendingUp,
|
|
||||||
FileText,
|
FileText,
|
||||||
CreditCard,
|
|
||||||
BarChart3,
|
|
||||||
Settings,
|
Settings,
|
||||||
MessageCircle,
|
|
||||||
MapPin,
|
MapPin,
|
||||||
Package,
|
|
||||||
DollarSign,
|
|
||||||
UserCheck,
|
UserCheck,
|
||||||
Building2,
|
|
||||||
Newspaper,
|
Newspaper,
|
||||||
HelpCircle,
|
HelpCircle,
|
||||||
Bell,
|
|
||||||
Shield,
|
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
X
|
X
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
@ -62,197 +52,58 @@ const menuItems: MenuItem[] = [
|
||||||
{
|
{
|
||||||
title: "Dashboard",
|
title: "Dashboard",
|
||||||
icon: <LayoutDashboard className="w-5 h-5" />,
|
icon: <LayoutDashboard className="w-5 h-5" />,
|
||||||
children: [
|
href: "/sys-rijig-adminpanel/dashboard"
|
||||||
{
|
|
||||||
title: "Overview",
|
|
||||||
href: "/sys-rijig-adminpanel/dashboard"
|
|
||||||
},
|
|
||||||
{ title: "Analytics", href: "/admin/analytics" },
|
|
||||||
{ title: "Statistik Sampah", href: "/admin/waste-stats", badge: "new" },
|
|
||||||
{ title: "Laporan Harian", href: "/admin/daily-reports" }
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Data Sampah",
|
title: "Data Sampah",
|
||||||
icon: <Recycle className="w-5 h-5" />,
|
icon: <Recycle className="w-5 h-5" />,
|
||||||
children: [
|
href: "/sys-rijig-adminpanel/dashboard/waste"
|
||||||
{ title: "Jenis Sampah", href: "/sys-rijig-adminpanel/dashboard/waste" },
|
|
||||||
{ title: "Harga Sampah", href: "/admin/waste-prices" },
|
|
||||||
{ title: "Volume Sampah", href: "/admin/waste-volume" },
|
|
||||||
{ title: "Tracking Sampah", href: "/admin/waste-tracking", badge: "new" },
|
|
||||||
{ title: "Kualitas Sampah", href: "/admin/waste-quality" }
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Manajemen User",
|
title: "Manajemen User",
|
||||||
icon: <Users className="w-5 h-5" />,
|
icon: <Users className="w-5 h-5" />,
|
||||||
children: [
|
children: [
|
||||||
{ title: "Masyarakat", href: "/admin/users/community" },
|
|
||||||
{ title: "Pengepul", href: "/admin/users/collectors" },
|
|
||||||
{ title: "Pengelola Daur Ulang", href: "/admin/users/recyclers" },
|
|
||||||
{
|
{
|
||||||
title: "Verifikasi User",
|
title: "Verifikasi User",
|
||||||
href: "/sys-rijig-adminpanel/dashboard/users",
|
href: "/sys-rijig-adminpanel/dashboard/users",
|
||||||
badge: "urgent"
|
badge: "urgent"
|
||||||
},
|
},
|
||||||
{ title: "Rating & Review", href: "/admin/users/reviews" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Transaksi",
|
|
||||||
icon: <CreditCard className="w-5 h-5" />,
|
|
||||||
children: [
|
|
||||||
{ title: "Semua Transaksi", href: "/admin/transactions/all" },
|
|
||||||
{
|
{
|
||||||
title: "Pembayaran Pending",
|
title: "Masyarakat",
|
||||||
href: "/admin/transactions/pending",
|
href: "/sys-rijig-adminpanel/masyarakat"
|
||||||
badge: "urgent"
|
|
||||||
},
|
},
|
||||||
{ title: "Riwayat Pembayaran", href: "/admin/transactions/history" },
|
|
||||||
{ title: "Komisi & Fee", href: "/admin/transactions/commission" },
|
|
||||||
{
|
{
|
||||||
title: "Laporan Keuangan",
|
title: "Pengepul",
|
||||||
href: "/admin/transactions/financial-report"
|
href: "/sys-rijig-adminpanel/pengepul"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Pengelola",
|
||||||
|
href: "/sys-rijig-adminpanel/pengelola"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const contentMenuItems: MenuItem[] = [
|
|
||||||
{
|
|
||||||
title: "Content Management",
|
|
||||||
icon: <FileText className="w-5 h-5" />,
|
|
||||||
children: [
|
|
||||||
{ title: "Artikel & Blog", href: "/sys-rijig-adminpanel/dashboard/artikel-blog" },
|
|
||||||
{ title: "Tips & Panduan", href: "/sys-rijig-adminpanel/dashboard/tips-panduan" },
|
|
||||||
{ title: "FAQ", href: "/admin/content/faq" },
|
|
||||||
{
|
|
||||||
title: "Pengumuman",
|
|
||||||
href: "/admin/content/announcements",
|
|
||||||
badge: "new"
|
|
||||||
},
|
|
||||||
{ title: "Testimoni", href: "/admin/content/testimonials" }
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Lokasi & Mapping",
|
title: "Artikel & Blog",
|
||||||
icon: <MapPin className="w-5 h-5" />,
|
icon: <Newspaper className="w-5 h-5" />,
|
||||||
children: [
|
href: "/sys-rijig-adminpanel/dashboard/artikel-blog"
|
||||||
{ title: "Peta Pengepul", href: "/admin/mapping/collectors" },
|
|
||||||
{ title: "Area Coverage", href: "/sys-rijig-adminpanel/dashboard/areacoverage" },
|
|
||||||
{ title: "Titik Pengumpulan", href: "/admin/mapping/collection-points" },
|
|
||||||
{ title: "Rute Optimal", href: "/admin/mapping/routes", badge: "new" }
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Notifikasi",
|
title: "Tips & Panduan",
|
||||||
icon: <Bell className="w-5 h-5" />,
|
|
||||||
children: [
|
|
||||||
{ title: "Push Notifications", href: "/admin/notifications/push" },
|
|
||||||
{ title: "Email Broadcast", href: "/admin/notifications/email" },
|
|
||||||
{ title: "SMS Gateway", href: "/admin/notifications/sms" },
|
|
||||||
{ title: "Template Pesan", href: "/admin/notifications/templates" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const analyticsMenuItems: MenuItem[] = [
|
|
||||||
{
|
|
||||||
title: "Reports & Analytics",
|
|
||||||
icon: <BarChart3 className="w-5 h-5" />,
|
|
||||||
children: [
|
|
||||||
{ title: "Laporan Bulanan", href: "/admin/reports/monthly" },
|
|
||||||
{
|
|
||||||
title: "Performa Pengepul",
|
|
||||||
href: "/admin/reports/collector-performance"
|
|
||||||
},
|
|
||||||
{ title: "Tren Harga", href: "/admin/reports/price-trends" },
|
|
||||||
{
|
|
||||||
title: "Dampak Lingkungan",
|
|
||||||
href: "/admin/reports/environmental-impact",
|
|
||||||
badge: "new"
|
|
||||||
},
|
|
||||||
{ title: "ROI Analysis", href: "/admin/reports/roi-analysis" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Partner & Kerjasama",
|
|
||||||
icon: <Building2 className="w-5 h-5" />,
|
|
||||||
children: [
|
|
||||||
{ title: "Bank Sampah", href: "/admin/partners/waste-banks" },
|
|
||||||
{
|
|
||||||
title: "Industri Daur Ulang",
|
|
||||||
href: "/admin/partners/recycling-industry"
|
|
||||||
},
|
|
||||||
{ title: "Pemerintah Daerah", href: "/admin/partners/government" },
|
|
||||||
{ title: "NGO & Komunitas", href: "/admin/partners/ngo-community" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Support & Help",
|
|
||||||
icon: <HelpCircle className="w-5 h-5" />,
|
icon: <HelpCircle className="w-5 h-5" />,
|
||||||
children: [
|
href: "/sys-rijig-adminpanel/dashboard/tips-panduan"
|
||||||
{
|
},
|
||||||
title: "Tiket Support",
|
{
|
||||||
href: "/admin/support/tickets",
|
title: "Area Coverage",
|
||||||
badge: "urgent"
|
icon: <MapPin className="w-5 h-5" />,
|
||||||
},
|
href: "/sys-rijig-adminpanel/dashboard/areacoverage"
|
||||||
{ title: "Live Chat", href: "/admin/support/chat" },
|
|
||||||
{ title: "Knowledge Base", href: "/admin/support/knowledge-base" },
|
|
||||||
{ title: "Training Materials", href: "/admin/support/training" }
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Pengaturan",
|
title: "Pengaturan",
|
||||||
icon: <Settings className="w-5 h-5" />,
|
icon: <Settings className="w-5 h-5" />,
|
||||||
children: [
|
href: "/sys-rijig-adminpanel/dashboard/pengaturan"
|
||||||
{ title: "Konfigurasi Sistem", href: "/sys-rijig-adminpanel/dashboard/pengaturan" },
|
|
||||||
{ title: "Pengaturan Harga", href: "/admin/settings/pricing" },
|
|
||||||
{
|
|
||||||
title: "Role & Permission",
|
|
||||||
href: "/admin/settings/roles",
|
|
||||||
badge: "pro"
|
|
||||||
},
|
|
||||||
{ title: "Backup & Restore", href: "/admin/settings/backup" },
|
|
||||||
{ title: "API Management", href: "/admin/settings/api", badge: "pro" }
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
function MenuSection({
|
|
||||||
title,
|
|
||||||
items,
|
|
||||||
isCollapsed,
|
|
||||||
isHovered
|
|
||||||
}: {
|
|
||||||
title: string;
|
|
||||||
items: MenuItem[];
|
|
||||||
isCollapsed: boolean;
|
|
||||||
isHovered: boolean;
|
|
||||||
}) {
|
|
||||||
const showText = !isCollapsed || isHovered;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-2">
|
|
||||||
{showText && (
|
|
||||||
<h3 className="mb-4 px-3 text-xs uppercase text-gray-500 dark:text-gray-300 font-semibold tracking-wider">
|
|
||||||
{title}
|
|
||||||
</h3>
|
|
||||||
)}
|
|
||||||
<ul className="space-y-1">
|
|
||||||
{items.map((item, index) => (
|
|
||||||
<MenuItemComponent
|
|
||||||
key={index}
|
|
||||||
item={item}
|
|
||||||
isCollapsed={isCollapsed}
|
|
||||||
isHovered={isHovered}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MenuItemComponent({
|
function MenuItemComponent({
|
||||||
item,
|
item,
|
||||||
isCollapsed,
|
isCollapsed,
|
||||||
|
@ -292,7 +143,8 @@ function MenuItemComponent({
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full h-12 p-0 justify-center transition-colors",
|
"w-full h-12 p-0 justify-center transition-colors",
|
||||||
isChildActive && "bg-green-100 dark:bg-green-900/30"
|
(isChildActive || pathname === item.href) &&
|
||||||
|
"bg-green-100 dark:bg-green-900/30"
|
||||||
)}
|
)}
|
||||||
asChild={!hasChildren}
|
asChild={!hasChildren}
|
||||||
>
|
>
|
||||||
|
@ -437,33 +289,55 @@ function MenuItemComponent({
|
||||||
asChild
|
asChild
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full justify-start px-3 py-2.5 h-auto transition-colors",
|
"w-full justify-between px-3 py-2.5 h-auto transition-colors",
|
||||||
"hover:bg-gray-100 dark:hover:bg-gray-700",
|
"hover:bg-gray-100 dark:hover:bg-gray-700",
|
||||||
isActive &&
|
isActive &&
|
||||||
"bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/40 dark:text-green-400 dark:hover:bg-green-900/50"
|
"bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/40 dark:text-green-400 dark:hover:bg-green-900/50"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Link to={item.href || "#"}>
|
<Link to={item.href || "#"}>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center justify-between w-full">
|
||||||
<span
|
<div className="flex items-center gap-3">
|
||||||
className={cn(
|
|
||||||
"transition-colors",
|
|
||||||
isActive
|
|
||||||
? "text-green-600 dark:text-green-400"
|
|
||||||
: "text-gray-700 dark:text-gray-100"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{item.icon}
|
|
||||||
</span>
|
|
||||||
{showText && (
|
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-sm font-medium text-gray-900 dark:text-gray-100",
|
"transition-colors",
|
||||||
isActive && "text-green-700 dark:text-green-400"
|
isActive
|
||||||
|
? "text-green-600 dark:text-green-400"
|
||||||
|
: "text-gray-700 dark:text-gray-100"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{item.title}
|
{item.icon}
|
||||||
</span>
|
</span>
|
||||||
|
{showText && (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"text-sm font-medium text-gray-900 dark:text-gray-100",
|
||||||
|
isActive && "text-green-700 dark:text-green-400"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{item.title}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{showText && item.badge && (
|
||||||
|
<Badge
|
||||||
|
variant={
|
||||||
|
item.badge === "urgent"
|
||||||
|
? "destructive"
|
||||||
|
: item.badge === "pro"
|
||||||
|
? "secondary"
|
||||||
|
: "default"
|
||||||
|
}
|
||||||
|
className={cn(
|
||||||
|
"text-xs ml-2",
|
||||||
|
item.badge === "urgent" &&
|
||||||
|
"bg-red-100 text-red-700 hover:bg-red-200",
|
||||||
|
item.badge === "new" &&
|
||||||
|
"bg-green-100 text-green-700 hover:bg-green-200"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{item.badge}
|
||||||
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -500,12 +374,15 @@ export function AdminSidebar({
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<div className="h-16 flex items-center justify-between px-4 border-b border-gray-200 dark:border-gray-700">
|
<div className="h-16 flex items-center justify-between px-4 border-b border-gray-200 dark:border-gray-700">
|
||||||
{showText && (
|
{showText && (
|
||||||
<Link to="/admin/dashboard" className="flex items-center gap-2">
|
<Link
|
||||||
|
to="/sys-rijig-adminpanel/dashboard"
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
<div className="w-8 h-8 bg-green-600 rounded-lg flex items-center justify-center text-white text-sm font-bold">
|
<div className="w-8 h-8 bg-green-600 rounded-lg flex items-center justify-center text-white text-sm font-bold">
|
||||||
♻️
|
♻️
|
||||||
</div>
|
</div>
|
||||||
<div className="text-lg font-bold text-gray-900 dark:text-white">
|
<div className="text-lg font-bold text-gray-900 dark:text-white">
|
||||||
RIjig Admin
|
Rijig Admin
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
@ -530,25 +407,25 @@ export function AdminSidebar({
|
||||||
|
|
||||||
{/* Navigation */}
|
{/* Navigation */}
|
||||||
<ScrollArea className="flex-1 h-[calc(100vh-64px)]">
|
<ScrollArea className="flex-1 h-[calc(100vh-64px)]">
|
||||||
<div className="py-6 px-3 space-y-6">
|
<div className="py-6 px-3">
|
||||||
<MenuSection
|
{/* Main Menu */}
|
||||||
title="Core Management"
|
<div className="space-y-1">
|
||||||
items={menuItems}
|
{showText && (
|
||||||
isCollapsed={isCollapsed}
|
<h3 className="mb-4 px-3 text-xs uppercase text-gray-500 dark:text-gray-300 font-semibold tracking-wider">
|
||||||
isHovered={isHovered}
|
Admin Panel
|
||||||
/>
|
</h3>
|
||||||
<MenuSection
|
)}
|
||||||
title="Content & Communications"
|
<ul className="space-y-1">
|
||||||
items={contentMenuItems}
|
{menuItems.map((item, index) => (
|
||||||
isCollapsed={isCollapsed}
|
<MenuItemComponent
|
||||||
isHovered={isHovered}
|
key={index}
|
||||||
/>
|
item={item}
|
||||||
<MenuSection
|
isCollapsed={isCollapsed}
|
||||||
title="Analytics & Administration"
|
isHovered={isHovered}
|
||||||
items={analyticsMenuItems}
|
/>
|
||||||
isCollapsed={isCollapsed}
|
))}
|
||||||
isHovered={isHovered}
|
</ul>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer CTA - Only show when not collapsed or when hovered */}
|
{/* Footer CTA - Only show when not collapsed or when hovered */}
|
||||||
|
@ -557,7 +434,7 @@ export function AdminSidebar({
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<Recycle className="w-4 h-4 text-green-600 dark:text-green-400" />
|
<Recycle className="w-4 h-4 text-green-600 dark:text-green-400" />
|
||||||
<h4 className="font-semibold text-green-900 dark:text-green-100 text-sm">
|
<h4 className="font-semibold text-green-900 dark:text-green-100 text-sm">
|
||||||
RIjig Dashboard
|
Rijig Dashboard
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<p className="mb-4 text-green-700 dark:text-green-200 text-xs">
|
<p className="mb-4 text-green-700 dark:text-green-200 text-xs">
|
||||||
|
|
|
@ -19,8 +19,6 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// pada kode app/routes/pengelola.tsx pada bagian:
|
|
||||||
|
|
||||||
export default function PengelolaPanelLayout() {
|
export default function PengelolaPanelLayout() {
|
||||||
const { user } = useLoaderData<LoaderData>();
|
const { user } = useLoaderData<LoaderData>();
|
||||||
return (
|
return (
|
||||||
|
@ -29,7 +27,3 @@ export default function PengelolaPanelLayout() {
|
||||||
</PengelolaLayoutWrapper>
|
</PengelolaLayoutWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* terdapat error ini: Type '{ children: Element; user: SessionData; }' is not assignable to type 'IntrinsicAttributes & PengelolaLayoutWrapperProps'.
|
|
||||||
Property 'user' does not exist on type 'IntrinsicAttributes & PengelolaLayoutWrapperProps'. */
|
|
|
@ -1,25 +1,30 @@
|
||||||
import { json } from "@remix-run/node";
|
import { json, LoaderFunctionArgs } from "@remix-run/node";
|
||||||
import { Outlet, useLoaderData } from "@remix-run/react";
|
import { Outlet, useLoaderData } from "@remix-run/react";
|
||||||
import { AdminLayoutWrapper } from "~/components/layoutadmin/layout-wrapper";
|
import { AdminLayoutWrapper } from "~/components/layoutadmin/layout-wrapper";
|
||||||
|
import { requireUserSession, SessionData } from "~/sessions.server";
|
||||||
|
|
||||||
export const loader = async () => {
|
interface LoaderData {
|
||||||
// Data untuk layout bisa diambil di sini
|
user: SessionData;
|
||||||
return json({
|
}
|
||||||
user: {
|
|
||||||
name: "Musharof",
|
export async function loader({ request }: LoaderFunctionArgs) {
|
||||||
email: "admin@example.com",
|
const userSession = await requireUserSession(
|
||||||
role: "Administrator"
|
request,
|
||||||
}
|
"administrator",
|
||||||
|
"complete"
|
||||||
|
);
|
||||||
|
|
||||||
|
return json<LoaderData>({
|
||||||
|
user: userSession
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
export default function AdminPanelLayout() {
|
export default function AdminPanelLayout() {
|
||||||
const { user } = useLoaderData<typeof loader>();
|
const { user } = useLoaderData<typeof loader>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminLayoutWrapper>
|
<AdminLayoutWrapper user={user}>
|
||||||
{/* Outlet akan merender child routes */}
|
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</AdminLayoutWrapper>
|
</AdminLayoutWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue