feat: authorization admin panel

This commit is contained in:
pahmiudahgede 2025-07-09 08:21:10 +07:00
parent fd740a99c3
commit c35b1c85df
5 changed files with 343 additions and 416 deletions

View File

@ -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>
</>
); );
} }

View File

@ -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 */}

View File

@ -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">

View File

@ -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'. */

View File

@ -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>
); );
} }