diff --git a/prisma/data/landing-page/profile.json b/prisma/data/landing-page/profile.json new file mode 100644 index 00000000..15f6f542 --- /dev/null +++ b/prisma/data/landing-page/profile.json @@ -0,0 +1,8 @@ +[ + { + "id": "edit", + "name": "I.B Surya Prabhawa Manuaba, S.H., M.H.", + "position": "Perbekel Darmasaba periode 2021-2027" + } + ] + \ No newline at end of file diff --git a/prisma/migrations/20250722071634_nico_22_jul_25/migration.sql b/prisma/migrations/20250722071634_nico_22_jul_25/migration.sql new file mode 100644 index 00000000..1379f639 --- /dev/null +++ b/prisma/migrations/20250722071634_nico_22_jul_25/migration.sql @@ -0,0 +1,56 @@ +-- CreateTable +CREATE TABLE "PejabatDesa" ( + "id" TEXT NOT NULL, + "name" VARCHAR(255) NOT NULL, + "position" TEXT NOT NULL, + "imageId" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "PejabatDesa_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ProgramInovasi" ( + "id" TEXT NOT NULL, + "name" VARCHAR(255) NOT NULL, + "description" TEXT, + "imageId" TEXT, + "link" VARCHAR(255), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "ProgramInovasi_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "MediaSosial" ( + "id" TEXT NOT NULL, + "imageId" TEXT NOT NULL, + "iconUrl" VARCHAR(255), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3), + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "MediaSosial_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "PejabatDesa_name_key" ON "PejabatDesa"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "ProgramInovasi_name_key" ON "ProgramInovasi"("name"); + +-- AddForeignKey +ALTER TABLE "PejabatDesa" ADD CONSTRAINT "PejabatDesa_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ProgramInovasi" ADD CONSTRAINT "ProgramInovasi_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "MediaSosial" ADD CONSTRAINT "MediaSosial_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e02db313..7fd500e0 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -88,6 +88,50 @@ model FileStorage { PengaduanMasyarakat PengaduanMasyarakat[] KegiatanDesa KegiatanDesa[] + + ProgramInovasi ProgramInovasi[] + + PejabatDesa PejabatDesa[] + + MediaSosial MediaSosial[] +} + +//========================================= MENU LANDING PAGE ========================================= // +//========================================= PROFILE ========================================= // +model PejabatDesa { + id String @id @default(cuid()) + name String @unique @db.VarChar(255) + position 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) +} + +model ProgramInovasi { + id String @id @default(cuid()) + name String @unique @db.VarChar(255) + description String? @db.Text + image FileStorage? @relation(fields: [imageId], references: [id]) + imageId String? + link String? @db.VarChar(255) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? @default(now()) + isActive Boolean @default(true) +} + +model MediaSosial { + id String @id @default(cuid()) + image FileStorage @relation(fields: [imageId], references: [id]) + imageId String + iconUrl String? @db.VarChar(255) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + isActive Boolean @default(true) } //========================================= MENU PPID ========================================= // @@ -1503,31 +1547,31 @@ model DataLingkunganDesa { // ========================================= GOTONG ROYONG ========================================= // model KegiatanDesa { - id String @id @default(uuid()) - judul String - deskripsiSingkat String - deskripsiLengkap String - tanggal DateTime - lokasi String - partisipan Int - image FileStorage @relation(fields: [imageId], references: [id]) - imageId String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) - isActive Boolean @default(true) - kategoriKegiatan KategoriKegiatan @relation(fields: [kategoriKegiatanId], references: [id]) - kategoriKegiatanId String + id String @id @default(uuid()) + judul String + deskripsiSingkat String + deskripsiLengkap String + tanggal DateTime + lokasi String + partisipan Int + image FileStorage @relation(fields: [imageId], references: [id]) + imageId String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime @default(now()) + isActive Boolean @default(true) + kategoriKegiatan KategoriKegiatan @relation(fields: [kategoriKegiatanId], references: [id]) + kategoriKegiatanId String } model KategoriKegiatan { - id String @id @default(cuid()) - nama String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) - isActive Boolean @default(true) - KegiatanDesa KegiatanDesa[] + id String @id @default(cuid()) + nama String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime @default(now()) + isActive Boolean @default(true) + KegiatanDesa KegiatanDesa[] } // ========================================= EDUKASI LINGKUNGAN ========================================= // diff --git a/prisma/seed.ts b/prisma/seed.ts index 66360a50..40676081 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -28,7 +28,7 @@ import contohEdukasiLingkungan from './data/lingkungan/edukasi-lingkungan/contoh import nilaiKonservasiAdat from './data/lingkungan/konservasi-adat-bali/nilai-konservasi-adat.json'; import bentukKonservasiBerdasarkanAdat from './data/lingkungan/konservasi-adat-bali/bentuk-konservasi.json'; import filosofiTriHita from './data/lingkungan/konservasi-adat-bali/filosofi-tri-hita.json'; - +import profilePejabatDesa from './data/landing-page/profile.json'; (async () => { for (const l of layanan) { @@ -578,6 +578,22 @@ import filosofiTriHita from './data/lingkungan/konservasi-adat-bali/filosofi-tri console.log("nilai konservasi adat success ..."); + for (const p of profilePejabatDesa) { + await prisma.pejabatDesa.upsert({ + where: { id: p.id }, + update: { + name: p.name, + position: p.position, + }, + create: { + id: p.id, + name: p.name, + position: p.position, + }, + }); + } + console.log("✅ profilePejabatDesa seeded without imageId (editable later via UI)"); + })() .then(() => prisma.$disconnect()) .catch((e) => { diff --git a/src/app/admin/(dashboard)/_state/landing-page/profile.ts b/src/app/admin/(dashboard)/_state/landing-page/profile.ts new file mode 100644 index 00000000..2e06c04a --- /dev/null +++ b/src/app/admin/(dashboard)/_state/landing-page/profile.ts @@ -0,0 +1,620 @@ +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 templateProgramInovasi = z.object({ + name: z.string().min(3, "Nama minimal 3 karakter"), + description: z.string().min(3, "Deskripsi minimal 3 karakter"), + imageId: z.string().min(1, "Gambar wajib dipilih"), + link: z.string().min(3, "Link minimal 3 karakter"), +}); + +type ProgramInovasiForm = Prisma.ProgramInovasiGetPayload<{ + select: { + name: true; + description: true; + imageId: true; + link: true; + }; +}>; + +const programInovasi = proxy({ + create: { + form: {} as ProgramInovasiForm, + loading: false, + async create() { + // Ensure all required fields are non-null + const formData = { + name: programInovasi.create.form.name || "", + description: programInovasi.create.form.description || "", + imageId: programInovasi.create.form.imageId || "", + link: programInovasi.create.form.link || "", + }; + + const cek = templateProgramInovasi.safeParse(formData); + if (!cek.success) { + const err = `[${cek.error.issues + .map((v) => `${v.path.join(".")}`) + .join("\n")}] required`; + return toast.error(err); + } + try { + programInovasi.create.loading = true; + const res = await ApiFetch.api.landingpage.programinovasi[ + "create" + ].post(formData); + if (res.status === 200) { + programInovasi.findMany.load(); + return toast.success("success create"); + } + console.log(res); + return toast.error("failed create"); + } catch (error) { + console.log((error as Error).message); + } finally { + programInovasi.create.loading = false; + } + }, + }, + findMany: { + data: null as + | Prisma.ProgramInovasiGetPayload<{ include: { image: true } }>[] + | null, + async load() { + const res = await ApiFetch.api.landingpage.programinovasi[ + "find-many" + ].get(); + if (res.status === 200) { + programInovasi.findMany.data = res.data?.data ?? []; + } + }, + }, + findUnique: { + data: null as Prisma.ProgramInovasiGetPayload<{ + include: { + image: true; + }; + }> | null, + async load(id: string) { + try { + const res = await fetch(`/api/landingpage/programinovasi/${id}`); + if (res.ok) { + const data = await res.json(); + programInovasi.findUnique.data = data.data ?? null; + } else { + console.error("Failed to fetch program inovasi:", res.statusText); + programInovasi.findUnique.data = null; + } + } catch (error) { + console.error("Error fetching program inovasi:", error); + programInovasi.findUnique.data = null; + } + }, + }, + delete: { + loading: false, + async byId(id: string) { + if (!id) return toast.warn("ID tidak valid"); + + try { + programInovasi.delete.loading = true; + + const response = await fetch( + `/api/landingpage/programinovasi/del/${id}`, + { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + } + ); + + const result = await response.json(); + + if (response.ok && result?.success) { + toast.success(result.message || "Program inovasi berhasil dihapus"); + await programInovasi.findMany.load(); // refresh list + } else { + toast.error(result?.message || "Gagal menghapus program inovasi"); + } + } catch (error) { + console.error("Gagal delete:", error); + toast.error("Terjadi kesalahan saat menghapus program inovasi"); + } finally { + programInovasi.delete.loading = false; + } + }, + }, + update: { + id: "", + form: {} as ProgramInovasiForm, + loading: false, + + async load(id: string) { + if (!id) { + toast.warn("ID tidak valid"); + return null; + } + + try { + const response = await fetch(`/api/landingpage/programinovasi/${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, + description: data.description, + imageId: data.imageId, + link: data.link, + }; + return data; + } else { + throw new Error( + result?.message || "Gagal mengambil data program inovasi" + ); + } + } catch (error) { + console.error((error as Error).message); + toast.error("Terjadi kesalahan saat mengambil data program inovasi"); + } finally { + programInovasi.update.loading = false; + } + }, + + async update() { + const cek = templateProgramInovasi.safeParse(programInovasi.update.form); + if (!cek.success) { + const err = `[${cek.error.issues + .map((v) => `${v.path.join(".")}`) + .join("\n")}] required`; + toast.error(err); + return false; + } + + try { + programInovasi.update.loading = true; + + const response = await fetch( + `/api/landingpage/programinovasi/${this.id}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: this.form.name, + description: this.form.description, + imageId: this.form.imageId, + link: this.form.link, + }), + } + ); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error( + errorData.message || `HTTP error! status: ${response.status}` + ); + } + + const result = await response.json(); + + if (result.success) { + toast.success("Berhasil update program inovasi"); + await programInovasi.findMany.load(); // refresh list + return true; + } else { + throw new Error(result.message || "Gagal update program inovasi"); + } + } catch (error) { + console.error("Error updating program inovasi:", error); + toast.error( + error instanceof Error + ? error.message + : "Terjadi kesalahan saat update program inovasi" + ); + return false; + } finally { + programInovasi.update.loading = false; + } + }, + }, +}); + +const templatePejabatDesa = z.object({ + name: z.string().min(3, "Nama minimal 3 karakter"), + position: z.string().min(3, "Posisi minimal 3 karakter"), + imageId: z.string().min(1, "Gambar wajib dipilih"), +}); + +const defaultFormPejabatDesa = { + name: "", + position: "", + imageId: "", +}; + +type PejabatDesaForm = { + id: string; + name: string; + position: string; + imageId: string | null; + image?: { + id: string; + name: string; + link: string; + path: string; + mimeType: string; + realName: string; + isActive: boolean; + createdAt: Date; + updatedAt: Date; + deletedAt: Date | null; + } | null; + createdAt: Date; + updatedAt: Date; + deletedAt: Date | null; + isActive: boolean; +}; + +const pejabatDesa = proxy({ + findUnique: { + data: null as PejabatDesaForm | null, + loading: false, + error: null as string | null, + async load(id: string) { + if (!id) { + toast.warn("ID tidak valid"); + return null; + } + + this.loading = true; + this.error = null; + + try { + const response = await fetch(`/api/landingpage/pejabatdesa/${id}`); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + if (result.success) { + this.data = result.data; + return result.data; + } else { + throw new Error( + result.message || "Gagal mengambil data pejabat desa" + ); + } + } catch (error) { + const errorMessage = (error as Error).message; + this.error = errorMessage; + console.error("Load pejabat desa error:", errorMessage); + toast.error("Terjadi kesalahan saat mengambil data pejabat desa"); + return null; + } finally { + this.loading = false; + } + }, + + reset() { + this.data = null; + this.error = null; + this.loading = false; + }, + }, + edit: { + id: "", + form: { ...defaultFormPejabatDesa }, + loading: false, + error: null as string | null, + isReadOnly: false, + + initialize(profileData: PejabatDesaForm) { + this.id = profileData.id; + this.isReadOnly = false; // Semua data bisa diedit + this.form = { + name: profileData.name || "", + position: profileData.position || "", + imageId: profileData.imageId || "", + }; + }, + + // Update form field + updateField(field: keyof typeof defaultFormPejabatDesa, value: string) { + this.form[field] = value; + }, + + // Submit form + async submit() { + // Validate form + const validation = templatePejabatDesa.safeParse(this.form); + + if (!validation.success) { + const errors = validation.error.issues + .map((issue) => `${issue.path.join(".")}: ${issue.message}`) + .join(", "); + toast.error(`Form tidak valid: ${errors}`); + return false; + } + + this.loading = true; + this.error = null; + + try { + const response = await fetch( + `/api/landingpage/pejabatdesa/${this.id}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(this.form), + } + ); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error( + errorData.message || `HTTP error! status: ${response.status}` + ); + } + + const result = await response.json(); + + if (result.success) { + toast.success("Berhasil update profile"); + // Refresh profile data + await pejabatDesa.findUnique.load(this.id); + return true; + } else { + throw new Error(result.message || "Gagal update profile"); + } + } catch (error) { + const errorMessage = (error as Error).message; + this.error = errorMessage; + console.error("Update profile error:", errorMessage); + toast.error("Terjadi kesalahan saat update profile"); + return false; + } finally { + this.loading = false; + } + }, + + reset() { + this.id = ""; + this.form = { ...defaultFormPejabatDesa }; + this.error = null; + this.loading = false; + this.isReadOnly = false; + }, + }, +}); + +const templateMediaSosial = z.object({ + imageId: z.string().min(1, "Gambar wajib dipilih"), + iconUrl: z.string().min(3, "Icon URL minimal 3 karakter"), +}); + +type MediaSosialForm = { + imageId: string; + iconUrl: string; +}; + +const mediaSosial = proxy({ + create: { + form: {} as MediaSosialForm, + loading: false, + async create() { + // Ensure all required fields are non-null + const formData = { + imageId: mediaSosial.create.form.imageId || "", + iconUrl: mediaSosial.create.form.iconUrl || "", + }; + + const cek = templateMediaSosial.safeParse(formData); + if (!cek.success) { + const err = `[${cek.error.issues + .map((v) => `${v.path.join(".")}`) + .join("\n")}] required`; + return toast.error(err); + } + try { + mediaSosial.create.loading = true; + const res = await ApiFetch.api.landingpage.mediasosial["create"].post( + formData + ); + if (res.status === 200) { + mediaSosial.findMany.load(); + return toast.success("success create"); + } + console.log(res); + return toast.error("failed create"); + } catch (error) { + console.log((error as Error).message); + } finally { + mediaSosial.create.loading = false; + } + }, + }, + findMany: { + data: null as + | Prisma.MediaSosialGetPayload<{ include: { image: true } }>[] + | null, + async load() { + const res = await ApiFetch.api.landingpage.mediasosial["find-many"].get(); + if (res.status === 200) { + mediaSosial.findMany.data = res.data?.data ?? []; + } + }, + }, + findUnique: { + data: null as Prisma.MediaSosialGetPayload<{ + include: { + image: true; + }; + }> | null, + async load(id: string) { + try { + const res = await fetch(`/api/landingpage/mediasosial/${id}`); + if (res.ok) { + const data = await res.json(); + mediaSosial.findUnique.data = data.data ?? null; + } else { + console.error("Failed to fetch media sosial:", res.statusText); + mediaSosial.findUnique.data = null; + } + } catch (error) { + console.error("Error fetching media sosial:", error); + mediaSosial.findUnique.data = null; + } + }, + }, + delete: { + loading: false, + async byId(id: string) { + if (!id) return toast.warn("ID tidak valid"); + + try { + mediaSosial.delete.loading = true; + + const response = await fetch(`/api/landingpage/mediasosial/del/${id}`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + }); + + const result = await response.json(); + + if (response.ok && result?.success) { + toast.success(result.message || "Media Sosial berhasil dihapus"); + await mediaSosial.findMany.load(); // refresh list + } else { + toast.error(result?.message || "Gagal menghapus media sosial"); + } + } catch (error) { + console.error("Gagal delete:", error); + toast.error("Terjadi kesalahan saat menghapus media sosial"); + } finally { + mediaSosial.delete.loading = false; + } + }, + }, + update: { + id: "", + form: {} as MediaSosialForm, + loading: false, + + async load(id: string) { + if (!id) { + toast.warn("ID tidak valid"); + return null; + } + + try { + const response = await fetch(`/api/landingpage/mediasosial/${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 = { + imageId: data.imageId, + iconUrl: data.iconUrl, + }; + return data; + } else { + throw new Error(result?.message || "Gagal mengambil data media sosial"); + } + } catch (error) { + console.error((error as Error).message); + toast.error("Terjadi kesalahan saat mengambil data media sosial"); + } finally { + mediaSosial.update.loading = false; + } + }, + + async update() { + const cek = templateMediaSosial.safeParse(mediaSosial.update.form); + if (!cek.success) { + const err = `[${cek.error.issues + .map((v) => `${v.path.join(".")}`) + .join("\n")}] required`; + toast.error(err); + return false; + } + + try { + mediaSosial.update.loading = true; + + const response = await fetch(`/api/landingpage/mediasosial/${this.id}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + imageId: this.form.imageId, + iconUrl: this.form.iconUrl, + }), + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error( + errorData.message || `HTTP error! status: ${response.status}` + ); + } + + const result = await response.json(); + + if (result.success) { + toast.success("Berhasil update media sosial"); + await mediaSosial.findMany.load(); // refresh list + return true; + } else { + throw new Error(result.message || "Gagal update media sosial"); + } + } catch (error) { + console.error("Error updating media sosial:", error); + toast.error( + error instanceof Error + ? error.message + : "Terjadi kesalahan saat update media sosial" + ); + return false; + } finally { + mediaSosial.update.loading = false; + } + }, + }, +}); + +const profileLandingPageState = proxy({ + programInovasi, + pejabatDesa, + mediaSosial, +}); + +export default profileLandingPageState; diff --git a/src/app/admin/(dashboard)/landing-page/layanan/page.tsx b/src/app/admin/(dashboard)/landing-page/layanan/page.tsx deleted file mode 100644 index ea38150b..00000000 --- a/src/app/admin/(dashboard)/landing-page/layanan/page.tsx +++ /dev/null @@ -1,23 +0,0 @@ -'use client' -import colors from '@/con/colors'; -import { Box, Button, Group, Stack, Text, Textarea, Title } from '@mantine/core'; - -function Page() { - return ( - - - Layanan - Deskripsi} - placeholder='tambah deskripsi' - /> - - Submit - - - - ); -} - -export default Page; diff --git a/src/app/admin/(dashboard)/landing-page/profile/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/landing-page/profile/_lib/layoutTabs.tsx new file mode 100644 index 00000000..5811596c --- /dev/null +++ b/src/app/admin/(dashboard)/landing-page/profile/_lib/layoutTabs.tsx @@ -0,0 +1,67 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +'use client' +import colors from '@/con/colors'; +import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core'; +import { usePathname, useRouter } from 'next/navigation'; +import React, { useEffect, useState } from 'react'; + +function LayoutTabs({ children }: { children: React.ReactNode }) { + const router = useRouter() + const pathname = usePathname() + const tabs = [ + { + label: "Program Inovasi", + value: "program-inovasi", + href: "/admin/landing-page/profile/program-inovasi" + }, + { + label: "Pejabat Desa", + value: "pejabat-desa", + href: "/admin/landing-page/profile/pejabat-desa" + }, + { + label: "Media Sosial", + value: "media-sosial", + href: "/admin/landing-page/profile/media-sosial" + }, + ]; + const curentTab = tabs.find(tab => tab.href === pathname) + const [activeTab, setActiveTab] = useState(curentTab?.value || tabs[0].value); + + const handleTabChange = (value: string | null) => { + const tab = tabs.find(t => t.value === value) + if (tab) { + router.push(tab.href) + } + setActiveTab(value) + } + + useEffect(() => { + const match = tabs.find(tab => tab.href === pathname) + if (match) { + setActiveTab(match.value) + } + }, [pathname]) + + return ( + + Profile + + + {tabs.map((e, i) => ( + {e.label} + ))} + + {tabs.map((e, i) => ( + + {/* Konten dummy, bisa diganti tergantung routing */} + <>> + + ))} + + {children} + + ); +} + +export default LayoutTabs; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/landing-page/profile/layout.tsx b/src/app/admin/(dashboard)/landing-page/profile/layout.tsx new file mode 100644 index 00000000..f1a8084c --- /dev/null +++ b/src/app/admin/(dashboard)/landing-page/profile/layout.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import LayoutTabs from './_lib/layoutTabs'; + +function Layout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} +export default Layout; diff --git a/src/app/admin/(dashboard)/landing-page/potensi/page.tsx b/src/app/admin/(dashboard)/landing-page/profile/media-sosial/[id]/edit/page.tsx similarity index 88% rename from src/app/admin/(dashboard)/landing-page/potensi/page.tsx rename to src/app/admin/(dashboard)/landing-page/profile/media-sosial/[id]/edit/page.tsx index 8a2285b8..69da2f21 100644 --- a/src/app/admin/(dashboard)/landing-page/potensi/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/profile/media-sosial/[id]/edit/page.tsx @@ -3,7 +3,7 @@ import React from 'react'; function Page() { return ( - Potensi + Page ); } diff --git a/src/app/admin/(dashboard)/landing-page/profile/page.tsx b/src/app/admin/(dashboard)/landing-page/profile/media-sosial/[id]/page.tsx similarity index 88% rename from src/app/admin/(dashboard)/landing-page/profile/page.tsx rename to src/app/admin/(dashboard)/landing-page/profile/media-sosial/[id]/page.tsx index 216d6d1d..69da2f21 100644 --- a/src/app/admin/(dashboard)/landing-page/profile/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/profile/media-sosial/[id]/page.tsx @@ -3,7 +3,7 @@ import React from 'react'; function Page() { return ( - Profile + Page ); } diff --git a/src/app/admin/(dashboard)/landing-page/penghargaan/page.tsx b/src/app/admin/(dashboard)/landing-page/profile/media-sosial/create/page.tsx similarity index 85% rename from src/app/admin/(dashboard)/landing-page/penghargaan/page.tsx rename to src/app/admin/(dashboard)/landing-page/profile/media-sosial/create/page.tsx index 8e3cf122..69da2f21 100644 --- a/src/app/admin/(dashboard)/landing-page/penghargaan/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/profile/media-sosial/create/page.tsx @@ -3,7 +3,7 @@ import React from 'react'; function Page() { return ( - Penghargaan + Page ); } diff --git a/src/app/admin/(dashboard)/landing-page/profile/media-sosial/page.tsx b/src/app/admin/(dashboard)/landing-page/profile/media-sosial/page.tsx new file mode 100644 index 00000000..c38efbfd --- /dev/null +++ b/src/app/admin/(dashboard)/landing-page/profile/media-sosial/page.tsx @@ -0,0 +1,9 @@ +import { Box, Title } from "@mantine/core"; + +export default function MediaSosial() { + return ( + + Media Sosial + + ) +} \ No newline at end of file diff --git a/src/app/admin/(dashboard)/landing-page/profile/pejabat-desa/page.tsx b/src/app/admin/(dashboard)/landing-page/profile/pejabat-desa/page.tsx new file mode 100644 index 00000000..69da2f21 --- /dev/null +++ b/src/app/admin/(dashboard)/landing-page/profile/pejabat-desa/page.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +function Page() { + return ( + + Page + + ); +} + +export default Page; diff --git a/src/app/admin/(dashboard)/landing-page/profile/program-inovasi/[id]/edit/page.tsx b/src/app/admin/(dashboard)/landing-page/profile/program-inovasi/[id]/edit/page.tsx new file mode 100644 index 00000000..3c56680d --- /dev/null +++ b/src/app/admin/(dashboard)/landing-page/profile/program-inovasi/[id]/edit/page.tsx @@ -0,0 +1,181 @@ +'use client' +import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile'; +import colors from '@/con/colors'; +import ApiFetch from '@/lib/api-fetch'; +import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; +import { Dropzone } from '@mantine/dropzone'; +import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; +import { useParams, useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; + +function EditProgramInovasi() { + const stateProgramInovasi = useProxy(profileLandingPageState.programInovasi) + const router = useRouter(); + const params = useParams(); + + const [previewImage, setPreviewImage] = useState(null); + const [file, setFile] = useState(null); + const [formData, setFormData] = useState({ + name: stateProgramInovasi.update.form.name || "", + description: stateProgramInovasi.update.form.description || "", + imageId: stateProgramInovasi.update.form.imageId || "", + link: stateProgramInovasi.update.form.link || "", + }) + + useEffect(() => { + const id = params?.id as string; + if (!id) return; + + const loadProgramInovasi = async () => { + try { + const data = await stateProgramInovasi.update.load(id); + if (data) { + setFormData({ + name: data.name || "", + description: data.description || "", + imageId: data.imageId || "", + link: data.link || "" + }); + // Tampilkan preview gambar + if (data.image?.link) { + setPreviewImage(data.image.link); + } + } + } catch (error) { + console.error("Error loading program inovasi:", error); + toast.error( + error instanceof Error ? error.message : "Gagal mengambil data program inovasi" + ); + } + } + + loadProgramInovasi(); + }, [params?.id, stateProgramInovasi.update]); + + const handleSubmit = async () => { + try { + stateProgramInovasi.update.form = { + ...stateProgramInovasi.update.form, + name: formData.name, + description: formData.description, + imageId: formData.imageId, + link: formData.link, + } + if (file) { + const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name }); + const uploaded = res.data?.data; + + if (!uploaded?.id) { + return toast.error("Gagal upload gambar"); + } + + // Update imageId in global state + stateProgramInovasi.update.form.imageId = uploaded.id; + } + + await stateProgramInovasi.update.update(); + toast.success("Program Inovasi berhasil diperbarui!"); + router.push("/admin/landing-page/profile/program-inovasi"); + } catch (error) { + console.error("Error updating program inovasi:", error); + toast.error("Terjadi kesalahan saat memperbarui program inovasi"); + } + }; + + return ( + + + router.back()} variant='subtle' color={'blue'}> + + + + + + + Edit Program Inovasi + + Gambar + + { + const selectedFile = files[0]; // Ambil file pertama + if (selectedFile) { + setFile(selectedFile); + setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview + } + }} + onReject={() => toast.error('File tidak valid.')} + maxSize={5 * 1024 ** 2} // Maks 5MB + accept={{ 'image/*': [] }} + > + + + + + + + + + + + + + + Drag gambar ke sini atau klik untuk pilih file + + + Maksimal 5MB dan harus format gambar + + + + + + {/* Tampilkan preview kalau ada */} + {previewImage && ( + + + + )} + + + + setFormData({ ...formData, name: e.target.value })} + label={Nama Produk} + placeholder='Masukkan nama produk' + /> + setFormData({ ...formData, description: e.target.value })} + label={Deskripsi} + placeholder='Masukkan deskripsi' + /> + setFormData({ ...formData, link: e.target.value })} + label={Link} + placeholder='Masukkan link' + /> + + Submit + + + + + ); +} + +export default EditProgramInovasi; diff --git a/src/app/admin/(dashboard)/landing-page/profile/program-inovasi/[id]/page.tsx b/src/app/admin/(dashboard)/landing-page/profile/program-inovasi/[id]/page.tsx new file mode 100644 index 00000000..d7e075c9 --- /dev/null +++ b/src/app/admin/(dashboard)/landing-page/profile/program-inovasi/[id]/page.tsx @@ -0,0 +1,109 @@ +'use client' +import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; +import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile'; +import colors from '@/con/colors'; +import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; +import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; +import { useParams, useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; + +function DetailProgramInovasi() { + const stateProgramInovasi = useProxy(profileLandingPageState.programInovasi) + const [modalHapus, setModalHapus] = useState(false) + const [selectedId, setSelectedId] = useState(null) + const params = useParams() + const router = useRouter(); + + useShallowEffect(() => { + stateProgramInovasi.findUnique.load(params?.id as string) + }, []) + + const handleHapus = () => { + if (selectedId) { + stateProgramInovasi.delete.byId(selectedId) + setModalHapus(false) + setSelectedId(null) + router.push("/admin/landing-page/profile/program-inovasi") + } + } + + if (!stateProgramInovasi.findUnique.data) { + return ( + + + + ) + } + + return ( + + + router.back()}> + + + + + + Detail Program Inovasi + + + + Nama Program Inovasi + {stateProgramInovasi.findUnique.data?.name} + + + Deskripsi + {stateProgramInovasi.findUnique.data?.description} + + + Link + {stateProgramInovasi.findUnique.data?.link} + + + Gambar + + + + + { + if (stateProgramInovasi.findUnique.data) { + setSelectedId(stateProgramInovasi.findUnique.data.id); + setModalHapus(true); + } + }} + disabled={!stateProgramInovasi.findUnique.data} + color="red"> + + + { + if (stateProgramInovasi.findUnique.data) { + router.push(`/admin/landing-page/profile/program-inovasi/${stateProgramInovasi.findUnique.data.id}/edit`); + } + }} + disabled={!stateProgramInovasi.findUnique.data} + color="green"> + + + + + + + + + + {/* Modal Hapus */} + setModalHapus(false)} + onConfirm={handleHapus} + text="Apakah anda yakin ingin menghapus program inovasi ini?" + /> + + ); +} + +export default DetailProgramInovasi; diff --git a/src/app/admin/(dashboard)/landing-page/profile/program-inovasi/create/page.tsx b/src/app/admin/(dashboard)/landing-page/profile/program-inovasi/create/page.tsx new file mode 100644 index 00000000..8b09cc86 --- /dev/null +++ b/src/app/admin/(dashboard)/landing-page/profile/program-inovasi/create/page.tsx @@ -0,0 +1,158 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +'use client' +import colors from '@/con/colors'; +import ApiFetch from '@/lib/api-fetch'; +import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; +import { Dropzone } from '@mantine/dropzone'; +import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; +import profileLandingPageState from '../../../../_state/landing-page/profile'; + +function CreateProgramInovasi() { + const router = useRouter(); + const stateProgramInovasi = useProxy(profileLandingPageState.programInovasi) + const [previewImage, setPreviewImage] = useState(null); + const [file, setFile] = useState(null); + + useEffect(() => { + stateProgramInovasi.findMany.load(); + }, []); + + const resetForm = () => { + stateProgramInovasi.create.form = { + name: "", + description: "", + imageId: "", + link: "", + }; + setPreviewImage(null); + setFile(null); + }; + const handleSubmit = async () => { + if (!file) { + return toast.warn("Pilih file gambar terlebih dahulu"); + } + + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }) + + const uploaded = res.data?.data; + + if (!uploaded?.id) { + return toast.error("Gagal mengupload file"); + } + + stateProgramInovasi.create.form.imageId = uploaded.id; + + await stateProgramInovasi.create.create(); + + resetForm(); + router.push("/admin/landing-page/profile/program-inovasi") + } + return ( + + + router.back()} variant='subtle' color={'blue'}> + + + + + + + Create Program Inovasi + + Gambar + + { + const selectedFile = files[0]; // Ambil file pertama + if (selectedFile) { + setFile(selectedFile); + setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview + } + }} + onReject={() => toast.error('File tidak valid.')} + maxSize={5 * 1024 ** 2} // Maks 5MB + accept={{ 'image/*': [] }} + > + + + + + + + + + + + + + + Drag gambar ke sini atau klik untuk pilih file + + + Maksimal 5MB dan harus format gambar + + + + + + {/* Tampilkan preview kalau ada */} + {previewImage && ( + + + + )} + + + + { + stateProgramInovasi.create.form.name = val.target.value; + }} + label={Nama Program Inovasi} + placeholder='Masukkan nama program inovasi' + /> + { + stateProgramInovasi.create.form.description = val.target.value; + }} + label={Deskripsi} + placeholder='Masukkan deskripsi' + /> + + { + stateProgramInovasi.create.form.link = val.target.value; + }} + label={Link} + placeholder='Masukkan link' + /> + + Submit + + + + + ); +} + +export default CreateProgramInovasi; diff --git a/src/app/admin/(dashboard)/landing-page/profile/program-inovasi/page.tsx b/src/app/admin/(dashboard)/landing-page/profile/program-inovasi/page.tsx new file mode 100644 index 00000000..76ce1d86 --- /dev/null +++ b/src/app/admin/(dashboard)/landing-page/profile/program-inovasi/page.tsx @@ -0,0 +1,90 @@ +'use client' +import colors from '@/con/colors'; +import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; +import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; +import HeaderSearch from '../../../_com/header'; +import JudulList from '../../../_com/judulList'; +import profileLandingPageState from '../../../_state/landing-page/profile'; + +function ProgramInovasi() { + const [search, setSearch] = useState(""); + return ( + + } + value={search} + onChange={(e) => setSearch(e.currentTarget.value)} + /> + + + ); +} + +function ListProgramInovasi({ search }: { search: string }) { + const stateProgramInovasi = useProxy(profileLandingPageState.programInovasi) + const router = useRouter(); + + useShallowEffect(() => { + stateProgramInovasi.findMany.load() + }, []) + + const filteredData = (stateProgramInovasi.findMany.data || []).filter(item => { + const keyword = search.toLowerCase(); + return ( + item.name.toLowerCase().includes(keyword) || + item.description?.toLowerCase().includes(keyword) || + item.link?.toLowerCase().includes(keyword) + ); + }); + + if (!stateProgramInovasi.findMany.data) { + return ( + + + + ) + } + + return ( + + + + + + + Nama Program + Deskripsi + Link + Detail + + + + {filteredData.map((item) => ( + + {item.name} + {item.description} + {item.link} + + router.push(`/admin/landing-page/profile/program-inovasi/${item.id}`)}> + + + + + ))} + + + + + ); +} + +export default ProgramInovasi; diff --git a/src/app/admin/_com/list_PageAdmin.tsx b/src/app/admin/_com/list_PageAdmin.tsx index 0523b834..a4019f63 100644 --- a/src/app/admin/_com/list_PageAdmin.tsx +++ b/src/app/admin/_com/list_PageAdmin.tsx @@ -7,7 +7,7 @@ export const navBar = [ { id: "Landing_Page_1", name: "Profile", - path: "/admin/landing-page/profile" + path: "/admin/landing-page/profile/program-inovasi" }, { id: "Landing_Page_2", diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/index.ts b/src/app/api/[[...slugs]]/_lib/landing_page/index.ts new file mode 100644 index 00000000..29ea04f4 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/index.ts @@ -0,0 +1,15 @@ +import Elysia from "elysia"; +import MediaSosial from "./profile/media-sosial"; +import ProgramInovasi from "./profile/program-inovasi"; +import PejabatDesa from "./profile/pejabat-desa"; + +const LandingPage = new Elysia({ + prefix: "/api/landingpage", + tags: ["Landing Page/Profile"] +}) + +.use(MediaSosial) +.use(ProgramInovasi) +.use(PejabatDesa) + +export default LandingPage diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/profile/media-sosial/create.ts b/src/app/api/[[...slugs]]/_lib/landing_page/profile/media-sosial/create.ts new file mode 100644 index 00000000..9e5f5c66 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/profile/media-sosial/create.ts @@ -0,0 +1,34 @@ +import prisma from "@/lib/prisma"; +import { Context } from "elysia"; + +type FormCreate = { + imageId: string; + iconUrl: string; +}; + +export default async function mediaSosialCreate(context: Context) { + const body = context.body as FormCreate; + + try { + const result = await prisma.mediaSosial.create({ + data: { + imageId: body.imageId, + iconUrl: body.iconUrl, + }, + include: { + image: true, + }, + }); + + return { + success: true, + message: "Berhasil membuat media sosial", + data: result, + }; + } catch (error) { + console.error("Error creating media sosial:", error); + throw new Error( + "Gagal membuat media sosial: " + (error as Error).message + ); + } +} diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/profile/media-sosial/del.ts b/src/app/api/[[...slugs]]/_lib/landing_page/profile/media-sosial/del.ts new file mode 100644 index 00000000..3c5b0665 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/profile/media-sosial/del.ts @@ -0,0 +1,21 @@ +import prisma from "@/lib/prisma"; +import { Context } from "elysia"; + +export default async function mediaSosialDelete(context: Context) { + const { params } = context; + const id = params?.id as string; + + if (!id) { + throw new Error("ID tidak ditemukan dalam parameter"); + } + + const deleted = await prisma.mediaSosial.delete({ + where: { id }, + }); + + return { + success: true, + message: "Berhasil menghapus media sosial", + data: deleted, + }; +} diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/profile/media-sosial/findMany.ts b/src/app/api/[[...slugs]]/_lib/landing_page/profile/media-sosial/findMany.ts new file mode 100644 index 00000000..a3825955 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/profile/media-sosial/findMany.ts @@ -0,0 +1,43 @@ +// /api/berita/findManyPaginated.ts +import prisma from "@/lib/prisma"; +import { Context } from "elysia"; + +async function mediaSosialFindMany(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.mediaSosial.findMany({ + where: { isActive: true }, + include: { + image: true, + }, + skip, + take: limit, + orderBy: { createdAt: "desc" }, // opsional, kalau mau urut berdasarkan waktu + }), + prisma.mediaSosial.count({ + where: { isActive: true }, + }), + ]); + + return { + success: true, + message: "Success fetch media sosial with pagination", + data, + page, + totalPages: Math.ceil(total / limit), + total, + }; + } catch (e) { + console.error("Find many paginated error:", e); + return { + success: false, + message: "Failed fetch media sosial with pagination", + }; + } +} + +export default mediaSosialFindMany; diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/profile/media-sosial/findUnique.ts b/src/app/api/[[...slugs]]/_lib/landing_page/profile/media-sosial/findUnique.ts new file mode 100644 index 00000000..ca3303f2 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/profile/media-sosial/findUnique.ts @@ -0,0 +1,28 @@ +import prisma from "@/lib/prisma"; +import { Context } from "elysia"; + +export default async function mediaSosialFindUnique(context: Context) { + const { params } = context; + const id = params?.id as string; + + if (!id) { + throw new Error("ID tidak ditemukan dalam parameter"); + } + + const data = await prisma.mediaSosial.findUnique({ + where: { id }, + include: { + image: true, + }, + }); + + if (!data) { + throw new Error("Media sosial tidak ditemukan"); + } + + return { + success: true, + message: "Data media sosial ditemukan", + data, + }; +} diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/profile/media-sosial/index.ts b/src/app/api/[[...slugs]]/_lib/landing_page/profile/media-sosial/index.ts new file mode 100644 index 00000000..f4de76e3 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/profile/media-sosial/index.ts @@ -0,0 +1,37 @@ +import Elysia, { t } from "elysia"; +import MediaSosialCreate from "./create"; +import MediaSosialDelete from "./del"; +import MediaSosialFindMany from "./findMany"; +import MediaSosialFindUnique from "./findUnique"; +import MediaSosialUpdate from "./updt"; + +const MediaSosial = new Elysia({ + prefix: "/mediasosial", + tags: ["Landing Page/Profile/Media Sosial"], +}) + + // ✅ Find all + .get("/find-many", MediaSosialFindMany) + + // ✅ Find by ID + .get("/:id", MediaSosialFindUnique) + + // ✅ Create + .post("/create", MediaSosialCreate, { + body: t.Object({ + imageId: t.String(), + iconUrl: t.String(), + }), + }) + + // ✅ Update + .put("/:id", MediaSosialUpdate, { + body: t.Object({ + imageId: t.Optional(t.String()), + iconUrl: t.Optional(t.String()), + }), + }) + // ✅ Delete + .delete("/del/:id", MediaSosialDelete); + +export default MediaSosial; diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/profile/media-sosial/updt.ts b/src/app/api/[[...slugs]]/_lib/landing_page/profile/media-sosial/updt.ts new file mode 100644 index 00000000..585d2a7f --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/profile/media-sosial/updt.ts @@ -0,0 +1,47 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import prisma from "@/lib/prisma"; +import { Context } from "elysia"; + +type FormUpdateMediaSosial = { + imageId?: string; + iconUrl?: string; +}; + +export default async function mediaSosialUpdate(context: Context) { + const body = context.body as FormUpdateMediaSosial; + + const id = context.params.id; + + if (!id) { + return { + success: false, + message: "ID media sosial wajib diisi", + }; + } + + try { + const updated = await prisma.mediaSosial.update({ + where: { id }, + data: { + imageId: body.imageId, + iconUrl: body.iconUrl, + }, + include: { + image: true, + }, + }); + + return { + success: true, + message: "Media sosial berhasil diperbarui", + data: updated, + }; + } catch (error: any) { + console.error("❌ Error update media sosial:", error); + return { + success: false, + message: "Gagal memperbarui data media sosial", + error: error.message, + }; + } +} diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/profile/pejabat-desa/findUnique.ts b/src/app/api/[[...slugs]]/_lib/landing_page/profile/pejabat-desa/findUnique.ts new file mode 100644 index 00000000..c2854a6a --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/profile/pejabat-desa/findUnique.ts @@ -0,0 +1,115 @@ +import prisma from "@/lib/prisma"; +import { Prisma } from "@prisma/client"; +import { Context } from "elysia"; +import fs from "fs/promises"; +import path from "path"; + +type FormUpdate = Prisma.PejabatDesaGetPayload<{ + select: { + id: true; + name: true; + position: true; + imageId: true; + }; +}>; +export default async function pejabatDesaFindUnique(context: Context) { + try { + const id = context.params?.id as string; + const body = (await context.body) as Omit; + + const { name, position, imageId } = body; + + if (!id) { + return new Response( + JSON.stringify({ + success: false, + message: "ID tidak boleh kosong", + }), + { + status: 400, + headers: { + "Content-Type": "application/json", + }, + } + ); + } + + const existing = await prisma.pejabatDesa.findUnique({ + where: { + id, + }, + include: { + image: true, + }, + }); + + if (!existing) { + return new Response( + JSON.stringify({ + success: false, + message: "Data tidak ditemukan", + }), + { + status: 404, + headers: { + "Content-Type": "application/json", + }, + } + ); + } + + if (existing.imageId !== imageId) { + const oldImage = existing.image; + if (oldImage) { + try { + const filePath = path.join(oldImage.path, oldImage.name); + await fs.unlink(filePath); + await prisma.fileStorage.delete({ + where: { id: oldImage.id }, + }); + } catch (error) { + console.error("Gagal hapus gambar lama:", error); + } + } + } + + const updated = await prisma.pejabatDesa.update({ + where: { + id, + }, + data: { + name, + position, + imageId, + }, + }); + + return new Response( + JSON.stringify({ + success: true, + message: "Data pejabat desa berhasil ditemukan", + data: updated, + }), + { + status: 200, + headers: { + "Content-Type": "application/json", + }, + } + ); + } catch (error) { + console.error("Error updating pejabat desa:", error); + return new Response( + JSON.stringify({ + success: false, + message: "Terjadi kesalahan saat mengupdate pejabat desa", + }), + { + status: 500, + headers: { + "Content-Type": "application/json", + }, + } + ); + } +} diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/profile/pejabat-desa/index.ts b/src/app/api/[[...slugs]]/_lib/landing_page/profile/pejabat-desa/index.ts new file mode 100644 index 00000000..872a8d22 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/profile/pejabat-desa/index.ts @@ -0,0 +1,28 @@ +import Elysia, { t } from "elysia"; + +import pejabatDesaFindUnique from "./findUnique"; +import pejabatDesaUpdate from "./updt"; + +const PejabatDesa = new Elysia({ + prefix: "/pejabatdesa", + tags: ["PPID/Profile PPID"] +}) + .get("/:id", async (context) => { + const response = await pejabatDesaFindUnique(context) + return response + }) + .put("/:id", async (context) => { + const response = await pejabatDesaUpdate(context) + return response + }, + { + body: t.Object({ + name: t.String(), + position: t.String(), + imageId: t.String(), + }) + } +) + + +export default PejabatDesa; diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/profile/pejabat-desa/updt.ts b/src/app/api/[[...slugs]]/_lib/landing_page/profile/pejabat-desa/updt.ts new file mode 100644 index 00000000..c09fb0c3 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/profile/pejabat-desa/updt.ts @@ -0,0 +1,115 @@ +import prisma from "@/lib/prisma"; +import { Prisma } from "@prisma/client"; +import { Context } from "elysia"; +import fs from "fs/promises"; +import path from "path"; + +type FormUpdate = Prisma.PejabatDesaGetPayload<{ + select: { + id: true; + name: true; + position: true; + imageId: true; + }; +}>; +export default async function pejabatDesaUpdate(context: Context) { + try { + const id = context.params?.id as string; + const body = (await context.body) as Omit; + + const { name, position, imageId } = body; + + if (!id) { + return new Response( + JSON.stringify({ + success: false, + message: "ID tidak boleh kosong", + }), + { + status: 400, + headers: { + "Content-Type": "application/json", + }, + } + ); + } + + const existing = await prisma.pejabatDesa.findUnique({ + where: { + id, + }, + include: { + image: true, + }, + }); + + if (!existing) { + return new Response( + JSON.stringify({ + success: false, + message: "Data tidak ditemukan", + }), + { + status: 404, + headers: { + "Content-Type": "application/json", + }, + } + ); + } + + if (existing.imageId !== imageId) { + const oldImage = existing.image; + if (oldImage) { + try { + const filePath = path.join(oldImage.path, oldImage.name); + await fs.unlink(filePath); + await prisma.fileStorage.delete({ + where: { id: oldImage.id }, + }); + } catch (error) { + console.error("Gagal hapus gambar lama:", error); + } + } + } + + const updated = await prisma.pejabatDesa.update({ + where: { + id, + }, + data: { + name, + position, + imageId, + }, + }); + + return new Response( + JSON.stringify({ + success: true, + message: "Pejabat Desa Berhasil Dibuat", + data: updated, + }), + { + status: 200, + headers: { + "Content-Type": "application/json", + }, + } + ); + } catch (error) { + console.error("Error updating pejabat desa:", error); + return new Response( + JSON.stringify({ + success: false, + message: "Terjadi kesalahan saat mengupdate pejabat desa", + }), + { + status: 500, + headers: { + "Content-Type": "application/json", + }, + } + ); + } +} diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/profile/program-inovasi/create.ts b/src/app/api/[[...slugs]]/_lib/landing_page/profile/program-inovasi/create.ts new file mode 100644 index 00000000..2d6393d6 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/profile/program-inovasi/create.ts @@ -0,0 +1,42 @@ +import prisma from "@/lib/prisma"; +import { Context } from "elysia"; + +type FormCreate = { + name: string; + description: string; + imageId: string; + link: string; +}; + +export default async function programInovasiCreate(context: Context) { + const body = context.body as FormCreate; + + if (!body.name) { + throw new Error("name wajib diisi"); + } + + try { + const result = await prisma.programInovasi.create({ + data: { + name: body.name, + description: body.description, + imageId: body.imageId, + link: body.link, + }, + include: { + image: true, + }, + }); + + return { + success: true, + message: "Berhasil membuat program inovasi", + data: result, + }; + } catch (error) { + console.error("Error creating program inovasi:", error); + throw new Error( + "Gagal membuat program inovasi: " + (error as Error).message + ); + } +} diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/profile/program-inovasi/del.ts b/src/app/api/[[...slugs]]/_lib/landing_page/profile/program-inovasi/del.ts new file mode 100644 index 00000000..525e8b71 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/profile/program-inovasi/del.ts @@ -0,0 +1,21 @@ +import prisma from "@/lib/prisma"; +import { Context } from "elysia"; + +export default async function programInovasiDelete(context: Context) { + const { params } = context; + const id = params?.id as string; + + if (!id) { + throw new Error("ID tidak ditemukan dalam parameter"); + } + + const deleted = await prisma.programInovasi.delete({ + where: { id }, + }); + + return { + success: true, + message: "Berhasil menghapus program inovasi", + data: deleted, + }; +} diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/profile/program-inovasi/findMany.ts b/src/app/api/[[...slugs]]/_lib/landing_page/profile/program-inovasi/findMany.ts new file mode 100644 index 00000000..37eebad0 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/profile/program-inovasi/findMany.ts @@ -0,0 +1,43 @@ +// /api/berita/findManyPaginated.ts +import prisma from "@/lib/prisma"; +import { Context } from "elysia"; + +async function programInovasiFindMany(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.programInovasi.findMany({ + where: { isActive: true }, + include: { + image: true, + }, + skip, + take: limit, + orderBy: { createdAt: "desc" }, // opsional, kalau mau urut berdasarkan waktu + }), + prisma.programInovasi.count({ + where: { isActive: true }, + }), + ]); + + return { + success: true, + message: "Success fetch program inovasi with pagination", + data, + page, + totalPages: Math.ceil(total / limit), + total, + }; + } catch (e) { + console.error("Find many paginated error:", e); + return { + success: false, + message: "Failed fetch program inovasi with pagination", + }; + } +} + +export default programInovasiFindMany; diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/profile/program-inovasi/findUnique.ts b/src/app/api/[[...slugs]]/_lib/landing_page/profile/program-inovasi/findUnique.ts new file mode 100644 index 00000000..8b494108 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/profile/program-inovasi/findUnique.ts @@ -0,0 +1,28 @@ +import prisma from "@/lib/prisma"; +import { Context } from "elysia"; + +export default async function programInovasiFindUnique(context: Context) { + const { params } = context; + const id = params?.id as string; + + if (!id) { + throw new Error("ID tidak ditemukan dalam parameter"); + } + + const data = await prisma.programInovasi.findUnique({ + where: { id }, + include: { + image: true, + }, + }); + + if (!data) { + throw new Error("Program inovasi tidak ditemukan"); + } + + return { + success: true, + message: "Data program inovasi ditemukan", + data, + }; +} diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/profile/program-inovasi/index.ts b/src/app/api/[[...slugs]]/_lib/landing_page/profile/program-inovasi/index.ts new file mode 100644 index 00000000..247a043f --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/profile/program-inovasi/index.ts @@ -0,0 +1,41 @@ +import Elysia, { t } from "elysia"; +import ProgramInovasiCreate from "./create"; +import ProgramInovasiDelete from "./del"; +import ProgramInovasiFindMany from "./findMany"; +import ProgramInovasiFindUnique from "./findUnique"; +import ProgramInovasiUpdate from "./updt"; + +const ProgramInovasi = new Elysia({ + prefix: "/programinovasi", + tags: ["Landing Page/Profile/Program Inovasi"], +}) + + // ✅ Find all + .get("/find-many", ProgramInovasiFindMany) + + // ✅ Find by ID + .get("/:id", ProgramInovasiFindUnique) + + // ✅ Create + .post("/create", ProgramInovasiCreate, { + body: t.Object({ + name: t.String(), + description: t.String(), + imageId: t.String(), + link: t.String(), + }), + }) + + // ✅ Update + .put("/:id", ProgramInovasiUpdate, { + body: t.Object({ + name: t.Optional(t.String()), + description: t.Optional(t.String()), + imageId: t.Optional(t.String()), + link: t.Optional(t.String()), + }), + }) + // ✅ Delete + .delete("/del/:id", ProgramInovasiDelete); + +export default ProgramInovasi; diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/profile/program-inovasi/updt.ts b/src/app/api/[[...slugs]]/_lib/landing_page/profile/program-inovasi/updt.ts new file mode 100644 index 00000000..822ad9e7 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/profile/program-inovasi/updt.ts @@ -0,0 +1,52 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import prisma from "@/lib/prisma"; +import { Context } from "elysia"; + +type FormUpdateProgramInovasi = { + name?: string; + description?: string; + imageId?: string; + link?: string; +}; + +export default async function programInovasiUpdate(context: Context) { + const body = context.body as FormUpdateProgramInovasi; + + const id = context.params.id; + + if (!id) { + return { + success: false, + message: "ID program inovasi wajib diisi", + }; + } + + try { + const updated = await prisma.programInovasi.update({ + where: { id }, + data: { + name: body.name, + description: body.description, + imageId: body.imageId, + link: body.link, + updatedAt: new Date(), + }, + include: { + image: true, + }, + }); + + return { + success: true, + message: "Program inovasi berhasil diperbarui", + data: updated, + }; + } catch (error: any) { + console.error("❌ Error update program inovasi:", error); + return { + success: false, + message: "Gagal memperbarui data program inovasi", + error: error.message, + }; + } +} diff --git a/src/app/api/[[...slugs]]/route.ts b/src/app/api/[[...slugs]]/route.ts index ff3726e3..73bc58ba 100644 --- a/src/app/api/[[...slugs]]/route.ts +++ b/src/app/api/[[...slugs]]/route.ts @@ -21,6 +21,7 @@ import Keamanan from "./_lib/keamanan"; import Ekonomi from "./_lib/ekonomi"; import Inovasi from "./_lib/inovasi"; import Lingkungan from "./_lib/lingkungan"; +import LandingPage from "./_lib/landing_page"; const ROOT = process.cwd(); @@ -85,6 +86,7 @@ const ApiServer = new Elysia() .use(Ekonomi) .use(Inovasi) .use(Lingkungan) + .use(LandingPage) .onError(({ code }) => { if (code === "NOT_FOUND") { return {