fecth dynamic navitem

This commit is contained in:
vergiLgood1 2025-02-20 23:50:45 +07:00
parent 7c58a7e46c
commit bc0c6438a2
19 changed files with 1058 additions and 171 deletions

View File

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

View File

@ -16,37 +16,34 @@ import {
export default function Page() {
return (
<SidebarProvider>
<AppSidebar />
<SidebarInset>
<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">
<div className="flex items-center gap-2 px-4">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 h-4" />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem className="hidden md:block">
<BreadcrumbLink href="#">
Building Your Application
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem>
<BreadcrumbPage>Data Fetching</BreadcrumbPage>
</BreadcrumbItem>
</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" />
<>
<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">
<div className="flex items-center gap-2 px-4">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 h-4" />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem className="hidden md:block">
<BreadcrumbLink href="#">
Building Your Application
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem>
<BreadcrumbPage>Data Fetching</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
</SidebarInset>
</SidebarProvider>
</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>
</>
);
}

View File

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

View File

@ -1,33 +1,36 @@
"use client"
"use client";
import * as React from "react"
import * as React from "react";
import { useEffect, useState } from "react";
import {
AudioWaveform,
BookOpen,
Bot,
Command,
Frame,
GalleryVerticalEnd,
Map,
PieChart,
Settings2,
Loader2,
SquareTerminal,
} from "lucide-react"
} 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 { NavMain } from "@/components/nav-main";
import { NavProjects } from "@/components/nav-projects";
import { NavUser } from "@/components/nav-user";
import { TeamSwitcher } from "@/components/team-switcher";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
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.
const data = {
// 🔧 Konstanta untuk localStorage key
const NAV_ITEMS_STORAGE_KEY = "navItemsData";
// 🎨 Data fallback
const fallbackData = {
user: {
name: "shadcn",
email: "m@example.com",
@ -71,71 +74,7 @@ const data = {
},
],
},
{
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: "#",
},
],
},
// additional items...
],
projects: [
{
@ -143,33 +82,109 @@ const data = {
url: "#",
icon: Frame,
},
{
name: "Sales & Marketing",
url: "#",
icon: PieChart,
},
{
name: "Travel",
url: "#",
icon: Map,
},
// 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: 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 (
<Sidebar collapsible="icon" {...props}>
<SidebarHeader>
<TeamSwitcher teams={data.teams} />
<TeamSwitcher teams={fallbackData.teams} />
</SidebarHeader>
<SidebarContent>
<NavMain items={data.navMain} />
<NavProjects projects={data.projects} />
{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} />
</SidebarContent>
<SidebarFooter>
<NavUser user={data.user} />
<NavUser user={fallbackData.user} />
</SidebarFooter>
<SidebarRail />
</Sidebar>
)
);
}

View File

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

View File

@ -8,8 +8,8 @@ declare const globalThis: {
prismaGlobal: ReturnType<typeof prismaClientSingleton>;
} & 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;

View File

