diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e93d4996..1cd3f723 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -92,6 +92,8 @@ model FileStorage { Pegawai Pegawai[] DesaDigital DesaDigital[] + + KolaborasiInovasi KolaborasiInovasi[] } //========================================= MENU PPID ========================================= // @@ -1341,15 +1343,32 @@ model DesaDigital { deletedAt DateTime @default(now()) isActive Boolean @default(true) } + // ========================================= PROGRAM KREATIF ========================================= // model ProgramKreatif { - id String @id @default(cuid()) + id String @id @default(cuid()) name String - slug String @db.Text //deskripsi singkat - deskripsi String @db.Text //deskripsi panjang + slug String @db.Text //deskripsi singkat + deskripsi String @db.Text //deskripsi panjang icon String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) - isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime @default(now()) + isActive Boolean @default(true) +} + +// ========================================= KOLABORASI INOVASI ========================================= // +model KolaborasiInovasi { + id String @id @default(cuid()) + name String + tahun Int + slug String @db.Text //deskripsi singkat + deskripsi String @db.Text //deskripsi panjang + kolaborator String + image FileStorage @relation(fields: [imageId], references: [id]) + imageId String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime @default(now()) + isActive Boolean @default(true) } diff --git a/src/app/admin/(dashboard)/_state/inovasi/kolaborasi-inovasi.ts b/src/app/admin/(dashboard)/_state/inovasi/kolaborasi-inovasi.ts new file mode 100644 index 00000000..4f82d04d --- /dev/null +++ b/src/app/admin/(dashboard)/_state/inovasi/kolaborasi-inovasi.ts @@ -0,0 +1,234 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import ApiFetch from "@/lib/api-fetch"; +import { Prisma } from "@prisma/client"; +import { toast } from "react-toastify"; +import { proxy } from "valtio"; +import { z } from "zod"; + +const templateForm = z.object({ + name: z.string().min(1, "Nama minimal 1 karakter"), + tahun: z.number().min(4, "Tahun minimal 4 karakter"), + slug: z.string().min(1, "Deskripsi singkat minimal 1 karakter"), + deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"), + kolaborator: z.string().min(1, "Kolaborator minimal 1 karakter"), + imageId: z.string().min(1, "Image ID minimal 1 karakter"), +}) + +const defaultForm = { + name: "", + tahun: 0, + slug: "", + deskripsi: "", + kolaborator: "", + imageId: "", +} + +const kolaborasiInovasiState = proxy({ + create: { + form: { ...defaultForm }, + loading: false, + async create() { + const cek = templateForm.safeParse(kolaborasiInovasiState.create.form); + if (!cek.success) { + const err = `[${cek.error.issues + .map((v) => `${v.path.join(".")}`) + .join("\n")}] required`; + return toast.error(err); + } + + try { + kolaborasiInovasiState.create.loading = true; + const res = await ApiFetch.api.inovasi.kolaborasiinovasi["create"].post( + kolaborasiInovasiState.create.form + ); + if (res.status === 200) { + kolaborasiInovasiState.findMany.load(); + return toast.success("success create"); + } + console.log(res); + return toast.error("failed create"); + } catch (error) { + console.log((error as Error).message); + } finally { + kolaborasiInovasiState.create.loading = false; + } + }, + }, + findMany: { + data: null as any[] | null, + page: 1, + totalPages: 1, + total: 0, + loading: false, + load: async (page = 1, limit = 10) => { + // Change to arrow function + kolaborasiInovasiState.findMany.loading = true; // Use the full path to access the property + kolaborasiInovasiState.findMany.page = page; + try { + const res = await ApiFetch.api.inovasi.kolaborasiinovasi["find-many"].get({ + query: { page, limit }, + }); + + if (res.status === 200 && res.data?.success) { + kolaborasiInovasiState.findMany.data = res.data.data || []; + kolaborasiInovasiState.findMany.total = res.data.total || 0; + kolaborasiInovasiState.findMany.totalPages = res.data.totalPages || 1; + } else { + console.error( + "Failed to load grafik berdasarkan jenis kelamin:", + res.data?.message + ); + kolaborasiInovasiState.findMany.data = []; + kolaborasiInovasiState.findMany.total = 0; + kolaborasiInovasiState.findMany.totalPages = 1; + } + } catch (error) { + console.error("Error loading grafik berdasarkan jenis kelamin:", error); + kolaborasiInovasiState.findMany.data = []; + kolaborasiInovasiState.findMany.total = 0; + kolaborasiInovasiState.findMany.totalPages = 1; + } finally { + kolaborasiInovasiState.findMany.loading = false; + } + }, + }, + update: { + id: "", + form: { ...defaultForm }, + loading: false, + async load(id: string) { + if (!id) { + toast.warn("ID tidak valid"); + return null; + } + + try { + const response = await fetch(`/api/inovasi/kolaborasiinovasi/${id}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const result = await response.json(); + + if (result?.success) { + const data = result.data; + this.id = data.id; + this.form = { + name: data.name, + tahun: data.tahun, + slug: data.slug, + deskripsi: data.deskripsi, + kolaborator: data.kolaborator, + imageId: data.imageId, + }; + return data; + } else { + throw new Error(result?.message || "Gagal mengambil data"); + } + } catch (error) { + console.error("Error loading kolaborasi inovasi:", error); + toast.error( + error instanceof Error ? error.message : "Gagal memuat data" + ); + return null; + } + }, + + async submit() { + const id = this.id; + if (!id) { + toast.warn("ID tidak valid"); + return null; + } + const cek = templateForm.safeParse(this.form); + if (!cek.success) { + const err = `[${cek.error.issues + .map((v) => `${v.path.join(".")}`) + .join("\n")}] required`; + toast.error(err); + return null; + } + this.loading = true; + try { + const response = await fetch(`/api/inovasi/kolaborasiinovasi/${id}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(this.form), + }); + const result = await response.json(); + if (!response.ok || !result?.success) { + throw new Error(result?.message || "Gagal update data"); + } + toast.success("Berhasil update data!"); + await kolaborasiInovasiState.findMany.load(); + return result.data; + } catch (error) { + console.error("Error update data:", error); + toast.error("Gagal update data kolaborasi inovasi"); + } finally { + this.loading = false; + } + }, + }, + findUnique: { + data: null as Prisma.KolaborasiInovasiGetPayload<{ + omit: { isActive: true }; + }> | null, + async load(id: string) { + try { + const res = await fetch(`/api/inovasi/kolaborasiinovasi/${id}`); + if (res.ok) { + const data = await res.json(); + kolaborasiInovasiState.findUnique.data = data.data ?? null; + } else { + console.error("Failed to fetch data", res.status, res.statusText); + kolaborasiInovasiState.findUnique.data = null; + } + } catch (error) { + console.error("Error loading kolaborasi inovasi:", error); + kolaborasiInovasiState.findUnique.data = null; + } + }, + }, + delete: { + loading: false, + async byId(id: string) { + if (!id) return toast.warn("ID tidak valid"); + + try { + kolaborasiInovasiState.delete.loading = true; + + const response = await fetch(`/api/inovasi/kolaborasiinovasi/del/${id}`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + }); + + const result = await response.json(); + + if (response.ok && result?.success) { + toast.success(result.message || "Kolaborasi inovasi berhasil dihapus"); + await kolaborasiInovasiState.findMany.load(); // refresh list + } else { + toast.error(result?.message || "Gagal menghapus kolaborasi inovasi"); + } + } catch (error) { + console.error("Gagal delete:", error); + toast.error("Terjadi kesalahan saat menghapus kolaborasi inovasi"); + } finally { + kolaborasiInovasiState.delete.loading = false; + } + }, + }, +}); + +export default kolaborasiInovasiState; + \ No newline at end of file diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/edit/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/[id]/edit/page.tsx similarity index 100% rename from src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/edit/page.tsx rename to src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/[id]/edit/page.tsx diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/detail/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/[id]/page.tsx similarity index 100% rename from src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/detail/page.tsx rename to src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/[id]/page.tsx diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/page.tsx index 7344cdb1..ea59d136 100644 --- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/page.tsx @@ -1,26 +1,84 @@ +/* eslint-disable react-hooks/exhaustive-deps */ 'use client' import colors from '@/con/colors'; -import { Box, Button, Paper, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core'; +import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; import HeaderSearch from '../../_com/header'; import JudulList from '../../_com/judulList'; import { useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import kolaborasiInovasiState from '../../_state/inovasi/kolaborasi-inovasi'; +import { useProxy } from 'valtio/utils'; function KolaborasiInovasi() { + const [search, setSearch] = useState(''); return ( } + value={search} + onChange={(e) => setSearch(e.currentTarget.value)} /> - + ); } -function ListKolaborasiInovasi() { +function ListKolaborasiInovasi({ search }: { search: string }) { + const listState = useProxy(kolaborasiInovasiState) + const { data, loading, page, totalPages, load } = listState.findMany const router = useRouter(); + + useEffect(() => { + load(page, 10) + }, [page]) + + const filteredData = (data || []).filter(item => { + const keyword = search.toLowerCase(); + return ( + item.name.toLowerCase().includes(keyword) || + item.deskripsi.toLowerCase().includes(keyword) || + item.slug.toLowerCase().includes(keyword) || + item.kolaborator.toLowerCase().includes(keyword) + ); + }); + + if (loading || !data) { + return ( + + + + ); + } + if (data.length === 0) { + return ( + + + + + + + + No + Nama Kolaborasi Inovasi + Tahun + Deskripsi Singkat + Detail + + +
+ Tidak ada data kolaborasi inovasi yang tersedia +
+
+
+ ); + } + return ( @@ -31,26 +89,43 @@ function ListKolaborasiInovasi() { + No Nama Kolaborasi Inovasi - Image + Tahun Deskripsi Singkat Detail - + - - Kolaborasi Inovasi 1 - Image - Deskripsi Singkat - - - - + {filteredData.map((item, index) => ( + + {index + 1} + {item.name} + {item.tahun} + {item.slug} + + + + + ))} -
+
+
+ { + load(newPage, 10); + window.scrollTo(0, 0); + }} + total={totalPages} + mt="md" + mb="md" + /> +
+
); } diff --git a/src/app/api/[[...slugs]]/_lib/inovasi/index.ts b/src/app/api/[[...slugs]]/_lib/inovasi/index.ts index 758d4203..b4ebd4bf 100644 --- a/src/app/api/[[...slugs]]/_lib/inovasi/index.ts +++ b/src/app/api/[[...slugs]]/_lib/inovasi/index.ts @@ -1,6 +1,7 @@ import Elysia from "elysia"; import DesaDigital from "./desa-digital"; import ProgramKreatif from "./program-kreatif"; +import KolaborasiInovasi from "./kolaborasi-inovasi"; const Inovasi = new Elysia({ prefix: "/api/inovasi", @@ -8,5 +9,6 @@ const Inovasi = new Elysia({ }) .use(DesaDigital) .use(ProgramKreatif) + .use(KolaborasiInovasi) export default Inovasi; diff --git a/src/app/api/[[...slugs]]/_lib/inovasi/kolaborasi-inovasi/create.ts b/src/app/api/[[...slugs]]/_lib/inovasi/kolaborasi-inovasi/create.ts new file mode 100644 index 00000000..474b79a4 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/inovasi/kolaborasi-inovasi/create.ts @@ -0,0 +1,34 @@ +import prisma from "@/lib/prisma"; +import { Context } from "elysia"; + +type FormCreateKolaborasiInovasi = { + name: string; + tahun: number; + slug: string; + deskripsi: string; + kolaborator: string; + imageId: string; +} + +export default async function kolaborasiInovasiCreate(context: Context){ + const body = context.body as FormCreateKolaborasiInovasi; + + await prisma.kolaborasiInovasi.create({ + data: { + name: body.name, + tahun: body.tahun, + slug: body.slug, + deskripsi: body.deskripsi, + kolaborator: body.kolaborator, + imageId: body.imageId, + } + }) + + return { + success: true, + message: "Success create kolaborasi inovasi", + data: { + ...body, + } + } +} \ No newline at end of file diff --git a/src/app/api/[[...slugs]]/_lib/inovasi/kolaborasi-inovasi/del.ts b/src/app/api/[[...slugs]]/_lib/inovasi/kolaborasi-inovasi/del.ts new file mode 100644 index 00000000..272946d9 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/inovasi/kolaborasi-inovasi/del.ts @@ -0,0 +1,33 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import prisma from "@/lib/prisma"; +import { Context } from "elysia"; + +export default async function kolaborasiInovasiDelete(context: Context){ + const { id } = context.params as { id: string }; + + if (!id) { + return { + success: false, + message: "ID kolaborasi inovasi diperlukan", + }; + } + + try { + const deleted = await prisma.kolaborasiInovasi.delete({ + where: { id }, + }); + + return { + success: true, + message: "Kolaborasi inovasi berhasil dihapus", + data: deleted, + }; + } catch (error: any) { + console.error("Error delete kolaborasi inovasi:", error); + return { + success: false, + message: "Terjadi kesalahan saat menghapus kolaborasi inovasi", + error: error.message, + }; + } +} \ No newline at end of file diff --git a/src/app/api/[[...slugs]]/_lib/inovasi/kolaborasi-inovasi/findMany.ts b/src/app/api/[[...slugs]]/_lib/inovasi/kolaborasi-inovasi/findMany.ts new file mode 100644 index 00000000..10c19973 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/inovasi/kolaborasi-inovasi/findMany.ts @@ -0,0 +1,44 @@ +import prisma from "@/lib/prisma"; +import { Context } from "elysia"; + +// Di findMany.ts +export default async function kolaborasiInovasiFindMany(context: Context) { + const page = Number(context.query.page) || 1; + const limit = Number(context.query.limit) || 10; + const skip = (page - 1) * limit; + + try { + const [data, total] = await Promise.all([ + prisma.kolaborasiInovasi.findMany({ + where: { isActive: true }, + skip, + take: limit, + orderBy: { createdAt: 'desc' }, + }), + prisma.kolaborasiInovasi.count({ + where: { isActive: true } + }) + ]); + + const totalPages = Math.ceil(total / limit); + + return { + success: true, + message: "Success fetch kolaborasi inovasi with pagination", + data, + page, + totalPages, + total, + }; + } catch (e) { + console.error("Find many paginated error:", e); + return { + success: false, + message: "Failed fetch kolaborasi inovasi with pagination", + data: [], + page: 1, + totalPages: 1, + total: 0, + }; + } +} \ No newline at end of file diff --git a/src/app/api/[[...slugs]]/_lib/inovasi/kolaborasi-inovasi/findUnique.ts b/src/app/api/[[...slugs]]/_lib/inovasi/kolaborasi-inovasi/findUnique.ts new file mode 100644 index 00000000..72872731 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/inovasi/kolaborasi-inovasi/findUnique.ts @@ -0,0 +1,39 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import prisma from "@/lib/prisma"; +import { Context } from "elysia"; + +export default async function kolaborasiInovasiFindUnique(context: Context) { + const { id } = context.params as { id: string }; + + if (!id) { + return { + success: false, + message: "ID kolaborasi inovasi diperlukan", + }; + } + + try { + const kolaborasiInovasi = await prisma.kolaborasiInovasi.findUnique({ + where: { id }, + }); + + if (!kolaborasiInovasi) { + return { + success: false, + message: "Kolaborasi inovasi tidak ditemukan", + }; + } + + return { + success: true, + data: kolaborasiInovasi, + }; + } catch (error: any) { + console.error("Error findUnique kolaborasi inovasi:", error); + return { + success: false, + message: "Gagal mengambil data kolaborasi inovasi", + error: error.message, + }; + } +} \ No newline at end of file diff --git a/src/app/api/[[...slugs]]/_lib/inovasi/kolaborasi-inovasi/index.ts b/src/app/api/[[...slugs]]/_lib/inovasi/kolaborasi-inovasi/index.ts new file mode 100644 index 00000000..ea162530 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/inovasi/kolaborasi-inovasi/index.ts @@ -0,0 +1,45 @@ +import Elysia, { t } from "elysia"; +import kolaborasiInovasiFindMany from "./findMany"; +import kolaborasiInovasiFindUnique from "./findUnique"; +import kolaborasiInovasiCreate from "./create"; +import kolaborasiInovasiUpdate from "./updt"; +import kolaborasiInovasiDelete from "./del"; + +const KolaborasiInovasi = new Elysia({ + prefix: "/kolaborasiinovasi", + tags: ["Inovasi/Kolaborasi Inovasi"], +}) + .get("/find-many", kolaborasiInovasiFindMany) + .get("/:id", async (context) => { + const response = await kolaborasiInovasiFindUnique(context); + return response; + }) + .post("/create", kolaborasiInovasiCreate, { + body: t.Object({ + name: t.String(), + tahun: t.Number(), + slug: t.String(), + deskripsi: t.String(), + kolaborator: t.String(), + imageId: t.String(), + }), + }) + .put( + "/:id", + async (context) => { + const response = await kolaborasiInovasiUpdate(context); + return response; + }, + { + body: t.Object({ + name: t.String(), + tahun: t.Number(), + slug: t.String(), + deskripsi: t.String(), + kolaborator: t.String(), + imageId: t.String(), + }), + } + ) + .delete("/del/:id", kolaborasiInovasiDelete); +export default KolaborasiInovasi; diff --git a/src/app/api/[[...slugs]]/_lib/inovasi/kolaborasi-inovasi/updt.ts b/src/app/api/[[...slugs]]/_lib/inovasi/kolaborasi-inovasi/updt.ts new file mode 100644 index 00000000..774853b9 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/inovasi/kolaborasi-inovasi/updt.ts @@ -0,0 +1,51 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import prisma from "@/lib/prisma"; +import { Context } from "elysia"; + +type FormUpdateKolaborasiInovasi = { + id: string; + name?: string; + tahun?: number; + slug?: string; + deskripsi?: string; + kolaborator?: string; + imageId?: string; +}; +export default async function kolaborasiInovasiUpdate(context: Context) { + const body = context.body as FormUpdateKolaborasiInovasi; + const id = context.params?.id; // ambil dari URL param + + if (!id) { + return { + success: false, + message: "ID kolaborasi inovasi wajib diisi", + }; + } + + try { + const updated = await prisma.kolaborasiInovasi.update({ + where: { id }, + data: { + name: body.name, + tahun: body.tahun, + slug: body.slug, + deskripsi: body.deskripsi, + kolaborator: body.kolaborator, + imageId: body.imageId, + }, + }); + + return { + success: true, + message: "Kolaborasi inovasi berhasil diupdate", + data: updated, + }; + } catch (error: any) { + console.error("Error update kolaborasi inovasi:", error); + return { + success: false, + message: "Gagal mengupdate kolaborasi inovasi", + error: error.message, + }; + } + } \ No newline at end of file