Rekursif nav items but not finished yet
This commit is contained in:
parent
d14cd33ef3
commit
bf12ded925
|
@ -1,4 +1,3 @@
|
|||
import { AppSidebar } from "@/components/app-sidebar";
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { checkSession } from "@/actions/auth/session";
|
||||
import { AppSidebar } from "@/components/app-sidebar";
|
||||
import {
|
||||
Breadcrumb,
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { AppSidebar } from "@/components/app-sidebar";
|
||||
import { MapboxMap } from "@/components/map/mapbox-view";
|
||||
import {
|
||||
Breadcrumb,
|
||||
|
|
|
@ -1,20 +1,28 @@
|
|||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
AudioWaveform,
|
||||
Command,
|
||||
Frame,
|
||||
GalleryVerticalEnd,
|
||||
Loader2,
|
||||
SquareTerminal,
|
||||
} from "lucide-react";
|
||||
|
||||
|
||||
import { NavMain } from "@/components/nav-main";
|
||||
import { NavProjects } from "@/components/nav-projects";
|
||||
import { NavUser } from "@/components/nav-user";
|
||||
import { TeamSwitcher } from "@/components/team-switcher";
|
||||
|
||||
import {
|
||||
IconHome,
|
||||
IconAlertTriangle,
|
||||
IconSettings,
|
||||
IconMap,
|
||||
IconDatabase,
|
||||
IconUsers,
|
||||
IconMessageCircle,
|
||||
IconMenu2,
|
||||
IconAlbum,
|
||||
IconMusicBolt,
|
||||
IconCommand,
|
||||
IconFrame,
|
||||
IconChartPie,
|
||||
} from "@tabler/icons-react";
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
|
@ -22,18 +30,9 @@ import {
|
|||
SidebarHeader,
|
||||
SidebarRail,
|
||||
} from "@/components/ui/sidebar";
|
||||
import {
|
||||
NavItemsGet,
|
||||
NavSubItems,
|
||||
} from "@/src/applications/entities/models/nav-items.model";
|
||||
import { getNavItems } from "@/actions/dashboard/nav-items";
|
||||
import DynamicIcon, { DynamicIconProps } from "./dynamic-icon";
|
||||
|
||||
// 🔧 Konstanta untuk localStorage key
|
||||
const NAV_ITEMS_STORAGE_KEY = "navItemsData";
|
||||
|
||||
// 🎨 Data fallback
|
||||
const fallbackData = {
|
||||
// This is sample data.
|
||||
const data = {
|
||||
user: {
|
||||
name: "shadcn",
|
||||
email: "m@example.com",
|
||||
|
@ -42,160 +41,258 @@ const fallbackData = {
|
|||
teams: [
|
||||
{
|
||||
name: "Acme Inc",
|
||||
logo: GalleryVerticalEnd,
|
||||
logo: IconAlbum,
|
||||
plan: "Enterprise",
|
||||
},
|
||||
{
|
||||
name: "Acme Corp.",
|
||||
logo: AudioWaveform,
|
||||
logo: IconMusicBolt,
|
||||
plan: "Startup",
|
||||
},
|
||||
{
|
||||
name: "Evil Corp.",
|
||||
logo: Command,
|
||||
logo: IconCommand,
|
||||
plan: "Free",
|
||||
},
|
||||
],
|
||||
navMain: [
|
||||
{
|
||||
title: "Playground",
|
||||
url: "#",
|
||||
icon: SquareTerminal,
|
||||
title: "Dashboard",
|
||||
url: "/dashboard",
|
||||
slug: "dashboard",
|
||||
orderSeq: 1,
|
||||
icon: IconHome,
|
||||
isActive: true,
|
||||
items: [
|
||||
{
|
||||
title: "History",
|
||||
url: "#",
|
||||
subItems: [],
|
||||
},
|
||||
{
|
||||
title: "Starred",
|
||||
url: "#",
|
||||
title: "Crime Management",
|
||||
url: "/crime-management",
|
||||
slug: "crime-management",
|
||||
orderSeq: 2,
|
||||
icon: IconAlertTriangle,
|
||||
isActive: true,
|
||||
subItems: [
|
||||
{
|
||||
title: "Crime Overview",
|
||||
url: "/crime-management/crime-overview",
|
||||
slug: "crime-overview",
|
||||
icon: IconAlertTriangle,
|
||||
orderSeq: 1,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
title: "Settings",
|
||||
url: "#",
|
||||
title: "Crime Categories",
|
||||
url: "/crime-management/crime-categories",
|
||||
slug: "crime-categories",
|
||||
icon: IconSettings,
|
||||
orderSeq: 2,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
title: "Cases",
|
||||
url: "/crime-management/crime-cases",
|
||||
slug: "crime-cases",
|
||||
icon: IconAlertTriangle,
|
||||
orderSeq: 3,
|
||||
isActive: true,
|
||||
subSubItems: [
|
||||
{
|
||||
title: "New Case",
|
||||
url: "/crime-management/crime-cases/case-new",
|
||||
slug: "new-case",
|
||||
icon: IconAlertTriangle,
|
||||
orderSeq: 1,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
title: "Active Cases",
|
||||
url: "/crime-management/crime-cases/case-active",
|
||||
slug: "active-cases",
|
||||
icon: IconAlertTriangle,
|
||||
orderSeq: 2,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
title: "Resolved Cases",
|
||||
url: "/crime-management/crime-cases/case-closed",
|
||||
slug: "resolved-cases",
|
||||
icon: IconAlertTriangle,
|
||||
orderSeq: 3,
|
||||
isActive: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Geographic Data",
|
||||
url: "/geographic-data",
|
||||
slug: "geographic-data",
|
||||
orderSeq: 3,
|
||||
icon: IconMap,
|
||||
isActive: true,
|
||||
subItems: [
|
||||
{
|
||||
title: "Locations",
|
||||
url: "/geographic-data/locations",
|
||||
slug: "locations",
|
||||
icon: IconMap,
|
||||
orderSeq: 1,
|
||||
isActive: true,
|
||||
subSubItems: [
|
||||
{
|
||||
title: "Cities",
|
||||
url: "/geographic-data/cities",
|
||||
slug: "cities",
|
||||
icon: IconMap,
|
||||
orderSeq: 1,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
title: "Districts",
|
||||
url: "/geographic-data/districts",
|
||||
slug: "districts",
|
||||
icon: IconMap,
|
||||
orderSeq: 2,
|
||||
isActive: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Geographic Info",
|
||||
url: "/geographic-data/geographic-info",
|
||||
slug: "geographic-info",
|
||||
icon: IconMap,
|
||||
orderSeq: 3,
|
||||
isActive: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Demographics",
|
||||
url: "/demographics",
|
||||
slug: "demographics",
|
||||
orderSeq: 4,
|
||||
icon: IconDatabase,
|
||||
isActive: true,
|
||||
subItems: [
|
||||
{
|
||||
title: "Demographics Data",
|
||||
url: "/demographics/demographics-data",
|
||||
slug: "demographics-data",
|
||||
icon: IconDatabase,
|
||||
orderSeq: 1,
|
||||
isActive: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "User Management",
|
||||
url: "/user-management",
|
||||
slug: "user-management",
|
||||
orderSeq: 5,
|
||||
icon: IconUsers,
|
||||
isActive: true,
|
||||
subItems: [
|
||||
{
|
||||
title: "Users",
|
||||
url: "/user-management/users",
|
||||
slug: "users",
|
||||
icon: IconUsers,
|
||||
orderSeq: 1,
|
||||
isActive: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Communication",
|
||||
url: "/communication",
|
||||
slug: "communication",
|
||||
orderSeq: 6,
|
||||
icon: IconMessageCircle,
|
||||
isActive: true,
|
||||
subItems: [
|
||||
{
|
||||
title: "Contact Messages",
|
||||
url: "/communication/contact-messages",
|
||||
slug: "contact-messages",
|
||||
icon: IconMessageCircle,
|
||||
orderSeq: 1,
|
||||
isActive: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Settings",
|
||||
url: "/settings",
|
||||
slug: "settings",
|
||||
orderSeq: 7,
|
||||
icon: IconSettings,
|
||||
isActive: true,
|
||||
subItems: [
|
||||
{
|
||||
title: "Navigation",
|
||||
url: "/settings/navigation",
|
||||
slug: "navigation",
|
||||
icon: IconMenu2,
|
||||
orderSeq: 1,
|
||||
isActive: true,
|
||||
subSubItems: [
|
||||
{
|
||||
title: "Nav Items",
|
||||
url: "/settings/navigation/nav-items",
|
||||
slug: "nav-items",
|
||||
icon: IconMenu2,
|
||||
orderSeq: 1,
|
||||
isActive: true,
|
||||
subSubItems: [
|
||||
{
|
||||
title: "Nav Sub Items",
|
||||
url: "/settings/navigation/nav-sub-items",
|
||||
slug: "nav-sub-items",
|
||||
icon: IconMenu2,
|
||||
orderSeq: 1,
|
||||
isActive: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
// additional items...
|
||||
],
|
||||
projects: [
|
||||
{
|
||||
name: "Design Engineering",
|
||||
url: "#",
|
||||
icon: Frame,
|
||||
icon: IconFrame,
|
||||
},
|
||||
{
|
||||
name: "Sales & Marketing",
|
||||
url: "#",
|
||||
icon: IconChartPie,
|
||||
},
|
||||
{
|
||||
name: "Travel",
|
||||
url: "#",
|
||||
icon: IconMap,
|
||||
},
|
||||
// additional projects...
|
||||
],
|
||||
};
|
||||
|
||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
const [navItems, setNavItems] = useState<NavItemsGet | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// 📦 Ambil data dari localStorage
|
||||
const loadNavItemsFromStorage = (): NavItemsGet | null => {
|
||||
const storedData = localStorage.getItem(NAV_ITEMS_STORAGE_KEY);
|
||||
return storedData ? (JSON.parse(storedData) as NavItemsGet) : null;
|
||||
};
|
||||
|
||||
// ⚡ Fungsi untuk membandingkan dua objek secara deep
|
||||
const isDataEqual = (data1: NavItemsGet, data2: NavItemsGet): boolean =>
|
||||
JSON.stringify(data1) === JSON.stringify(data2);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAndSyncNavItems = async () => {
|
||||
try {
|
||||
const localData = loadNavItemsFromStorage();
|
||||
|
||||
if (localData) {
|
||||
setNavItems(localData);
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
const response = await getNavItems();
|
||||
if (response.success && response.data) {
|
||||
const fetchedData = response.data as NavItemsGet;
|
||||
const currentStoredData = loadNavItemsFromStorage();
|
||||
|
||||
// 🔄 Jika data berbeda, update localStorage dan state
|
||||
if (
|
||||
currentStoredData &&
|
||||
!isDataEqual(currentStoredData, fetchedData)
|
||||
) {
|
||||
localStorage.setItem(
|
||||
NAV_ITEMS_STORAGE_KEY,
|
||||
JSON.stringify(fetchedData)
|
||||
);
|
||||
setNavItems(fetchedData);
|
||||
}
|
||||
} else {
|
||||
setError(response.error || "Failed to load navigation items");
|
||||
}
|
||||
} catch (err) {
|
||||
setError("An unexpected error occurred while fetching navigation");
|
||||
console.error(err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchAndSyncNavItems();
|
||||
}, []);
|
||||
|
||||
// 🎛️ Transformasi data DB ke format NavMain
|
||||
const formatNavItems = React.useMemo(() => {
|
||||
if (!navItems) return fallbackData.navMain;
|
||||
return navItems.map((item) => ({
|
||||
title: item.title,
|
||||
url: item.url,
|
||||
icon: item.icon,
|
||||
isActive: item.isActive,
|
||||
items: item.subItems.map((subItem) => ({
|
||||
title: subItem.title,
|
||||
url: subItem.url,
|
||||
icon: subItem.icon,
|
||||
isActive: subItem.isActive,
|
||||
})),
|
||||
}));
|
||||
}, [navItems]);
|
||||
|
||||
// 🧩 Komponen NavMain dinamis dengan ikon
|
||||
const DynamicNavMain = ({ items }: { items: any[] }) => (
|
||||
<NavMain
|
||||
items={items.map((item) => ({
|
||||
...item,
|
||||
icon: () => <DynamicIcon iconName={item.icon} size={24} stroke={2} />,
|
||||
items: item.items.map((subItem: NavSubItems) => ({
|
||||
...subItem,
|
||||
icon: () => (
|
||||
<DynamicIcon iconName={subItem.icon} size={24} stroke={2} />
|
||||
),
|
||||
})),
|
||||
}))}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Sidebar collapsible="icon" {...props}>
|
||||
<SidebarHeader>
|
||||
<TeamSwitcher teams={fallbackData.teams} />
|
||||
<TeamSwitcher teams={data.teams} />
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center items-center py-8">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="px-4 py-2 text-sm text-destructive">{error}</div>
|
||||
) : (
|
||||
<DynamicNavMain items={formatNavItems} />
|
||||
)}
|
||||
{/* <NavProjects projects={fallbackData.projects} /> */}
|
||||
<NavMain items={data.navMain} />
|
||||
<NavProjects projects={data.projects} />
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<NavUser user={fallbackData.user} />
|
||||
<NavUser user={data.user} />
|
||||
</SidebarFooter>
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { ChevronRight, type LucideIcon } from "lucide-react"
|
||||
import { ChevronRight } from "lucide-react";
|
||||
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible"
|
||||
} from "@/components/ui/collapsible";
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupLabel,
|
||||
|
@ -14,71 +14,132 @@ import {
|
|||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
} from "@/components/ui/sidebar"
|
||||
} from "@/components/ui/sidebar";
|
||||
|
||||
import * as TablerIcons from "@tabler/icons-react";
|
||||
|
||||
export function NavMain({
|
||||
items,
|
||||
}: {
|
||||
items: {
|
||||
interface SubSubItem {
|
||||
title: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface SubItem {
|
||||
title: string;
|
||||
url: string;
|
||||
icon?: TablerIcons.Icon;
|
||||
subSubItems?: SubSubItem[];
|
||||
}
|
||||
|
||||
interface NavItem {
|
||||
title: string;
|
||||
url: string;
|
||||
icon?: TablerIcons.Icon;
|
||||
isActive?: boolean;
|
||||
items?: {
|
||||
title: string;
|
||||
icon?: TablerIcons.Icon;
|
||||
url: string;
|
||||
}[];
|
||||
}[];
|
||||
}) {
|
||||
subItems?: SubItem[];
|
||||
}
|
||||
|
||||
function SubSubItemComponent({ item }: { item: SubSubItem }) {
|
||||
return (
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>Platform</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
{items.map((item) => (
|
||||
<Collapsible
|
||||
key={item.title}
|
||||
asChild
|
||||
defaultOpen={item.isActive}
|
||||
className="group/collapsible"
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
{item.items && item.items.length > 0 ? (
|
||||
<SidebarMenuButton asChild>
|
||||
<a href={item.url}>
|
||||
<span>{item.title}</span>
|
||||
</a>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
function SubItemComponent({ item }: { item: SubItem }) {
|
||||
const hasSubSubItems = item.subSubItems && item.subSubItems.length > 0;
|
||||
|
||||
if (!hasSubSubItems) {
|
||||
return (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild>
|
||||
<a href={item.url}>
|
||||
{item.icon && <item.icon />}
|
||||
<span>{item.title}</span>
|
||||
</a>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Collapsible asChild className="group/collapsible">
|
||||
<SidebarMenuItem>
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuButton tooltip={item.title}>
|
||||
<SidebarMenuButton>
|
||||
{item.icon && <item.icon />}
|
||||
<span>{item.title}</span>
|
||||
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
) : (
|
||||
<SidebarMenuButton asChild tooltip={item.title}>
|
||||
<a href={`/protected${item.url}`}>
|
||||
{item.icon && <item.icon />}
|
||||
<span>{item.title}</span>
|
||||
</a>
|
||||
</SidebarMenuButton>
|
||||
)}
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub>
|
||||
{item.items?.map((subItem) => (
|
||||
<SidebarMenuSubItem key={subItem.title}>
|
||||
<SidebarMenuSubButton asChild>
|
||||
<a href={`/protected${subItem.url}`}>
|
||||
{subItem.icon && <subItem.icon />}
|
||||
<span>{subItem.title}</span>
|
||||
</a>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
{item.subSubItems!.map((subSubItem) => (
|
||||
<SubSubItemComponent key={subSubItem.title} item={subSubItem} />
|
||||
))}
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
);
|
||||
}
|
||||
|
||||
function RecursiveNavItem({ item, index }: { item: NavItem; index: number }) {
|
||||
const hasSubItems = item.subItems && item.subItems.length > 0;
|
||||
|
||||
if (!hasSubItems) {
|
||||
return (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton tooltip={item.title} asChild>
|
||||
<a href={item.url}>
|
||||
{item.icon && <item.icon />}
|
||||
<span>{item.title}</span>
|
||||
</a>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Collapsible
|
||||
key={item.title}
|
||||
asChild
|
||||
defaultOpen={index === 1}
|
||||
className="group/collapsible"
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuButton tooltip={item.title}>
|
||||
{item.icon && <item.icon />}
|
||||
<span>{item.title}</span>
|
||||
{hasSubItems && (
|
||||
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
||||
)}
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub>
|
||||
{item.subItems!.map((subItem) => (
|
||||
<SubItemComponent key={subItem.title} item={subItem} />
|
||||
))}
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
);
|
||||
}
|
||||
|
||||
export function NavMain({ items }: { items: NavItem[] }) {
|
||||
return (
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>Platform</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
{items.map((item, index) => (
|
||||
<RecursiveNavItem key={item.title} item={item} index={index} />
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
|
|
|
@ -25,16 +25,18 @@ import {
|
|||
useSidebar,
|
||||
} from "@/components/ui/sidebar"
|
||||
|
||||
import * as TablerIcons from "@tabler/icons-react";
|
||||
|
||||
export function NavProjects({
|
||||
projects,
|
||||
}: {
|
||||
projects: {
|
||||
name: string
|
||||
url: string
|
||||
icon: LucideIcon
|
||||
}[]
|
||||
name: string;
|
||||
url: string;
|
||||
icon: TablerIcons.Icon;
|
||||
}[];
|
||||
}) {
|
||||
const { isMobile } = useSidebar()
|
||||
const { isMobile } = useSidebar();
|
||||
|
||||
return (
|
||||
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
|
||||
|
@ -85,5 +87,5 @@ export function NavProjects({
|
|||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,184 +1,53 @@
|
|||
import { useState, useCallback, useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { NavigationUseCases } from "@/src/applications/usecases/navigation-item.usecase";
|
||||
import { NavigationItem } from "@prisma/client";
|
||||
import {
|
||||
NavigationItemFormData,
|
||||
navigationItemSchema,
|
||||
} from "../validators/navigation-item.validator";
|
||||
import {
|
||||
CreateNavigationItemDTO,
|
||||
UpdateNavigationItemDTO,
|
||||
} from "@/src/applications/entities/models/navigation-item.model";
|
||||
import { toast } from "@/hooks/use-toast";
|
||||
import { NavigationItem } from "@/src/applications/entities/models/navigation-item.model";
|
||||
import { useEffect, useState } from "react";
|
||||
import { NavigationItemRepositoryImpl } from "../repositories/navigation-item.repository.impl";
|
||||
|
||||
export const useNavigationItem = (navigationUseCases: NavigationUseCases) => {
|
||||
export function useNavigationItem() {
|
||||
const [items, setItems] = useState<NavigationItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [formData, setFormData] = useState<NavigationItemFormData>({
|
||||
title: "",
|
||||
url: "",
|
||||
slug: "",
|
||||
icon: "",
|
||||
isActive: false,
|
||||
orderSeq: 0,
|
||||
parentId: null,
|
||||
});
|
||||
const [errors, setErrors] = useState<
|
||||
Partial<Record<keyof NavigationItemFormData, string>>
|
||||
>({});
|
||||
|
||||
const form = useForm<NavigationItemFormData>({
|
||||
resolver: zodResolver(navigationItemSchema),
|
||||
});
|
||||
|
||||
const loadNavigationTree = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await navigationUseCases.getNavigationTree();
|
||||
setItems(data);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(
|
||||
err instanceof Error ? err.message : "Failed to load navigation"
|
||||
);
|
||||
toast({
|
||||
title: "Error",
|
||||
description:
|
||||
err instanceof Error ? err.message : "Failed to load navigation",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [navigationUseCases]);
|
||||
|
||||
const createItem = useCallback(
|
||||
async (data: CreateNavigationItemDTO) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
await navigationUseCases.createNavigationItem(data);
|
||||
await loadNavigationTree();
|
||||
setError(null);
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "Navigation item created successfully",
|
||||
});
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to create item");
|
||||
toast({
|
||||
title: "Error",
|
||||
description:
|
||||
err instanceof Error ? err.message : "Failed to create item",
|
||||
variant: "destructive",
|
||||
});
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[navigationUseCases, loadNavigationTree]
|
||||
);
|
||||
|
||||
const updateItem = useCallback(
|
||||
async (id: string, data: UpdateNavigationItemDTO) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
await navigationUseCases.updateNavigationItem(id, data);
|
||||
await loadNavigationTree();
|
||||
setError(null);
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "Navigation item updated successfully",
|
||||
});
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to update item");
|
||||
toast({
|
||||
title: "Error",
|
||||
description:
|
||||
err instanceof Error ? err.message : "Failed to update item",
|
||||
variant: "destructive",
|
||||
});
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[navigationUseCases, loadNavigationTree]
|
||||
);
|
||||
|
||||
const deleteItem = useCallback(
|
||||
async (id: string) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
await navigationUseCases.deleteNavigationItem(id);
|
||||
await loadNavigationTree();
|
||||
setError(null);
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "Navigation item deleted successfully",
|
||||
});
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to delete item");
|
||||
toast({
|
||||
title: "Error",
|
||||
description:
|
||||
err instanceof Error ? err.message : "Failed to delete item",
|
||||
variant: "destructive",
|
||||
});
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[navigationUseCases, loadNavigationTree]
|
||||
);
|
||||
|
||||
const moveItem = useCallback(
|
||||
async (id: string, newParentPath: string, newOrderSeq: number) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
await navigationUseCases.moveItem(id, newParentPath, newOrderSeq);
|
||||
await loadNavigationTree();
|
||||
setError(null);
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "Navigation item moved successfully",
|
||||
});
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to move item");
|
||||
toast({
|
||||
title: "Error",
|
||||
description:
|
||||
err instanceof Error ? err.message : "Failed to move item",
|
||||
variant: "destructive",
|
||||
});
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[navigationUseCases, loadNavigationTree]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
loadNavigationTree();
|
||||
}, [loadNavigationTree]);
|
||||
const fetchNavigation = async () => {
|
||||
try {
|
||||
const repository = new NavigationItemRepositoryImpl();
|
||||
const navItems = await repository.findAll();
|
||||
|
||||
return {
|
||||
items,
|
||||
loading,
|
||||
error,
|
||||
form,
|
||||
formData,
|
||||
errors,
|
||||
setFormData,
|
||||
createItem,
|
||||
updateItem,
|
||||
deleteItem,
|
||||
moveItem,
|
||||
refresh: loadNavigationTree,
|
||||
// Convert flat structure to tree
|
||||
const buildNavigationTree = (
|
||||
items: NavigationItem[],
|
||||
parentPath: string = ""
|
||||
): any[] => {
|
||||
return items
|
||||
.filter((item) => {
|
||||
const itemPath = item.path.split("/").filter(Boolean);
|
||||
const parentPathParts = parentPath.split("/").filter(Boolean);
|
||||
return (
|
||||
itemPath.length === parentPathParts.length + 1 &&
|
||||
itemPath.slice(0, parentPathParts.length).join("/") ===
|
||||
parentPathParts.join("/")
|
||||
);
|
||||
})
|
||||
.map((item) => ({
|
||||
...item,
|
||||
subItems: buildNavigationTree(items, item.path),
|
||||
}))
|
||||
.sort((a, b) => a.orderSeq - b.orderSeq);
|
||||
};
|
||||
|
||||
const navigationTree = buildNavigationTree(navItems);
|
||||
setItems(navigationTree);
|
||||
} catch (err) {
|
||||
setError(
|
||||
err instanceof Error ? err.message : "Failed to fetch navigation"
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchNavigation();
|
||||
}, []);
|
||||
|
||||
return { items, loading, error };
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
} from "@/src/applications/entities/models/navigation-item.model";
|
||||
import { NavigationRepository } from "@/src/applications/repositories/navigation-item.repository";
|
||||
|
||||
export class NavigationItemRepository implements NavigationRepository {
|
||||
export class NavigationItemRepositoryImpl implements NavigationRepository {
|
||||
async findAll(): Promise<NavigationItem[]> {
|
||||
return db.navigationItem.findMany({
|
||||
orderBy: [{ path: "asc" }, { orderSeq: "asc" }],
|
||||
|
|
Loading…
Reference in New Issue