@ -26,6 +26,7 @@
"@react-email/components": "0.0.33",
"@supabase/ssr": "latest",
"@supabase/supabase-js": "latest",
"@tabler/icons-react": "^3.30.0",
"autoprefixer": "10.4.20",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@ -46,6 +47,7 @@
"@types/react-dom": "19.0.2",
"postcss": "8.4.49",
"prisma": "^6.3.1",
"prisma-json-types-generator": "^3.2.2",
"react-email": "3.0.7",
"supabase": "^2.12.1",
"tailwind-merge": "^2.6.0",
@ -1590,6 +1592,23 @@
"@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": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.3.1.tgz",
@ -2901,6 +2920,32 @@
"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": {
"version": "1.0.11",
"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": {
"version": "1.29.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz",

View File

@ -3,7 +3,11 @@
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
"start": "next start",
"db:seed": "npx prisma db seed"
},
"prisma": {
"seed": "ts-node prisma/seed.ts"
},
"dependencies": {
"@hookform/resolvers": "^4.0.0",
@ -23,6 +27,7 @@
"@react-email/components": "0.0.33",
"@supabase/ssr": "latest",
"@supabase/supabase-js": "latest",
"@tabler/icons-react": "^3.30.0",
"autoprefixer": "10.4.20",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@ -43,6 +48,7 @@
"@types/react-dom": "19.0.2",
"postcss": "8.4.49",
"prisma": "^6.3.1",
"prisma-json-types-generator": "^3.2.2",
"react-email": "3.0.7",
"supabase": "^2.12.1",
"tailwind-merge": "^2.6.0",

View File

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

View File

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

View File

@ -5,64 +5,106 @@
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
generator client {
provider = "prisma-client-js"
provider = "prisma-client-js"
previewFeatures = ["postgresqlExtensions"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_URL")
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_URL")
extensions = [pgcrypto]
}
model User {
id String @id
email String @unique
emailVerified Boolean @default(false)
password String?
firstName String?
lastName String?
avatar String?
role Role @default(user)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
lastSignedIn DateTime?
metadata Json?
id String @id @db.Uuid
email String @unique @db.VarChar(255)
email_verified Boolean @default(false)
password String? @db.VarChar(255)
first_name String? @db.VarChar(255)
last_name String? @db.VarChar(255)
avatar String? @db.VarChar(255)
role Role @default(user)
created_at DateTime @default(now())
updated_at DateTime @updatedAt
last_signed_in DateTime?
metadata Json?
// Relations (optional examples)
profile Profile?
@@map("users") // Maps to Supabase's 'users' table
@@index([role])
@@map("users")
}
model Profile {
id String @id @default(uuid())
userId String @unique
bio String?
phone String?
address String?
city String?
country String?
birthDate DateTime?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
user_id String @unique @db.Uuid
bio String? @db.Text
phone String? @db.VarChar(20)
address String? @db.VarChar(255)
city String? @db.VarChar(100)
country String? @db.VarChar(100)
birth_date DateTime?
user User @relation(fields: [user_id], references: [id])
@@index([user_id])
@@map("profiles") // Maps to Supabase's 'profiles' table
}
model ContactMessages {
id String @id @default(dbgenerated("gen_random_uuid()"))
name String?
email String?
phone String?
message_type String?
message_type_label String?
message String?
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
name String? @db.VarChar(255)
email String? @db.VarChar(255)
phone String? @db.VarChar(20)
message_type String? @db.VarChar(50)
message_type_label String? @db.VarChar(50)
message String? @db.Text
status StatusContactMessages @default(new)
createdAt DateTime @default(dbgenerated("now()")) @db.Timestamptz(6)
updatedAt DateTime @default(dbgenerated("now()")) @updatedAt @db.Timestamptz(6)
created_at DateTime @default(dbgenerated("now()")) @db.Timestamptz(6)
updated_at DateTime @default(dbgenerated("now()")) @updatedAt @db.Timestamptz(6)
@@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 {
admin
staff

View File

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

View File

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

View File

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

View File

@ -2,8 +2,8 @@ import {
ContactUs,
ContactUsInsert,
ContactUsResponse,
} from "../../entities/models/contact-us.model";
import { ContactUsRepository } from "../../repositories/contact-us.repository";
} from "../entities/models/contact-us.model";
import { ContactUsRepository } from "../repositories/contact-us.repository";
export class CreateContactUseCase {
constructor(private contactRepository: ContactUsRepository) {}

View File

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

View File

@ -1,5 +1,5 @@
import { SignInResponse, SignInWithOtp } from "../../entities/models/user.model";
import { SignInRepository } from "../../repositories/signin.repository";
import { SignInResponse, SignInWithOtp } from "../entities/models/user.model";
import { SignInRepository } from "../repositories/signin.repository";
export class SignInUseCase {
constructor(private signInRepository: SignInRepository) {}

View File

@ -0,0 +1 @@

View File

@ -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",
};
}
}