fecth dynamic navitem
This commit is contained in:
parent
7c58a7e46c
commit
bc0c6438a2
|
@ -0,0 +1,211 @@
|
||||||
|
"use server";
|
||||||
|
|
||||||
|
import {
|
||||||
|
NavItems,
|
||||||
|
navItemsDeleteSchema,
|
||||||
|
NavItemsInsert,
|
||||||
|
navItemsInsertSchema,
|
||||||
|
navItemsUpdateSchema,
|
||||||
|
} from "@/src/applications/entities/models/nav-items.model";
|
||||||
|
import { NavItemsRepository } from "@/src/applications/repositories/nav-items.repository";
|
||||||
|
import { NavItemsRepositoryImpl } from "@/src/infrastructure/repositories/nav-items.repository.impl";
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
// Initialize repository
|
||||||
|
const navItemsRepo: NavItemsRepository = new NavItemsRepositoryImpl();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all navigation items
|
||||||
|
*/
|
||||||
|
export async function getNavItems() {
|
||||||
|
return await navItemsRepo.getNavItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new navigation item
|
||||||
|
*/
|
||||||
|
export async function createNavItem(
|
||||||
|
formData: FormData | Record<string, NavItems>
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const data =
|
||||||
|
formData instanceof FormData
|
||||||
|
? Object.fromEntries(formData.entries())
|
||||||
|
: formData;
|
||||||
|
|
||||||
|
// Parse and validate input data
|
||||||
|
const validatedData = navItemsInsertSchema.parse(data);
|
||||||
|
|
||||||
|
// Call repository method (assuming it exists)
|
||||||
|
const result = await navItemsRepo.createNavItems(validatedData);
|
||||||
|
|
||||||
|
// Revalidate cache if successful
|
||||||
|
if (result.success) {
|
||||||
|
revalidatePath("/admin/navigation");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof z.ZodError) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
errors: error.errors.reduce(
|
||||||
|
(acc, curr) => {
|
||||||
|
acc[curr.path.join(".")] = curr.message;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, string>
|
||||||
|
),
|
||||||
|
message: "Validation failed",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Failed to create navigation item",
|
||||||
|
message:
|
||||||
|
error instanceof Error ? error.message : "Unknown error occurred",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an existing navigation item
|
||||||
|
*/
|
||||||
|
export async function updateNavItem(
|
||||||
|
id: string,
|
||||||
|
formData: FormData | Record<string, unknown>
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const data =
|
||||||
|
formData instanceof FormData
|
||||||
|
? Object.fromEntries(formData.entries())
|
||||||
|
: formData;
|
||||||
|
|
||||||
|
// Parse and validate input data
|
||||||
|
const validatedData = navItemsUpdateSchema.parse(data);
|
||||||
|
|
||||||
|
// Call repository method (assuming it exists)
|
||||||
|
const result = await navItemsRepo.updateNavItems(id, validatedData);
|
||||||
|
|
||||||
|
// Revalidate cache if successful
|
||||||
|
if (result.success) {
|
||||||
|
revalidatePath("/admin/navigation");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof z.ZodError) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
errors: error.errors.reduce(
|
||||||
|
(acc, curr) => {
|
||||||
|
acc[curr.path.join(".")] = curr.message;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, string>
|
||||||
|
),
|
||||||
|
message: "Validation failed",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Failed to update navigation item",
|
||||||
|
message:
|
||||||
|
error instanceof Error ? error.message : "Unknown error occurred",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a navigation item
|
||||||
|
*/
|
||||||
|
export async function deleteNavItem(id: string) {
|
||||||
|
try {
|
||||||
|
// Parse and validate input
|
||||||
|
const validatedData = navItemsDeleteSchema.parse({ id });
|
||||||
|
|
||||||
|
// Call repository method (assuming it exists)
|
||||||
|
const result = await navItemsRepo.deleteNavItems(validatedData.id);
|
||||||
|
|
||||||
|
// Revalidate cache if successful
|
||||||
|
if (result.success) {
|
||||||
|
revalidatePath("/admin/navigation");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof z.ZodError) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
errors: error.errors.reduce(
|
||||||
|
(acc, curr) => {
|
||||||
|
acc[curr.path.join(".")] = curr.message;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, string>
|
||||||
|
),
|
||||||
|
message: "Validation failed",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Failed to delete navigation item",
|
||||||
|
message:
|
||||||
|
error instanceof Error ? error.message : "Unknown error occurred",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Toggle active status of a navigation item
|
||||||
|
// */
|
||||||
|
// export async function toggleNavItemActive(id: string) {
|
||||||
|
// try {
|
||||||
|
// // Call repository method (assuming it exists)
|
||||||
|
// const result = await navItemsRepo.toggleActive(id);
|
||||||
|
|
||||||
|
// // Revalidate cache if successful
|
||||||
|
// if (result.success) {
|
||||||
|
// revalidatePath("/admin/navigation");
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return result;
|
||||||
|
// } catch (error) {
|
||||||
|
// return {
|
||||||
|
// success: false,
|
||||||
|
// error: "Failed to toggle navigation item status",
|
||||||
|
// message:
|
||||||
|
// error instanceof Error ? error.message : "Unknown error occurred",
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Update navigation items order
|
||||||
|
// */
|
||||||
|
// export async function updateNavItemsOrder(
|
||||||
|
// items: { id: string; order_seq: number }[]
|
||||||
|
// ) {
|
||||||
|
// try {
|
||||||
|
// // Call repository method (assuming it exists)
|
||||||
|
// const result = await navItemsRepo.updateOrder(items);
|
||||||
|
|
||||||
|
// // Revalidate cache if successful
|
||||||
|
// if (result.success) {
|
||||||
|
// revalidatePath("/admin/navigation");
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return result;
|
||||||
|
// } catch (error) {
|
||||||
|
// return {
|
||||||
|
// success: false,
|
||||||
|
// error: "Failed to update navigation order",
|
||||||
|
// message:
|
||||||
|
// error instanceof Error ? error.message : "Unknown error occurred",
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
// }
|
|
@ -16,37 +16,34 @@ import {
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return (
|
return (
|
||||||
<SidebarProvider>
|
<>
|
||||||
<AppSidebar />
|
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
|
||||||
<SidebarInset>
|
<div className="flex items-center gap-2 px-4">
|
||||||
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
|
<SidebarTrigger className="-ml-1" />
|
||||||
<div className="flex items-center gap-2 px-4">
|
<Separator orientation="vertical" className="mr-2 h-4" />
|
||||||
<SidebarTrigger className="-ml-1" />
|
<Breadcrumb>
|
||||||
<Separator orientation="vertical" className="mr-2 h-4" />
|
<BreadcrumbList>
|
||||||
<Breadcrumb>
|
<BreadcrumbItem className="hidden md:block">
|
||||||
<BreadcrumbList>
|
<BreadcrumbLink href="#">
|
||||||
<BreadcrumbItem className="hidden md:block">
|
Building Your Application
|
||||||
<BreadcrumbLink href="#">
|
</BreadcrumbLink>
|
||||||
Building Your Application
|
</BreadcrumbItem>
|
||||||
</BreadcrumbLink>
|
<BreadcrumbSeparator className="hidden md:block" />
|
||||||
</BreadcrumbItem>
|
<BreadcrumbItem>
|
||||||
<BreadcrumbSeparator className="hidden md:block" />
|
<BreadcrumbPage>Data Fetching</BreadcrumbPage>
|
||||||
<BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
<BreadcrumbPage>Data Fetching</BreadcrumbPage>
|
</BreadcrumbList>
|
||||||
</BreadcrumbItem>
|
</Breadcrumb>
|
||||||
</BreadcrumbList>
|
|
||||||
</Breadcrumb>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">
|
|
||||||
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
|
|
||||||
<div className="aspect-video rounded-xl bg-muted/50" />
|
|
||||||
<div className="aspect-video rounded-xl bg-muted/50" />
|
|
||||||
<div className="aspect-video rounded-xl bg-muted/50" />
|
|
||||||
</div>
|
|
||||||
<div className="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min" />
|
|
||||||
</div>
|
</div>
|
||||||
</SidebarInset>
|
</header>
|
||||||
</SidebarProvider>
|
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">
|
||||||
|
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
|
||||||
|
<div className="aspect-video rounded-xl bg-muted/50" />
|
||||||
|
<div className="aspect-video rounded-xl bg-muted/50" />
|
||||||
|
<div className="aspect-video rounded-xl bg-muted/50" />
|
||||||
|
</div>
|
||||||
|
<div className="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min" />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { checkSession } from "@/actions/auth/session";
|
||||||
|
import { AppSidebar } from "@/components/app-sidebar";
|
||||||
|
import {
|
||||||
|
Breadcrumb,
|
||||||
|
BreadcrumbItem,
|
||||||
|
BreadcrumbLink,
|
||||||
|
BreadcrumbList,
|
||||||
|
BreadcrumbPage,
|
||||||
|
BreadcrumbSeparator,
|
||||||
|
} from "@/components/ui/breadcrumb";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import {
|
||||||
|
SidebarInset,
|
||||||
|
SidebarProvider,
|
||||||
|
SidebarTrigger,
|
||||||
|
} from "@/components/ui/sidebar";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
|
export default async function Layout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<SidebarProvider>
|
||||||
|
<AppSidebar />
|
||||||
|
<SidebarInset>{children}</SidebarInset>
|
||||||
|
</SidebarProvider>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,33 +1,36 @@
|
||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
AudioWaveform,
|
AudioWaveform,
|
||||||
BookOpen,
|
|
||||||
Bot,
|
|
||||||
Command,
|
Command,
|
||||||
Frame,
|
Frame,
|
||||||
GalleryVerticalEnd,
|
GalleryVerticalEnd,
|
||||||
Map,
|
Loader2,
|
||||||
PieChart,
|
|
||||||
Settings2,
|
|
||||||
SquareTerminal,
|
SquareTerminal,
|
||||||
} from "lucide-react"
|
} from "lucide-react";
|
||||||
|
|
||||||
import { NavMain } from "@/components/nav-main"
|
import { NavMain } from "@/components/nav-main";
|
||||||
import { NavProjects } from "@/components/nav-projects"
|
import { NavProjects } from "@/components/nav-projects";
|
||||||
import { NavUser } from "@/components/nav-user"
|
import { NavUser } from "@/components/nav-user";
|
||||||
import { TeamSwitcher } from "@/components/team-switcher"
|
import { TeamSwitcher } from "@/components/team-switcher";
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
SidebarFooter,
|
SidebarFooter,
|
||||||
SidebarHeader,
|
SidebarHeader,
|
||||||
SidebarRail,
|
SidebarRail,
|
||||||
} from "@/components/ui/sidebar"
|
} from "@/components/ui/sidebar";
|
||||||
|
import { NavItemsGet } from "@/src/applications/entities/models/nav-items.model";
|
||||||
|
import { getNavItems } from "@/actions/dashboard/nav-items";
|
||||||
|
import DynamicIcon, { DynamicIconProps } from "./dynamic-icon";
|
||||||
|
|
||||||
// This is sample data.
|
// 🔧 Konstanta untuk localStorage key
|
||||||
const data = {
|
const NAV_ITEMS_STORAGE_KEY = "navItemsData";
|
||||||
|
|
||||||
|
// 🎨 Data fallback
|
||||||
|
const fallbackData = {
|
||||||
user: {
|
user: {
|
||||||
name: "shadcn",
|
name: "shadcn",
|
||||||
email: "m@example.com",
|
email: "m@example.com",
|
||||||
|
@ -71,71 +74,7 @@ const data = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
// additional items...
|
||||||
title: "Models",
|
|
||||||
url: "#",
|
|
||||||
icon: Bot,
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
title: "Genesis",
|
|
||||||
url: "#",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Explorer",
|
|
||||||
url: "#",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Quantum",
|
|
||||||
url: "#",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Documentation",
|
|
||||||
url: "#",
|
|
||||||
icon: BookOpen,
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
title: "Introduction",
|
|
||||||
url: "#",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Get Started",
|
|
||||||
url: "#",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Tutorials",
|
|
||||||
url: "#",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Changelog",
|
|
||||||
url: "#",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Settings",
|
|
||||||
url: "#",
|
|
||||||
icon: Settings2,
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
title: "General",
|
|
||||||
url: "#",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Team",
|
|
||||||
url: "#",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Billing",
|
|
||||||
url: "#",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Limits",
|
|
||||||
url: "#",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
|
@ -143,33 +82,109 @@ const data = {
|
||||||
url: "#",
|
url: "#",
|
||||||
icon: Frame,
|
icon: Frame,
|
||||||
},
|
},
|
||||||
{
|
// additional projects...
|
||||||
name: "Sales & Marketing",
|
|
||||||
url: "#",
|
|
||||||
icon: PieChart,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Travel",
|
|
||||||
url: "#",
|
|
||||||
icon: Map,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
}
|
};
|
||||||
|
|
||||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
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: any, data2: any): 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 (!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.is_active,
|
||||||
|
items: item.sub_items.map((subItem) => ({
|
||||||
|
title: subItem.title,
|
||||||
|
url: subItem.url,
|
||||||
|
isActive: subItem.is_active,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
}, [navItems]);
|
||||||
|
|
||||||
|
// 🧩 Komponen NavMain dinamis dengan ikon
|
||||||
|
const DynamicNavMain = ({ items }: { items: any[] }) => (
|
||||||
|
<NavMain
|
||||||
|
items={items.map((item) => ({
|
||||||
|
...item,
|
||||||
|
icon: () => <DynamicIcon iconName={item.icon} size={24} />,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sidebar collapsible="icon" {...props}>
|
<Sidebar collapsible="icon" {...props}>
|
||||||
<SidebarHeader>
|
<SidebarHeader>
|
||||||
<TeamSwitcher teams={data.teams} />
|
<TeamSwitcher teams={fallbackData.teams} />
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
<NavMain items={data.navMain} />
|
{isLoading ? (
|
||||||
<NavProjects projects={data.projects} />
|
<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} />
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
<NavUser user={data.user} />
|
<NavUser user={fallbackData.user} />
|
||||||
</SidebarFooter>
|
</SidebarFooter>
|
||||||
<SidebarRail />
|
<SidebarRail />
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
// components/icons/DynamicIcon.tsx
|
||||||
|
import React from "react";
|
||||||
|
import * as TablerIcons from "@tabler/icons-react";
|
||||||
|
|
||||||
|
export interface DynamicIconProps {
|
||||||
|
iconName: string;
|
||||||
|
size?: number;
|
||||||
|
color?: string;
|
||||||
|
className?: string;
|
||||||
|
stroke?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🔥 DynamicIcon - Reusable component untuk merender ikon @tabler/icons-react secara dinamis
|
||||||
|
* @param iconName - Nama ikon (harus sesuai dengan nama ikon dari @tabler/icons-react, contoh: "IconUsers")
|
||||||
|
* @param size - Ukuran ikon (default: 24)
|
||||||
|
* @param color - Warna ikon (opsional)
|
||||||
|
* @param className - CSS class tambahan (opsional)
|
||||||
|
* @param stroke - Ketebalan garis (opsional, default dari library)
|
||||||
|
*/
|
||||||
|
const DynamicIcon: React.FC<DynamicIconProps> = ({
|
||||||
|
iconName,
|
||||||
|
size = 32,
|
||||||
|
color,
|
||||||
|
className = "IconAlertHexagon",
|
||||||
|
stroke,
|
||||||
|
}) => {
|
||||||
|
// Safety check: Ensure the iconName exists in TablerIcons
|
||||||
|
if (!(iconName in TablerIcons)) {
|
||||||
|
console.warn(`Icon "${iconName}" not found in @tabler/icons-react library`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const IconComponent = TablerIcons[
|
||||||
|
iconName as keyof typeof TablerIcons
|
||||||
|
] as React.ComponentType<{
|
||||||
|
size?: number;
|
||||||
|
color?: string;
|
||||||
|
className?: string;
|
||||||
|
stroke?: number;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconComponent
|
||||||
|
size={size}
|
||||||
|
color={color}
|
||||||
|
className={className}
|
||||||
|
stroke={stroke}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DynamicIcon;
|
|
@ -8,8 +8,8 @@ declare const globalThis: {
|
||||||
prismaGlobal: ReturnType<typeof prismaClientSingleton>;
|
prismaGlobal: ReturnType<typeof prismaClientSingleton>;
|
||||||
} & typeof global;
|
} & typeof global;
|
||||||
|
|
||||||
const prisma = globalThis.prismaGlobal ?? prismaClientSingleton();
|
const db = globalThis.prismaGlobal ?? prismaClientSingleton();
|
||||||
|
|
||||||
export default prisma;
|
export default db;
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== "production") globalThis.prismaGlobal = prisma;
|
if (process.env.NODE_ENV !== "production") globalThis.prismaGlobal = db;
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
"@react-email/components": "0.0.33",
|
"@react-email/components": "0.0.33",
|
||||||
"@supabase/ssr": "latest",
|
"@supabase/ssr": "latest",
|
||||||
"@supabase/supabase-js": "latest",
|
"@supabase/supabase-js": "latest",
|
||||||
|
"@tabler/icons-react": "^3.30.0",
|
||||||
"autoprefixer": "10.4.20",
|
"autoprefixer": "10.4.20",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
@ -46,6 +47,7 @@
|
||||||
"@types/react-dom": "19.0.2",
|
"@types/react-dom": "19.0.2",
|
||||||
"postcss": "8.4.49",
|
"postcss": "8.4.49",
|
||||||
"prisma": "^6.3.1",
|
"prisma": "^6.3.1",
|
||||||
|
"prisma-json-types-generator": "^3.2.2",
|
||||||
"react-email": "3.0.7",
|
"react-email": "3.0.7",
|
||||||
"supabase": "^2.12.1",
|
"supabase": "^2.12.1",
|
||||||
"tailwind-merge": "^2.6.0",
|
"tailwind-merge": "^2.6.0",
|
||||||
|
@ -1590,6 +1592,23 @@
|
||||||
"@prisma/get-platform": "6.3.1"
|
"@prisma/get-platform": "6.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@prisma/generator-helper": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@prisma/generator-helper/-/generator-helper-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-5DkG7hspZo6U4OtqI2W0JcgtY37sr7HgT8Q0W/sjL4VoV4px6ivzK6Eif5bKM7q+S4yFUHtjUt/3s69ErfLn7A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@prisma/debug": "6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@prisma/generator-helper/node_modules/@prisma/debug": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-eUjoNThlDXdyJ1iQ2d7U6aTVwm59EwvODb5zFVNJEokNoSiQmiYWNzZIwZyDmZ+j51j42/0iTaHIJ4/aZPKFRg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/@prisma/get-platform": {
|
"node_modules/@prisma/get-platform": {
|
||||||
"version": "6.3.1",
|
"version": "6.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.3.1.tgz",
|
||||||
|
@ -2901,6 +2920,32 @@
|
||||||
"tslib": "^2.8.0"
|
"tslib": "^2.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tabler/icons": {
|
||||||
|
"version": "3.30.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.30.0.tgz",
|
||||||
|
"integrity": "sha512-c8OKLM48l00u9TFbh2qhSODMONIzML8ajtCyq95rW8vzkWcBrKRPM61tdkThz2j4kd5u17srPGIjqdeRUZdfdw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/codecalm"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tabler/icons-react": {
|
||||||
|
"version": "3.30.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.30.0.tgz",
|
||||||
|
"integrity": "sha512-9KZ9D1UNAyjlLkkYp2HBPHdf6lAJ2aelDqh8YYAnnmLF3xwprWKxxW8+zw5jlI0IwdfN4XFFuzqePkaw+DpIOg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tabler/icons": "3.30.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/codecalm"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">= 16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tsconfig/node10": {
|
"node_modules/@tsconfig/node10": {
|
||||||
"version": "1.0.11",
|
"version": "1.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
|
||||||
|
@ -5334,6 +5379,30 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prisma-json-types-generator": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/prisma-json-types-generator/-/prisma-json-types-generator-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-kvEbJPIP5gxk65KmLs0nAvY+CxpqVMWb4OsEvXlyXZmp2IGfi5f52BUV7ezTYQNjRPZyR4QlayWJXffoqVVAfA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@prisma/generator-helper": "6.0.0",
|
||||||
|
"tslib": "2.8.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"prisma-json-types-generator": "index.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/arthurfiorette/prisma-json-types-generator?sponsor=1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"prisma": "^5 || ^6",
|
||||||
|
"typescript": "^5.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/prismjs": {
|
"node_modules/prismjs": {
|
||||||
"version": "1.29.0",
|
"version": "1.29.0",
|
||||||
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz",
|
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz",
|
||||||
|
|
|
@ -3,7 +3,11 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start"
|
"start": "next start",
|
||||||
|
"db:seed": "npx prisma db seed"
|
||||||
|
},
|
||||||
|
"prisma": {
|
||||||
|
"seed": "ts-node prisma/seed.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^4.0.0",
|
"@hookform/resolvers": "^4.0.0",
|
||||||
|
@ -23,6 +27,7 @@
|
||||||
"@react-email/components": "0.0.33",
|
"@react-email/components": "0.0.33",
|
||||||
"@supabase/ssr": "latest",
|
"@supabase/ssr": "latest",
|
||||||
"@supabase/supabase-js": "latest",
|
"@supabase/supabase-js": "latest",
|
||||||
|
"@tabler/icons-react": "^3.30.0",
|
||||||
"autoprefixer": "10.4.20",
|
"autoprefixer": "10.4.20",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
@ -43,6 +48,7 @@
|
||||||
"@types/react-dom": "19.0.2",
|
"@types/react-dom": "19.0.2",
|
||||||
"postcss": "8.4.49",
|
"postcss": "8.4.49",
|
||||||
"prisma": "^6.3.1",
|
"prisma": "^6.3.1",
|
||||||
|
"prisma-json-types-generator": "^3.2.2",
|
||||||
"react-email": "3.0.7",
|
"react-email": "3.0.7",
|
||||||
"supabase": "^2.12.1",
|
"supabase": "^2.12.1",
|
||||||
"tailwind-merge": "^2.6.0",
|
"tailwind-merge": "^2.6.0",
|
||||||
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `createdAt` on the `contact_messages` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `updatedAt` on the `contact_messages` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `birthDate` on the `profiles` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `userId` on the `profiles` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `createdAt` on the `users` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `emailVerified` on the `users` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `firstName` on the `users` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `lastName` on the `users` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `lastSignedIn` on the `users` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `updatedAt` on the `users` table. All the data in the column will be lost.
|
||||||
|
- A unique constraint covering the columns `[user_id]` on the table `profiles` will be added. If there are existing duplicate values, this will fail.
|
||||||
|
- Added the required column `user_id` to the `profiles` table without a default value. This is not possible if the table is not empty.
|
||||||
|
- Added the required column `updated_at` to the `users` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- CreateExtension
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "profiles" DROP CONSTRAINT "profiles_userId_fkey";
|
||||||
|
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "profiles_userId_key";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "contact_messages" DROP COLUMN "createdAt",
|
||||||
|
DROP COLUMN "updatedAt",
|
||||||
|
ADD COLUMN "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT now(),
|
||||||
|
ADD COLUMN "updated_at" TIMESTAMPTZ(6) NOT NULL DEFAULT now(),
|
||||||
|
ALTER COLUMN "id" SET DEFAULT gen_random_uuid();
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "profiles" DROP COLUMN "birthDate",
|
||||||
|
DROP COLUMN "userId",
|
||||||
|
ADD COLUMN "birth_date" TIMESTAMP(3),
|
||||||
|
ADD COLUMN "user_id" TEXT NOT NULL,
|
||||||
|
ALTER COLUMN "id" SET DEFAULT gen_random_uuid();
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "users" DROP COLUMN "createdAt",
|
||||||
|
DROP COLUMN "emailVerified",
|
||||||
|
DROP COLUMN "firstName",
|
||||||
|
DROP COLUMN "lastName",
|
||||||
|
DROP COLUMN "lastSignedIn",
|
||||||
|
DROP COLUMN "updatedAt",
|
||||||
|
ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
ADD COLUMN "email_verified" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
ADD COLUMN "first_name" TEXT,
|
||||||
|
ADD COLUMN "last_name" TEXT,
|
||||||
|
ADD COLUMN "last_signed_in" TIMESTAMP(3),
|
||||||
|
ADD COLUMN "updated_at" TIMESTAMP(3) NOT NULL;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "nav_items" (
|
||||||
|
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
"title" VARCHAR(255) NOT NULL,
|
||||||
|
"url" VARCHAR(255) NOT NULL,
|
||||||
|
"icon" VARCHAR(100) NOT NULL,
|
||||||
|
"is_active" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"order_seq" INTEGER NOT NULL,
|
||||||
|
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT now(),
|
||||||
|
"updated_at" TIMESTAMPTZ(6) NOT NULL DEFAULT now(),
|
||||||
|
"created_by" UUID,
|
||||||
|
"updated_by" UUID,
|
||||||
|
|
||||||
|
CONSTRAINT "nav_items_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "sub_items" (
|
||||||
|
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
"title" VARCHAR(255) NOT NULL,
|
||||||
|
"url" VARCHAR(255) NOT NULL,
|
||||||
|
"order_seq" INTEGER NOT NULL,
|
||||||
|
"nav_item_id" UUID NOT NULL,
|
||||||
|
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT now(),
|
||||||
|
"updated_at" TIMESTAMPTZ(6) NOT NULL DEFAULT now(),
|
||||||
|
"created_by" UUID,
|
||||||
|
"updated_by" UUID,
|
||||||
|
|
||||||
|
CONSTRAINT "sub_items_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "nav_items_title_idx" ON "nav_items"("title");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "nav_items_is_active_idx" ON "nav_items"("is_active");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "sub_items_nav_item_id_idx" ON "sub_items"("nav_item_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "sub_items_title_idx" ON "sub_items"("title");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "profiles_user_id_key" ON "profiles"("user_id");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "profiles" ADD CONSTRAINT "profiles_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "sub_items" ADD CONSTRAINT "sub_items_nav_item_id_fkey" FOREIGN KEY ("nav_item_id") REFERENCES "nav_items"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
@ -0,0 +1,108 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- The primary key for the `contact_messages` table will be changed. If it partially fails, the table could be left without primary key constraint.
|
||||||
|
- The `id` column on the `contact_messages` table would be dropped and recreated. This will lead to data loss if there is data in the column.
|
||||||
|
- You are about to alter the column `name` on the `contact_messages` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(255)`.
|
||||||
|
- You are about to alter the column `email` on the `contact_messages` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(255)`.
|
||||||
|
- You are about to alter the column `phone` on the `contact_messages` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(20)`.
|
||||||
|
- You are about to alter the column `message_type` on the `contact_messages` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(50)`.
|
||||||
|
- You are about to alter the column `message_type_label` on the `contact_messages` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(50)`.
|
||||||
|
- The primary key for the `profiles` table will be changed. If it partially fails, the table could be left without primary key constraint.
|
||||||
|
- The `id` column on the `profiles` table would be dropped and recreated. This will lead to data loss if there is data in the column.
|
||||||
|
- You are about to alter the column `phone` on the `profiles` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(20)`.
|
||||||
|
- You are about to alter the column `address` on the `profiles` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(255)`.
|
||||||
|
- You are about to alter the column `city` on the `profiles` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(100)`.
|
||||||
|
- You are about to alter the column `country` on the `profiles` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(100)`.
|
||||||
|
- The primary key for the `users` table will be changed. If it partially fails, the table could be left without primary key constraint.
|
||||||
|
- You are about to alter the column `email` on the `users` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(255)`.
|
||||||
|
- You are about to alter the column `password` on the `users` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(255)`.
|
||||||
|
- You are about to alter the column `avatar` on the `users` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(255)`.
|
||||||
|
- You are about to alter the column `first_name` on the `users` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(255)`.
|
||||||
|
- You are about to alter the column `last_name` on the `users` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(255)`.
|
||||||
|
- You are about to drop the `sub_items` table. If the table is not empty, all the data it contains will be lost.
|
||||||
|
- Changed the type of `user_id` on the `profiles` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
|
||||||
|
- Changed the type of `id` on the `users` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "profiles" DROP CONSTRAINT "profiles_user_id_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "sub_items" DROP CONSTRAINT "sub_items_nav_item_id_fkey";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "contact_messages" DROP CONSTRAINT "contact_messages_pkey",
|
||||||
|
DROP COLUMN "id",
|
||||||
|
ADD COLUMN "id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
ALTER COLUMN "name" SET DATA TYPE VARCHAR(255),
|
||||||
|
ALTER COLUMN "email" SET DATA TYPE VARCHAR(255),
|
||||||
|
ALTER COLUMN "phone" SET DATA TYPE VARCHAR(20),
|
||||||
|
ALTER COLUMN "message_type" SET DATA TYPE VARCHAR(50),
|
||||||
|
ALTER COLUMN "message_type_label" SET DATA TYPE VARCHAR(50),
|
||||||
|
ALTER COLUMN "created_at" SET DEFAULT now(),
|
||||||
|
ALTER COLUMN "updated_at" SET DEFAULT now(),
|
||||||
|
ADD CONSTRAINT "contact_messages_pkey" PRIMARY KEY ("id");
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "nav_items" ALTER COLUMN "created_at" SET DEFAULT now(),
|
||||||
|
ALTER COLUMN "updated_at" SET DEFAULT now();
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "profiles" DROP CONSTRAINT "profiles_pkey",
|
||||||
|
DROP COLUMN "id",
|
||||||
|
ADD COLUMN "id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
ALTER COLUMN "phone" SET DATA TYPE VARCHAR(20),
|
||||||
|
ALTER COLUMN "address" SET DATA TYPE VARCHAR(255),
|
||||||
|
ALTER COLUMN "city" SET DATA TYPE VARCHAR(100),
|
||||||
|
ALTER COLUMN "country" SET DATA TYPE VARCHAR(100),
|
||||||
|
DROP COLUMN "user_id",
|
||||||
|
ADD COLUMN "user_id" UUID NOT NULL,
|
||||||
|
ADD CONSTRAINT "profiles_pkey" PRIMARY KEY ("id");
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "users" DROP CONSTRAINT "users_pkey",
|
||||||
|
DROP COLUMN "id",
|
||||||
|
ADD COLUMN "id" UUID NOT NULL,
|
||||||
|
ALTER COLUMN "email" SET DATA TYPE VARCHAR(255),
|
||||||
|
ALTER COLUMN "password" SET DATA TYPE VARCHAR(255),
|
||||||
|
ALTER COLUMN "avatar" SET DATA TYPE VARCHAR(255),
|
||||||
|
ALTER COLUMN "first_name" SET DATA TYPE VARCHAR(255),
|
||||||
|
ALTER COLUMN "last_name" SET DATA TYPE VARCHAR(255),
|
||||||
|
ADD CONSTRAINT "users_pkey" PRIMARY KEY ("id");
|
||||||
|
|
||||||
|
-- DropTable
|
||||||
|
DROP TABLE "sub_items";
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "nav_sub_items" (
|
||||||
|
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
"title" VARCHAR(255) NOT NULL,
|
||||||
|
"url" VARCHAR(255) NOT NULL,
|
||||||
|
"order_seq" INTEGER NOT NULL,
|
||||||
|
"nav_item_id" UUID NOT NULL,
|
||||||
|
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT now(),
|
||||||
|
"updated_at" TIMESTAMPTZ(6) NOT NULL DEFAULT now(),
|
||||||
|
"created_by" UUID,
|
||||||
|
"updated_by" UUID,
|
||||||
|
|
||||||
|
CONSTRAINT "nav_sub_items_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "nav_sub_items_nav_item_id_idx" ON "nav_sub_items"("nav_item_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "nav_sub_items_title_idx" ON "nav_sub_items"("title");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "profiles_user_id_key" ON "profiles"("user_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "users_role_idx" ON "users"("role");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "profiles" ADD CONSTRAINT "profiles_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "nav_sub_items" ADD CONSTRAINT "nav_sub_items_nav_item_id_fkey" FOREIGN KEY ("nav_item_id") REFERENCES "nav_items"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
@ -5,64 +5,106 @@
|
||||||
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
|
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
|
||||||
|
|
||||||
generator client {
|
generator client {
|
||||||
provider = "prisma-client-js"
|
provider = "prisma-client-js"
|
||||||
|
previewFeatures = ["postgresqlExtensions"]
|
||||||
}
|
}
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "postgresql"
|
provider = "postgresql"
|
||||||
url = env("DATABASE_URL")
|
url = env("DATABASE_URL")
|
||||||
directUrl = env("DIRECT_URL")
|
directUrl = env("DIRECT_URL")
|
||||||
|
extensions = [pgcrypto]
|
||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id String @id
|
id String @id @db.Uuid
|
||||||
email String @unique
|
email String @unique @db.VarChar(255)
|
||||||
emailVerified Boolean @default(false)
|
email_verified Boolean @default(false)
|
||||||
password String?
|
password String? @db.VarChar(255)
|
||||||
firstName String?
|
first_name String? @db.VarChar(255)
|
||||||
lastName String?
|
last_name String? @db.VarChar(255)
|
||||||
avatar String?
|
avatar String? @db.VarChar(255)
|
||||||
role Role @default(user)
|
role Role @default(user)
|
||||||
createdAt DateTime @default(now())
|
created_at DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updated_at DateTime @updatedAt
|
||||||
lastSignedIn DateTime?
|
last_signed_in DateTime?
|
||||||
metadata Json?
|
metadata Json?
|
||||||
|
|
||||||
// Relations (optional examples)
|
|
||||||
profile Profile?
|
profile Profile?
|
||||||
|
|
||||||
@@map("users") // Maps to Supabase's 'users' table
|
@@index([role])
|
||||||
|
@@map("users")
|
||||||
}
|
}
|
||||||
|
|
||||||
model Profile {
|
model Profile {
|
||||||
id String @id @default(uuid())
|
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||||
userId String @unique
|
user_id String @unique @db.Uuid
|
||||||
bio String?
|
bio String? @db.Text
|
||||||
phone String?
|
phone String? @db.VarChar(20)
|
||||||
address String?
|
address String? @db.VarChar(255)
|
||||||
city String?
|
city String? @db.VarChar(100)
|
||||||
country String?
|
country String? @db.VarChar(100)
|
||||||
birthDate DateTime?
|
birth_date DateTime?
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [user_id], references: [id])
|
||||||
|
|
||||||
|
@@index([user_id])
|
||||||
@@map("profiles") // Maps to Supabase's 'profiles' table
|
@@map("profiles") // Maps to Supabase's 'profiles' table
|
||||||
}
|
}
|
||||||
|
|
||||||
model ContactMessages {
|
model ContactMessages {
|
||||||
id String @id @default(dbgenerated("gen_random_uuid()"))
|
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||||
name String?
|
name String? @db.VarChar(255)
|
||||||
email String?
|
email String? @db.VarChar(255)
|
||||||
phone String?
|
phone String? @db.VarChar(20)
|
||||||
message_type String?
|
message_type String? @db.VarChar(50)
|
||||||
message_type_label String?
|
message_type_label String? @db.VarChar(50)
|
||||||
message String?
|
message String? @db.Text
|
||||||
status StatusContactMessages @default(new)
|
status StatusContactMessages @default(new)
|
||||||
createdAt DateTime @default(dbgenerated("now()")) @db.Timestamptz(6)
|
created_at DateTime @default(dbgenerated("now()")) @db.Timestamptz(6)
|
||||||
updatedAt DateTime @default(dbgenerated("now()")) @updatedAt @db.Timestamptz(6)
|
updated_at DateTime @default(dbgenerated("now()")) @updatedAt @db.Timestamptz(6)
|
||||||
|
|
||||||
@@map("contact_messages") // Maps to Supabase's 'contact_messages' table
|
@@map("contact_messages") // Maps to Supabase's 'contact_messages' table
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model NavItems {
|
||||||
|
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||||
|
title String @db.VarChar(255)
|
||||||
|
url String @db.VarChar(255)
|
||||||
|
slug String @db.VarChar(255)
|
||||||
|
icon String @db.VarChar(100)
|
||||||
|
is_active Boolean @default(false)
|
||||||
|
order_seq Int
|
||||||
|
created_at DateTime @default(dbgenerated("now()")) @db.Timestamptz(6)
|
||||||
|
updated_at DateTime @default(dbgenerated("now()")) @updatedAt @db.Timestamptz(6)
|
||||||
|
sub_items NavSubItems[]
|
||||||
|
created_by String? @db.Uuid
|
||||||
|
updated_by String? @db.Uuid
|
||||||
|
|
||||||
|
@@index([title])
|
||||||
|
@@index([is_active])
|
||||||
|
@@map("nav_items")
|
||||||
|
}
|
||||||
|
|
||||||
|
model NavSubItems {
|
||||||
|
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||||
|
title String @db.VarChar(255)
|
||||||
|
url String @db.VarChar(255)
|
||||||
|
slug String @db.VarChar(255)
|
||||||
|
icon String @db.VarChar(100)
|
||||||
|
is_active Boolean @default(false)
|
||||||
|
order_seq Int
|
||||||
|
nav_item_id String @db.Uuid
|
||||||
|
created_at DateTime @default(dbgenerated("now()")) @db.Timestamptz(6)
|
||||||
|
updated_at DateTime @default(dbgenerated("now()")) @updatedAt @db.Timestamptz(6)
|
||||||
|
created_by String? @db.Uuid
|
||||||
|
updated_by String? @db.Uuid
|
||||||
|
nav_item NavItems @relation(fields: [nav_item_id], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@index([nav_item_id])
|
||||||
|
@@index([title])
|
||||||
|
@@map("nav_sub_items")
|
||||||
|
}
|
||||||
|
|
||||||
enum Role {
|
enum Role {
|
||||||
admin
|
admin
|
||||||
staff
|
staff
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
const { PrismaClient } = require("@prisma/client");
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
// Clear existing data
|
||||||
|
await prisma.navSubItems.deleteMany({});
|
||||||
|
await prisma.navItems.deleteMany({});
|
||||||
|
|
||||||
|
const navItemDatas = [
|
||||||
|
{
|
||||||
|
title: "Dashboard",
|
||||||
|
url: "/dashboard",
|
||||||
|
slug: "dashboard",
|
||||||
|
order_seq: 1,
|
||||||
|
icon: "LayoutDashboard",
|
||||||
|
sub_items: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Master",
|
||||||
|
url: "/master",
|
||||||
|
slug: "master",
|
||||||
|
order_seq: 2,
|
||||||
|
icon: "IconDashboard",
|
||||||
|
sub_items: [
|
||||||
|
{
|
||||||
|
title: "Users",
|
||||||
|
url: "/master/users",
|
||||||
|
slug: "users",
|
||||||
|
icon: "IconUsers ",
|
||||||
|
order_seq: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Map",
|
||||||
|
url: "/map",
|
||||||
|
slug: "map",
|
||||||
|
order_seq: 3,
|
||||||
|
icon: "Map",
|
||||||
|
sub_items: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Create nav items and their sub-items
|
||||||
|
for (const navItemData of navItemDatas) {
|
||||||
|
const { sub_items, ...navItemFields } = navItemData;
|
||||||
|
|
||||||
|
await prisma.navItems.create({
|
||||||
|
data: {
|
||||||
|
...navItemFields,
|
||||||
|
sub_items: {
|
||||||
|
create: sub_items,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Seed data created successfully", navItemDatas);
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
process.exit(1);
|
||||||
|
})
|
||||||
|
.finally(async () => {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
});
|
|
@ -0,0 +1,77 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const navSubItemsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
title: z.string(),
|
||||||
|
url: z.string(),
|
||||||
|
slug: z.string(),
|
||||||
|
icon: z.string(),
|
||||||
|
is_active: z.boolean().default(false),
|
||||||
|
order_seq: z.number().int(),
|
||||||
|
nav_item_id: z.string().uuid(),
|
||||||
|
created_at: z.date(),
|
||||||
|
updated_at: z.date(),
|
||||||
|
created_by: z.string().uuid().nullable(),
|
||||||
|
updated_by: z.string().uuid().nullable(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type NavSubItems = z.infer<typeof navSubItemsSchema>;
|
||||||
|
|
||||||
|
export const navItemsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
title: z.string(),
|
||||||
|
url: z.string(),
|
||||||
|
slug: z.string(),
|
||||||
|
icon: z.string(),
|
||||||
|
is_active: z.boolean().default(false),
|
||||||
|
order_seq: z.number().int(),
|
||||||
|
created_at: z.date(),
|
||||||
|
updated_at: z.date(),
|
||||||
|
created_by: z.string().uuid().nullable(),
|
||||||
|
updated_by: z.string().uuid().nullable(),
|
||||||
|
sub_items: z.array(navSubItemsSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type NavItems = z.infer<typeof navItemsSchema>;
|
||||||
|
|
||||||
|
export const navItemsGetSchema = z.array(navItemsSchema);
|
||||||
|
|
||||||
|
export type NavItemsGet = z.infer<typeof navItemsGetSchema>;
|
||||||
|
|
||||||
|
export const navItemsInsertSchema = navItemsSchema.pick({
|
||||||
|
title: true,
|
||||||
|
url: true,
|
||||||
|
slug: true,
|
||||||
|
icon: true,
|
||||||
|
is_active: true,
|
||||||
|
order_seq: true,
|
||||||
|
sub_items: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type NavItemsInsert = z.infer<typeof navItemsInsertSchema>;
|
||||||
|
|
||||||
|
export const navItemsUpdateSchema = navItemsSchema.pick({
|
||||||
|
title: true,
|
||||||
|
url: true,
|
||||||
|
slug: true,
|
||||||
|
icon: true,
|
||||||
|
is_active: true,
|
||||||
|
order_seq: true,
|
||||||
|
sub_items: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type NavItemsUpdate = z.infer<typeof navItemsUpdateSchema>;
|
||||||
|
|
||||||
|
export const navItemsDeleteSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type NavItemsDelete = z.infer<typeof navItemsDeleteSchema>;
|
||||||
|
|
||||||
|
export interface NavItemsResponse {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
error?: string;
|
||||||
|
errors?: Record<string, string>;
|
||||||
|
data?: NavItems | NavItemsGet;
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
import {
|
||||||
|
NavItemsDelete,
|
||||||
|
NavItemsGet,
|
||||||
|
NavItemsInsert,
|
||||||
|
NavItemsResponse,
|
||||||
|
NavItemsUpdate,
|
||||||
|
} from "../entities/models/nav-items.model";
|
||||||
|
|
||||||
|
export interface NavItemsRepository {
|
||||||
|
getNavItems(): Promise<NavItemsResponse>;
|
||||||
|
createNavItems(navItems: NavItemsInsert): Promise<NavItemsResponse>;
|
||||||
|
updateNavItems(
|
||||||
|
id: string,
|
||||||
|
navItems: NavItemsUpdate
|
||||||
|
): Promise<NavItemsResponse>;
|
||||||
|
deleteNavItems(id: string): Promise<NavItemsResponse>;
|
||||||
|
}
|
|
@ -2,8 +2,8 @@ import {
|
||||||
ContactUs,
|
ContactUs,
|
||||||
ContactUsInsert,
|
ContactUsInsert,
|
||||||
ContactUsResponse,
|
ContactUsResponse,
|
||||||
} from "../../entities/models/contact-us.model";
|
} from "../entities/models/contact-us.model";
|
||||||
import { ContactUsRepository } from "../../repositories/contact-us.repository";
|
import { ContactUsRepository } from "../repositories/contact-us.repository";
|
||||||
|
|
||||||
export class CreateContactUseCase {
|
export class CreateContactUseCase {
|
||||||
constructor(private contactRepository: ContactUsRepository) {}
|
constructor(private contactRepository: ContactUsRepository) {}
|
|
@ -0,0 +1,33 @@
|
||||||
|
import {
|
||||||
|
NavItemsDelete,
|
||||||
|
NavItemsGet,
|
||||||
|
NavItemsInsert,
|
||||||
|
NavItemsResponse,
|
||||||
|
NavItemsUpdate,
|
||||||
|
} from "../entities/models/nav-items.model";
|
||||||
|
import { NavItemsRepository } from "../repositories/nav-items.repository";
|
||||||
|
|
||||||
|
export class NavItemsUseCase {
|
||||||
|
constructor(private navItemsRepository: NavItemsRepository) {}
|
||||||
|
|
||||||
|
async executeGetNavItems(): Promise<NavItemsResponse> {
|
||||||
|
return this.navItemsRepository.getNavItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
async executeCreateNavItems(
|
||||||
|
navItems: NavItemsInsert
|
||||||
|
): Promise<NavItemsResponse> {
|
||||||
|
return this.navItemsRepository.createNavItems(navItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
async executeUpdateNavItems(
|
||||||
|
id: string,
|
||||||
|
navItems: NavItemsUpdate
|
||||||
|
): Promise<NavItemsResponse> {
|
||||||
|
return this.navItemsRepository.updateNavItems(id, navItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
async executeDeleteNavItems(id: string): Promise<NavItemsResponse> {
|
||||||
|
return this.navItemsRepository.deleteNavItems(id);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import { SignInResponse, SignInWithOtp } from "../../entities/models/user.model";
|
import { SignInResponse, SignInWithOtp } from "../entities/models/user.model";
|
||||||
import { SignInRepository } from "../../repositories/signin.repository";
|
import { SignInRepository } from "../repositories/signin.repository";
|
||||||
|
|
||||||
export class SignInUseCase {
|
export class SignInUseCase {
|
||||||
constructor(private signInRepository: SignInRepository) {}
|
constructor(private signInRepository: SignInRepository) {}
|
|
@ -0,0 +1 @@
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
import db from "@/lib/db";
|
||||||
|
import {
|
||||||
|
NavItemsDelete,
|
||||||
|
NavItemsGet,
|
||||||
|
NavItemsInsert,
|
||||||
|
NavItemsResponse,
|
||||||
|
navItemsSchema,
|
||||||
|
NavItemsUpdate,
|
||||||
|
} from "@/src/applications/entities/models/nav-items.model";
|
||||||
|
import { NavItemsRepository } from "@/src/applications/repositories/nav-items.repository";
|
||||||
|
|
||||||
|
export class NavItemsRepositoryImpl implements NavItemsRepository {
|
||||||
|
async getNavItems(): Promise<NavItemsResponse> {
|
||||||
|
const data = await db.navItems.findMany({
|
||||||
|
where: {
|
||||||
|
is_active: true,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
sub_items: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async createNavItems(navItems: NavItemsInsert): Promise<NavItemsResponse> {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateNavItems(
|
||||||
|
id: string,
|
||||||
|
navItems: NavItemsUpdate
|
||||||
|
): Promise<NavItemsResponse> {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteNavItems(id: string): Promise<NavItemsResponse> {
|
||||||
|
await db.navItems.delete({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Navigation item deleted",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue