diff --git a/bun.lockb b/bun.lockb index df43242d..7d65a042 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 9fdd42ac..80a7f3a7 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "@mantine/carousel": "^7.16.2", "@mantine/charts": "^7.17.1", "@mantine/core": "^7.17.4", - "@mantine/dates": "^7.17.4", + "@mantine/dates": "^8.1.0", "@mantine/dropzone": "^7.17.0", "@mantine/form": "^8.1.0", "@mantine/hooks": "^7.17.4", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 50fd4371..238d68fc 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -50,36 +50,39 @@ model AppMenuChild { // ========================================= FILE STORAGE ========================================= // model FileStorage { - id String @id @default(cuid()) - name String @unique - realName String - path String - mimeType String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime? - isActive Boolean @default(true) - link String - Berita Berita[] - PotensiDesa PotensiDesa[] - Posyandu Posyandu[] - ProfilePPID ProfilePPID[] + id String @id @default(cuid()) + name String @unique + realName String + path String + mimeType String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + isActive Boolean @default(true) + link String + Berita Berita[] + PotensiDesa PotensiDesa[] + Posyandu Posyandu[] + ProfilePPID ProfilePPID[] StrukturPPID StrukturPPID[] + + GalleryFoto GalleryFoto[] } //========================================= MENU PPID ========================================= // //========================================= STRUKTUR PPID ========================================= // model StrukturPPID { - id String @id @default(cuid()) - name String @db.Text + id String @id @default(cuid()) + name String @db.Text image FileStorage? @relation(fields: [imageId], references: [id]) imageId 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) } + // ========================================= VISI MISI PPID ========================================= // model VisiMisiPPID { id String @id @default(cuid()) @@ -104,18 +107,18 @@ model DasarHukumPPID { // ========================================= PROFILE PPID ========================================= // model ProfilePPID { - id String @id @default(cuid()) - name String @db.Text - biodata String @db.Text - riwayat String @db.Text - pengalaman String @db.Text - unggulan String @db.Text + id String @id @default(cuid()) + name String @db.Text + biodata String @db.Text + riwayat String @db.Text + pengalaman String @db.Text + unggulan String @db.Text image FileStorage? @relation(fields: [imageId], references: [id]) imageId 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) } // ========================================= DAFTAR INFORMASI PUBLIK ========================================= // @@ -334,51 +337,28 @@ model CategoryPengumuman { isActive Boolean @default(true) } -// ========================================= IMAGES ========================================= // -model Images { - id String @id @default(cuid()) - url String - label String @default("null") - active Boolean @default(true) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - GalleryFoto GalleryFoto[] -} - -// ========================================= VIDEOS ========================================= // -model Videos { - id String @id @default(cuid()) - url String - label String @default("null") - active Boolean @default(true) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - GalleryVideo GalleryVideo[] -} - // ========================================= GALLERY ========================================= // model GalleryFoto { - id String @id @default(cuid()) + id String @id @default(cuid()) name String - image String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) - isActive Boolean @default(true) - imagesId String? @unique - imageGalleryFoto Images? @relation(fields: [imagesId], references: [id]) + deskripsi String @db.Text + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime @default(now()) + isActive Boolean @default(true) + imagesId String? @unique + imageGalleryFoto FileStorage? @relation(fields: [imagesId], references: [id]) } model GalleryVideo { - id String @id @default(cuid()) - name String - video String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) - isActive Boolean @default(true) - videosId String? @unique - videosGalleryVideo Videos? @relation(fields: [videosId], references: [id]) + id String @id @default(cuid()) + name String + deskripsi String @db.Text + linkVideo String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime @default(now()) + isActive Boolean @default(true) } // ========================================= MENU KESEHATAN ========================================= // diff --git a/public/Share.png b/public/Share.png new file mode 100644 index 00000000..82ffa19d Binary files /dev/null and b/public/Share.png differ diff --git a/public/bagikanPostingan.png b/public/bagikanPostingan.png new file mode 100644 index 00000000..72826cbf Binary files /dev/null and b/public/bagikanPostingan.png differ diff --git a/public/sematkan.png b/public/sematkan.png new file mode 100644 index 00000000..9ce93c24 Binary files /dev/null and b/public/sematkan.png differ diff --git a/public/video.png b/public/video.png new file mode 100644 index 00000000..3c144bc0 Binary files /dev/null and b/public/video.png differ diff --git a/src/app/admin/(dashboard)/_state/desa/gallery.ts b/src/app/admin/(dashboard)/_state/desa/gallery.ts new file mode 100644 index 00000000..3405a6ba --- /dev/null +++ b/src/app/admin/(dashboard)/_state/desa/gallery.ts @@ -0,0 +1,415 @@ +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 fotoForm = z.object({ + name: z.string().min(1, { message: "Name is required" }), + deskripsi: z.string().min(1, { message: "Deskripsi is required" }), + imagesId: z.string().nonempty(), +}); + +const videoForm = z.object({ + name: z.string().min(1, { message: "Name is required" }), + deskripsi: z.string().min(1, { message: "Deskripsi is required" }), + linkVideo: z.string().min(1, { message: "Link video is required" }), +}); + +const defaultFormFoto = { + name: "", + deskripsi: "", + imagesId: "", +}; + +const defaultFormVideo = { + name: "", + deskripsi: "", + linkVideo: "", +}; + +const foto = proxy({ + create: { + form: { ...defaultFormFoto }, + loading: false, + async create() { + const cek = fotoForm.safeParse(foto.create.form); + if (!cek.success) { + const err = `[${cek.error.issues + .map((v) => `${v.path.join(".")}`) + .join("\n")}] required`; + return toast.error(err); + } + try { + foto.create.loading = true; + const res = await ApiFetch.api.desa.gallery.foto["create"].post( + foto.create.form + ); + if (res.status === 200) { + foto.findMany.load(); + return toast.success("Foto berhasil disimpan!"); + } + return toast.error("Gagal menyimpan foto"); + } catch (error) { + console.log((error as Error).message); + } finally { + foto.create.loading = false; + } + }, + resetForm() { + foto.create.form = { ...defaultFormFoto }; + }, + }, + findMany: { + data: null as + | Prisma.GalleryFotoGetPayload<{ + include: { + imageGalleryFoto: true; + }; + }>[] + | null, + async load() { + const res = await ApiFetch.api.desa.gallery.foto["find-many"].get(); + if (res.status === 200) { + foto.findMany.data = res.data?.data ?? []; + } + }, + }, + findUnique: { + data: null as Prisma.GalleryFotoGetPayload<{ + include: { + imageGalleryFoto: true; + }; + }> | null, + async load(id: string) { + try { + const res = await fetch(`/api/desa/gallery/foto/${id}`); + if (res.ok) { + const data = await res.json(); + foto.findUnique.data = data.data ?? null; + } else { + console.error("Failed to fetch foto:", res.statusText); + foto.findUnique.data = null; + } + } catch (error) { + console.error("Error fetching foto:", error); + foto.findUnique.data = null; + } + }, + }, + delete: { + loading: false, + async byId(id: string) { + if (!id) return toast.warn("ID tidak valid"); + try { + foto.delete.loading = true; + const response = await fetch(`/api/desa/gallery/foto/del/${id}`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + }); + const result = await response.json(); + if (response.ok) { + toast.success(result.message || "Foto berhasil dihapus"); + await foto.findMany.load(); // refresh list + } else { + toast.error(result.message || "Gagal menghapus foto"); + } + } catch (error) { + console.error("Gagal delete:", error); + toast.error("Terjadi kesalahan saat menghapus foto"); + } finally { + foto.delete.loading = false; + } + }, + }, + update: { + id: "", + form: { ...defaultFormFoto }, + loading: false, + async load(id: string) { + if (!id) { + toast.warn("ID tidak valid"); + return null; + } + try { + const response = await fetch(`/api/desa/gallery/foto/${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, + deskripsi: data.deskripsi, + imagesId: data.imagesId || "", + }; + return data; + } else { + throw new Error(result.message || "Gagal memuat data"); + } + } catch (error) { + console.error("Error loading foto:", error); + toast.error( + error instanceof Error ? error.message : "Gagal memuat data" + ); + return null; + } + }, + async update() { + const cek = fotoForm.safeParse(foto.update.form); + if (!cek.success) { + const err = `[${cek.error.issues + .map((v) => `${v.path.join(".")}`) + .join("\n")}] required`; + toast.error(err); + return false; + } + try { + foto.update.loading = true; + const response = await fetch(`/api/desa/gallery/foto/${this.id}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: this.form.name, + deskripsi: this.form.deskripsi, + imagesId: this.form.imagesId, + }), + }); + 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(result.message || "Foto berhasil diupdate"); + await foto.findMany.load(); // refresh list + return true; + } else { + throw new Error(result.message || "Gagal mengupdate foto"); + } + } catch (error) { + console.error("Error updating foto:", error); + toast.error( + error instanceof Error ? error.message : "Gagal mengupdate foto" + ); + return false; + } finally { + foto.update.loading = false; + } + }, + reset() { + foto.update.id = ""; + foto.update.form = { ...defaultFormFoto }; + }, + }, +}); + +const video = proxy({ + create: { + form: { ...defaultFormVideo }, + loading: false, + async create() { + const cek = videoForm.safeParse(video.create.form); + if (!cek.success) { + const err = `[${cek.error.issues + .map((v) => `${v.path.join(".")}`) + .join("\n")}] required`; + return toast.error(err); + } + try { + video.create.loading = true; + const res = await ApiFetch.api.desa.gallery.video["create"].post( + video.create.form + ); + if (res.status === 200) { + video.findMany.load(); + return toast.success("Video berhasil disimpan!"); + } + return toast.error("Gagal menyimpan video"); + } catch (error) { + console.log((error as Error).message); + } finally { + video.create.loading = false; + } + }, + resetForm() { + video.create.form = { ...defaultFormVideo }; + }, + }, + findMany: { + data: null as + | Prisma.GalleryVideoGetPayload<{ + omit: { + isActive: true; + }; + }>[] + | null, + async load() { + const res = await ApiFetch.api.desa.gallery.video["find-many"].get(); + if (res.status === 200) { + video.findMany.data = res.data?.data ?? []; + } + }, + }, + findUnique: { + data: null as Prisma.GalleryVideoGetPayload<{ + omit: { + isActive: true; + }; + }> | null, + async load(id: string) { + try { + const res = await fetch(`/api/desa/gallery/video/${id}`); + if (res.ok) { + const data = await res.json(); + video.findUnique.data = data.data ?? null; + } else { + console.error("Failed to fetch video:", res.statusText); + video.findUnique.data = null; + } + } catch (error) { + console.error("Error fetching video:", error); + video.findUnique.data = null; + } + }, + }, + delete: { + loading: false, + async byId(id: string) { + if (!id) return toast.warn("ID tidak valid"); + try { + video.delete.loading = true; + const response = await fetch(`/api/desa/gallery/video/del/${id}`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + }); + const result = await response.json(); + if (response.ok) { + toast.success(result.message || "Video berhasil dihapus"); + await video.findMany.load(); // refresh list + } else { + toast.error(result?.message || "Gagal menghapus video"); + } + } catch (error) { + console.error("Gagal delete:", error); + toast.error("Terjadi kesalahan saat menghapus video"); + } finally { + video.delete.loading = false; + } + }, + }, + update: { + id: "", + form: { ...defaultFormVideo }, + loading: false, + async load(id: string) { + if (!id) { + toast.warn("ID tidak valid"); + return null; + } + try { + const response = await fetch(`/api/desa/gallery/video/${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, + deskripsi: data.deskripsi, + linkVideo: data.linkVideo, + }; + return data; + } else { + throw new Error(result.message || "Gagal memuat data"); + } + } catch (error) { + console.error("Error loading video:", error); + toast.error( + error instanceof Error ? error.message : "Gagal memuat data" + ); + return null; + } + }, + async update() { + const cek = videoForm.safeParse(video.update.form); + if (!cek.success) { + const err = `[${cek.error.issues + .map((v) => `${v.path.join(".")}`) + .join("\n")}] required`; + toast.error(err); + return false; + } + try { + video.update.loading = true; + const response = await fetch(`/api/desa/gallery/video/${this.id}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: this.form.name, + deskripsi: this.form.deskripsi, + linkVideo: this.form.linkVideo, + }), + }); + 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(result.message || "Video berhasil diupdate"); + await video.findMany.load(); // refresh list + return true; + } else { + throw new Error(result.message || "Gagal mengupdate video"); + } + } catch (error) { + console.error("Error updating video:", error); + toast.error( + error instanceof Error ? error.message : "Gagal mengupdate video" + ); + return false; + } finally { + video.update.loading = false; + } + }, + reset() { + video.update.id = ""; + video.update.form = { ...defaultFormVideo }; + }, + }, +}); + +const stateGallery = proxy({ + foto, + video, +}); + +export default stateGallery; diff --git a/src/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanResponden.ts b/src/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanResponden.ts index 545ec7bf..027669ad 100644 --- a/src/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanResponden.ts +++ b/src/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanResponden.ts @@ -52,7 +52,10 @@ const grafikBerdasarkanResponden = proxy({ if (id) { toast.success("Success create"); grafikBerdasarkanResponden.create.form = { - ...defaultForm + sangatbaik: "", + baik: "", + kurangbaik: "", + tidakbaik: "", }; grafikBerdasarkanResponden.findMany.load(); return id; @@ -109,7 +112,7 @@ const grafikBerdasarkanResponden = proxy({ form: {...defaultForm}, loading: false, async byId() { - + // Method implementation if needed }, async submit() { const id = this.id; diff --git a/src/app/admin/(dashboard)/desa/gallery/foto/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/gallery/foto/[id]/edit/page.tsx new file mode 100644 index 00000000..9fb7f3ff --- /dev/null +++ b/src/app/admin/(dashboard)/desa/gallery/foto/[id]/edit/page.tsx @@ -0,0 +1,119 @@ +'use client' +/* eslint-disable react-hooks/exhaustive-deps */ +import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; +import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery'; +import colors from '@/con/colors'; +import ApiFetch from '@/lib/api-fetch'; +import { Box, Button, Center, FileInput, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; +import { IconArrowBack, IconImageInPicture } 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 EditFoto() { + const fotoState = useProxy(stateGallery.foto) + const router = useRouter(); + const params = useParams(); + const [previewImage, setPreviewImage] = useState(null); + const [file, setFile] = useState(null); + + useEffect(() => { + const loadFoto = async () => { + const id = params?.id as string; + if (!id) return; + try { + const data = await fotoState.update.load(id); + if (data) { + if (data?.imageGalleryFoto?.link) { + setPreviewImage(data.imageGalleryFoto.link); + } + } + } catch (error) { + console.error('Error loading foto:', error); + toast.error('Gagal memuat data foto'); + } + }; + loadFoto(); + }, [params?.id]); + + const handleSubmit = async () => { + try { + 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"); + } + fotoState.update.form.imagesId = uploaded.id; + } + await fotoState.update.update(); + toast.success('Foto berhasil diperbarui!'); + router.push('/admin/desa/gallery/foto'); + } catch (error) { + console.error('Error updating foto:', error); + toast.error('Terjadi kesalahan saat memperbarui foto'); + } + }; + + return ( + + + + + + + + Edit Foto + Judul Foto} + placeholder='Masukkan judul foto' + value={fotoState.update.form.name} + onChange={(e) => + (fotoState.update.form.name = e.target.value) + } + /> + Upload Gambar} + value={file} + onChange={async (e) => { + if (!e) return; + setFile(e); + const base64 = await e.arrayBuffer().then((buf) => + "data:image/png;base64," + Buffer.from(buf).toString("base64") + ); + setPreviewImage(base64); + }} + /> + {previewImage ? ( + + ) : ( +
+ +
+ )} + + Deskripsi Foto + { + fotoState.update.form.deskripsi = val; + }} + /> + + + + +
+
+
+ ); +} + +export default EditFoto; diff --git a/src/app/admin/(dashboard)/desa/gallery/foto/[id]/page.tsx b/src/app/admin/(dashboard)/desa/gallery/foto/[id]/page.tsx new file mode 100644 index 00000000..682c33c0 --- /dev/null +++ b/src/app/admin/(dashboard)/desa/gallery/foto/[id]/page.tsx @@ -0,0 +1,112 @@ +'use client' +import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery'; +import React from 'react'; +import { useProxy } from 'valtio/utils'; +import { useState } from 'react'; +import { useParams, useRouter } from 'next/navigation'; +import { useShallowEffect } from '@mantine/hooks'; +import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; +import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; +import colors from '@/con/colors'; +import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; + +function DetailFoto() { + const fotoState = useProxy(stateGallery.foto) + const [modalHapus, setModalHapus] = useState(false); + const [selectedId, setSelectedId] = useState(null) + const params = useParams() + const router = useRouter() + + useShallowEffect(() => { + fotoState.findUnique.load(params?.id as string) + }, []) + + const handleHapus = () => { + if (selectedId) { + fotoState.delete.byId(selectedId) + setModalHapus(false) + setSelectedId(null) + router.push("/admin/desa/gallery/foto") + } + } + + if (!fotoState.findUnique.data) { + return ( + + + + ) + } + + return ( + + + + + + + Detail Foto + {fotoState.findUnique.data ? ( + + + + Judul + {fotoState.findUnique.data?.name} + + + Tanggal Foto + {new Date(fotoState.findUnique.data?.createdAt).toDateString()} + + + Deskripsi + + + + Gambar + gambar + + + + + + + + ) : null} + + + + {/* Modal Konfirmasi Hapus */} + setModalHapus(false)} + onConfirm={handleHapus} + text='Apakah anda yakin ingin menghapus berita ini?' + /> + + ); +} + +export default DetailFoto; diff --git a/src/app/admin/(dashboard)/desa/gallery/foto/create/page.tsx b/src/app/admin/(dashboard)/desa/gallery/foto/create/page.tsx index 6665ef5b..8f681295 100644 --- a/src/app/admin/(dashboard)/desa/gallery/foto/create/page.tsx +++ b/src/app/admin/(dashboard)/desa/gallery/foto/create/page.tsx @@ -1,45 +1,108 @@ 'use client' -import { KeamananEditor } from '@/app/admin/(dashboard)/keamanan/_com/keamananEditor'; +import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; +import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery'; import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; -import { IconArrowBack } from '@tabler/icons-react'; +import ApiFetch from '@/lib/api-fetch'; +import { Box, Button, Center, FileInput, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; +import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; function CreateFoto() { + const fotoState = useProxy(stateGallery.foto) const router = useRouter(); + const [previewImage, setPreviewImage] = useState(null); + const [file, setFile] = useState(null); + + const resetForm = () => { + fotoState.create.form = { + name: "", + deskripsi: "", + imagesId: "", + }; + + 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 upload gambar"); + } + + fotoState.create.form.imagesId = uploaded.id; + await fotoState.create.create(); + resetForm(); + router.push("/admin/desa/gallery/foto") + }; + return ( - + Create Foto Judul Foto} - placeholder='Masukkan judul foto' + label={Judul Foto} + placeholder='Masukkan judul foto' + value={fotoState.create.form.name} + onChange={(val) => { + fotoState.create.form.name = val.target.value; + }} /> - Tanggal Foto} - placeholder='Masukkan tanggal foto' + Upload Gambar} + value={file} + onChange={async (e) => { + if (!e) return; + setFile(e); + const base64 = await e.arrayBuffer().then((buf) => + "data:image/png;base64," + Buffer.from(buf).toString("base64") + ); + setPreviewImage(base64); + }} /> + {previewImage ? ( + + ) : ( +
+ +
+ )} Deskripsi Foto - { + fotoState.create.form.deskripsi = val; + }} /> - + -
+
-
+ ); } diff --git a/src/app/admin/(dashboard)/desa/gallery/foto/detail/page.tsx b/src/app/admin/(dashboard)/desa/gallery/foto/detail/page.tsx deleted file mode 100644 index 5df8ddb3..00000000 --- a/src/app/admin/(dashboard)/desa/gallery/foto/detail/page.tsx +++ /dev/null @@ -1,62 +0,0 @@ -'use client' -import colors from '@/con/colors'; -import { Box, Button, Flex, Paper, Stack, Text } from '@mantine/core'; -import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; -import { useRouter } from 'next/navigation'; -// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; - -function DetailFoto() { - const router = useRouter(); - return ( - - - - - - - Detail Foto - - - - - Judul Foto - Foto 1 - - - Tanggal Foto - 2022-01-01 - - - Deskripsi Foto - Deskripsi Foto 1 - - - - - - - - - - - - - {/* Modal Hapus - setModalHapus(false)} - onConfirm={handleHapus} - text="Apakah anda yakin ingin menghapus potensi ini?" - /> */} - - ); -} - -export default DetailFoto; - diff --git a/src/app/admin/(dashboard)/desa/gallery/foto/edit/page.tsx b/src/app/admin/(dashboard)/desa/gallery/foto/edit/page.tsx deleted file mode 100644 index 3912c2fe..00000000 --- a/src/app/admin/(dashboard)/desa/gallery/foto/edit/page.tsx +++ /dev/null @@ -1,46 +0,0 @@ -'use client' -import { KeamananEditor } from '@/app/admin/(dashboard)/keamanan/_com/keamananEditor'; -import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; -import { IconArrowBack } from '@tabler/icons-react'; -import { useRouter } from 'next/navigation'; - - - -function EditFoto() { - const router = useRouter(); - return ( - - - - - - - - Edit Foto - Judul Foto} - placeholder='Masukkan judul foto' - /> - Tanggal Foto} - placeholder='Masukkan tanggal foto' - /> - - Deskripsi Foto - - - - - - - - - ); -} - -export default EditFoto; diff --git a/src/app/admin/(dashboard)/desa/gallery/foto/page.tsx b/src/app/admin/(dashboard)/desa/gallery/foto/page.tsx index 4d32e6ab..08035001 100644 --- a/src/app/admin/(dashboard)/desa/gallery/foto/page.tsx +++ b/src/app/admin/(dashboard)/desa/gallery/foto/page.tsx @@ -1,12 +1,29 @@ 'use client' import colors from '@/con/colors'; -import { Box, Button, Paper, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core'; +import { Box, Button, Paper, Skeleton, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import JudulListTab from '../../../_com/jusulListTab'; +import { useProxy } from 'valtio/utils'; +import stateGallery from '../../../_state/desa/gallery'; +import { useShallowEffect } from '@mantine/hooks'; function Foto() { + const fotoState = useProxy(stateGallery.foto) const router = useRouter(); + + useShallowEffect(() => { + fotoState.findMany.load() + }, []) + + if (!fotoState.findMany.data) { + return ( + + + + ) + } + return ( @@ -26,16 +43,20 @@ function Foto() { - - Foto 1 - 2022-01-01 - Deskripsi Foto 1 - - - - + {fotoState.findMany.data?.map((item) => ( + + {item.name} + {new Date(item.createdAt).toDateString()} + + + + + + + + ))} diff --git a/src/app/admin/(dashboard)/desa/gallery/layout.tsx b/src/app/admin/(dashboard)/desa/gallery/layout.tsx new file mode 100644 index 00000000..78a5c467 --- /dev/null +++ b/src/app/admin/(dashboard)/desa/gallery/layout.tsx @@ -0,0 +1,10 @@ +'use client' +import LayoutTabsGallery from "../../ppid/_com/layoutTabsGallery" + +export default function Layout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ) +} \ No newline at end of file diff --git a/src/app/admin/(dashboard)/desa/gallery/page.tsx b/src/app/admin/(dashboard)/desa/gallery/page.tsx deleted file mode 100644 index cf0f58b8..00000000 --- a/src/app/admin/(dashboard)/desa/gallery/page.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import colors from '@/con/colors'; -import { Box, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core'; -import { IconPhoto, IconVideo } from '@tabler/icons-react'; -import Foto from './foto/page'; -import Video from './video/page'; - -function Gallery() { - return ( - - - Gallery - - - }> - Foto - - }> - Video - - - - - - - - - - - - - ); -} - -export default Gallery; diff --git a/src/app/admin/(dashboard)/desa/gallery/video/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/gallery/video/[id]/edit/page.tsx new file mode 100644 index 00000000..06b87f4e --- /dev/null +++ b/src/app/admin/(dashboard)/desa/gallery/video/[id]/edit/page.tsx @@ -0,0 +1,156 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +'use client' +import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; +import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery'; +import colors from '@/con/colors'; +import { ActionIcon, Box, Button, Flex, Group, Image, Modal, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; +import { IconArrowBack } 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 EditVideo() { + const router = useRouter(); + const [modalHapus, setModalHapus] = useState(false); + const videoState = useProxy(stateGallery.video) + const params = useParams() + const [formData, setFormData] = useState({ + name: videoState.findUnique.data?.name || '', + deskripsi: videoState.findUnique.data?.deskripsi || '', + linkVideo: videoState.findUnique.data?.linkVideo || '', + }) + + useEffect(() => { + const loadVideo = async () => { + const id = params?.id as string; + if (!id) return; + try { + const data = await videoState.update.load(id); + if (data) { + setFormData({ + name: data.name || '', + deskripsi: data.deskripsi || '', + linkVideo: data.linkVideo || '', + }); + } + } catch (error) { + console.error('Error loading video:', error); + toast.error('Gagal memuat data video'); + } + }; + loadVideo(); + }, [params?.id]); + + const handleSubmit = async () => { + try { + videoState.update.form = { + ...videoState.update.form, + name: formData.name, + deskripsi: formData.deskripsi, + linkVideo: formData.linkVideo, + }; + await videoState.update.update(); + toast.success('Video berhasil diperbarui!'); + router.push('/admin/desa/gallery/video'); + } catch (error) { + console.error('Error updating video:', error); + toast.error('Terjadi kesalahan saat memperbarui video'); + } + } + + return ( + + + + + + + + Edit Video + Judul Video} + placeholder='Masukkan judul video' + value={formData.name} + onChange={(val) => { + setFormData({ + ...formData, + name: val.target.value, + }) + }} + /> + + Link Video Youtube *} + placeholder='Masukkan link video youtube' + value={formData.linkVideo} + onChange={(val) => { + setFormData({ + ...formData, + linkVideo: val.target.value, + }) + }} + /> + + Cara mendapatkan link video youtube + setModalHapus(true)}> + ? + + + + + Deskripsi Video + { + setFormData({ + ...formData, + deskripsi: val, + }) + }} + /> + + + + + + + + {/* Modal Konfirmasi Hapus */} + setModalHapus(false)} + title={Cara mendapatkan link video youtube} + > + + + Langkah 1 + Buka video youtube yang ingin Anda bagikan lalu klik icon titik tiga + + + + Langkah 2 + Klik bagikan + + + + Langkah 3 + Klik dibagian sematkan + + + + Langkah 4 + Lalu copy pada bagaian srcnya aja + + + + + + ); +} + +export default EditVideo; diff --git a/src/app/admin/(dashboard)/desa/gallery/video/[id]/page.tsx b/src/app/admin/(dashboard)/desa/gallery/video/[id]/page.tsx new file mode 100644 index 00000000..1e8d7998 --- /dev/null +++ b/src/app/admin/(dashboard)/desa/gallery/video/[id]/page.tsx @@ -0,0 +1,111 @@ +'use client' +import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; +import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery'; +import colors from '@/con/colors'; +import { Box, Button, Flex, 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 DetailVideo() { + const videoState = useProxy(stateGallery.video) + const [modalHapus, setModalHapus] = useState(false); + const [selectedId, setSelectedId] = useState(null) + const params = useParams() + const router = useRouter() + + useShallowEffect(() => { + videoState.findUnique.load(params?.id as string) + }, []) + + const handleHapus = () => { + if (selectedId) { + videoState.delete.byId(selectedId) + setModalHapus(false) + setSelectedId(null) + router.push("/admin/desa/gallery/video") + } + } + + if (!videoState.findUnique.data) { + return ( + + + + ) + } + + return ( + + + + + + + Detail Video + {videoState.findUnique.data ? ( + + + + Judul + {videoState.findUnique.data?.name} + + + Link Video + {videoState.findUnique.data?.linkVideo} + + + Tanggal Video + {new Date(videoState.findUnique.data?.createdAt).toDateString()} + + + Deskripsi + + + + + + + + + ) : null} + + + + {/* Modal Konfirmasi Hapus */} + setModalHapus(false)} + onConfirm={handleHapus} + text='Apakah anda yakin ingin menghapus berita ini?' + /> + + ); +} + +export default DetailVideo; diff --git a/src/app/admin/(dashboard)/desa/gallery/video/create/page.tsx b/src/app/admin/(dashboard)/desa/gallery/video/create/page.tsx index d22037bf..4e2ec598 100644 --- a/src/app/admin/(dashboard)/desa/gallery/video/create/page.tsx +++ b/src/app/admin/(dashboard)/desa/gallery/video/create/page.tsx @@ -1,45 +1,114 @@ 'use client' -import { KeamananEditor } from '@/app/admin/(dashboard)/keamanan/_com/keamananEditor'; +import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; +import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery'; import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; +import { ActionIcon, Box, Button, Flex, Group, Image, Modal, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; function CreateVideo() { + const videoState = useProxy(stateGallery.video) const router = useRouter(); + const [modalHapus, setModalHapus] = useState(false); + + const resetForm = () => { + videoState.create.form = { + name: "", + deskripsi: "", + linkVideo: "", + }; + }; + + const handleSubmit = async () => { + await videoState.create.create(); + resetForm(); + router.push("/admin/desa/gallery/video") + }; + return ( - + Create Video Judul Video} - placeholder='Masukkan judul video' - /> - Tanggal Video} - placeholder='Masukkan tanggal video' + label={Judul Video} + placeholder='Masukkan judul video' + value={videoState.create.form.name} + onChange={(val) => { + videoState.create.form.name = val.target.value; + }} /> + + Link Video Youtube *} + placeholder='Masukkan link video youtube' + value={videoState.create.form.linkVideo} + onChange={(val) => { + videoState.create.form.linkVideo = val.target.value; + }} + /> + + Cara mendapatkan link video youtube + setModalHapus(true)}> + ? + + + Deskripsi Video - { + videoState.create.form.deskripsi = val; + }} /> - + - + - + + {/* Modal Konfirmasi Hapus */} + setModalHapus(false)} + title={Cara mendapatkan link video youtube} + > + + + Langkah 1 + Buka video youtube yang ingin Anda bagikan lalu klik icon titik tiga + + + + Langkah 2 + Klik bagikan + + + + Langkah 3 + Klik dibagian sematkan + + + + Langkah 4 + Lalu copy pada bagaian srcnya aja + + + + + ); } diff --git a/src/app/admin/(dashboard)/desa/gallery/video/detail/page.tsx b/src/app/admin/(dashboard)/desa/gallery/video/detail/page.tsx deleted file mode 100644 index f8a1af21..00000000 --- a/src/app/admin/(dashboard)/desa/gallery/video/detail/page.tsx +++ /dev/null @@ -1,62 +0,0 @@ -'use client' -import colors from '@/con/colors'; -import { Box, Button, Flex, Paper, Stack, Text } from '@mantine/core'; -import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; -import { useRouter } from 'next/navigation'; -// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; - -function DetailVideo() { - const router = useRouter(); - return ( - - - - - - - Detail Video - - - - - Judul Video - Video 1 - - - Tanggal Video - 2022-01-01 - - - Deskripsi Video - Deskripsi Video 1 - - - - - - - - - - - - - {/* Modal Hapus - setModalHapus(false)} - onConfirm={handleHapus} - text="Apakah anda yakin ingin menghapus potensi ini?" - /> */} - - ); -} - -export default DetailVideo; - diff --git a/src/app/admin/(dashboard)/desa/gallery/video/edit/page.tsx b/src/app/admin/(dashboard)/desa/gallery/video/edit/page.tsx deleted file mode 100644 index eecef4ac..00000000 --- a/src/app/admin/(dashboard)/desa/gallery/video/edit/page.tsx +++ /dev/null @@ -1,46 +0,0 @@ -'use client' -import { KeamananEditor } from '@/app/admin/(dashboard)/keamanan/_com/keamananEditor'; -import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; -import { IconArrowBack } from '@tabler/icons-react'; -import { useRouter } from 'next/navigation'; - - - -function EditVideo() { - const router = useRouter(); - return ( - - - - - - - - Edit Video - Judul Video} - placeholder='Masukkan judul video' - /> - Tanggal Video} - placeholder='Masukkan tanggal video' - /> - - Deskripsi Video - - - - - - - - - ); -} - -export default EditVideo; diff --git a/src/app/admin/(dashboard)/desa/gallery/video/page.tsx b/src/app/admin/(dashboard)/desa/gallery/video/page.tsx index 5b5437a7..f0cea1d9 100644 --- a/src/app/admin/(dashboard)/desa/gallery/video/page.tsx +++ b/src/app/admin/(dashboard)/desa/gallery/video/page.tsx @@ -1,12 +1,29 @@ 'use client' import colors from '@/con/colors'; -import { Box, Button, Paper, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core'; +import { Box, Button, Paper, Skeleton, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import JudulListTab from '../../../_com/jusulListTab'; +import { useProxy } from 'valtio/utils'; +import stateGallery from '../../../_state/desa/gallery'; +import { useShallowEffect } from '@mantine/hooks'; function Video() { + const videoState = useProxy(stateGallery.video) const router = useRouter(); + + useShallowEffect(() => { + videoState.findMany.load() + }, []) + + if (!videoState.findMany.data) { + return ( + + + + ) + } + return ( @@ -26,16 +43,20 @@ function Video() { - - Video 1 - 2022-01-01 - Deskripsi Video 1 - - - - + {videoState.findMany.data?.map((item) => ( + + {item.name} + {new Date(item.createdAt).toDateString()} + + + + + + + + ))} diff --git a/src/app/admin/(dashboard)/ppid/_com/layoutTabsGallery.tsx b/src/app/admin/(dashboard)/ppid/_com/layoutTabsGallery.tsx new file mode 100644 index 00000000..fd3cbfcf --- /dev/null +++ b/src/app/admin/(dashboard)/ppid/_com/layoutTabsGallery.tsx @@ -0,0 +1,63 @@ +/* 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 LayoutTabsGallery({ children }: { children: React.ReactNode }) { + const router = useRouter() + const pathname = usePathname() + const tabs = [ + { + label: "Foto", + value: "foto", + href: "/admin/desa/gallery/foto" + }, + { + label: "Video", + value: "video", + href: "/admin/desa/gallery/video" + }, + + ]; + 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 ( + + Gallery + + + {tabs.map((e, i) => ( + {e.label} + ))} + + {tabs.map((e, i) => ( + + {/* Konten dummy, bisa diganti tergantung routing */} + <> + + ))} + + {children} + + ); +} + +export default LayoutTabsGallery; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/ppid/ikm-desa-darmasaba/grafik_berdasarkan_jenis_kelamin_responden/create/page.tsx b/src/app/admin/(dashboard)/ppid/ikm-desa-darmasaba/grafik_berdasarkan_jenis_kelamin_responden/create/page.tsx index 5a890673..cc07a64d 100644 --- a/src/app/admin/(dashboard)/ppid/ikm-desa-darmasaba/grafik_berdasarkan_jenis_kelamin_responden/create/page.tsx +++ b/src/app/admin/(dashboard)/ppid/ikm-desa-darmasaba/grafik_berdasarkan_jenis_kelamin_responden/create/page.tsx @@ -55,7 +55,7 @@ function GrafikBerdasarkanJenisKelaminRespondenCreate() { }} /> ([]); const [mounted, setMounted] = useState(false); const [modalHapus, setModalHapus] = useState(false) @@ -39,7 +39,7 @@ function GrafikBerdasarkanResponden() { }, []) useEffect(() => { - if (stategrafikBerdasarkanResponden.findMany.data && stategrafikBerdasarkanResponden.findMany.data.length > 0) { + if (stategrafikBerdasarkanResponden.findMany.data) { const totalSangatBaik = stategrafikBerdasarkanResponden.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.sangatbaik || 0), 0); const totalBaik = stategrafikBerdasarkanResponden.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.baik || 0), 0); const totalKurangBaik = stategrafikBerdasarkanResponden.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.kurangbaik || 0), 0); diff --git a/src/app/admin/(dashboard)/ppid/ikm-desa-darmasaba/grafik_hasil_kepuasan_masyarakat/page.tsx b/src/app/admin/(dashboard)/ppid/ikm-desa-darmasaba/grafik_hasil_kepuasan_masyarakat/page.tsx index d3dee5f4..532ff1c6 100644 --- a/src/app/admin/(dashboard)/ppid/ikm-desa-darmasaba/grafik_hasil_kepuasan_masyarakat/page.tsx +++ b/src/app/admin/(dashboard)/ppid/ikm-desa-darmasaba/grafik_hasil_kepuasan_masyarakat/page.tsx @@ -1,7 +1,7 @@ 'use client' import JudulListTab from '@/app/admin/(dashboard)/_com/jusulListTab'; import colors from '@/con/colors'; -import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Title } from '@mantine/core'; +import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import { useMediaQuery, useShallowEffect } from '@mantine/hooks'; import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; @@ -30,30 +30,34 @@ function GrafikHasilKepuasanMasyarakat() { const router = useRouter(); - + const handleDelete = () => { if (selectedId) { stateGrafikHasilKepuasan.delete.byId(selectedId) setModalHapus(false) setSelectedId(null) + + stateGrafikHasilKepuasan.findMany.load() } } - + useShallowEffect(() => { setMounted(true) stateGrafikHasilKepuasan.findMany.load() }, []) - + useEffect(() => { - if (stateGrafikHasilKepuasan.findMany.data && stateGrafikHasilKepuasan.findMany.data.length > 0) { - setChartData([...stateGrafikHasilKepuasan.findMany.data.map((item) => ({ - id: item.id, - label: item.label, - kepuasan: Number(item.kepuasan), - }))]); + if (stateGrafikHasilKepuasan.findMany.data) { + setChartData( + stateGrafikHasilKepuasan.findMany.data.map((item) => ({ + id: item.id, + label: item.label, + kepuasan: Number(item.kepuasan), + })) + ); } - }, [stateGrafikHasilKepuasan.findMany.data]) - + }, [stateGrafikHasilKepuasan.findMany.data]); + if (!stateGrafikHasilKepuasan.findMany.data) { @@ -114,7 +118,7 @@ function GrafikHasilKepuasanMasyarakat() { Data Kepuasan Masyarakat - {mounted && chartData.length > 0 && ( + {mounted && chartData.length > 0 ? ( @@ -122,6 +126,8 @@ function GrafikHasilKepuasanMasyarakat() { + ) : ( + Belum ada data untuk ditampilkan dalam grafik )} diff --git a/src/app/admin/_com/list_PageAdmin.tsx b/src/app/admin/_com/list_PageAdmin.tsx index a05afcd3..8f532a82 100644 --- a/src/app/admin/_com/list_PageAdmin.tsx +++ b/src/app/admin/_com/list_PageAdmin.tsx @@ -128,7 +128,7 @@ export const navBar = [ { id: "Desa_5", name: "Gallery", - path: "/admin/desa/gallery" + path: "/admin/desa/gallery/foto" }, { id: "Desa_6", diff --git a/src/app/api/[[...slugs]]/_lib/desa/gallery/foto/create.ts b/src/app/api/[[...slugs]]/_lib/desa/gallery/foto/create.ts new file mode 100644 index 00000000..e53f35af --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/desa/gallery/foto/create.ts @@ -0,0 +1,32 @@ +import prisma from "@/lib/prisma"; +import { Prisma } from "@prisma/client"; +import { Context } from "elysia"; + +type FormCreate = Prisma.GalleryFotoGetPayload<{ + select: { + name: true; + imagesId: true; + deskripsi: true; + + }; +}>; +async function galleryFotoCreate(context: Context) { + const body = context.body as FormCreate; + + await prisma.galleryFoto.create({ + data: { + name: body.name, + deskripsi: body.deskripsi, + imagesId: body.imagesId, + }, + }); + return { + success: true, + message: "Success create gallery foto", + data: { + ...body, + }, + }; +} + +export default galleryFotoCreate diff --git a/src/app/api/[[...slugs]]/_lib/desa/gallery/foto/del.ts b/src/app/api/[[...slugs]]/_lib/desa/gallery/foto/del.ts new file mode 100644 index 00000000..859c1a4b --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/desa/gallery/foto/del.ts @@ -0,0 +1,53 @@ +import prisma from "@/lib/prisma"; +import { Context } from "elysia"; +import fs from "fs/promises"; +import path from "path"; + +const galleryFotoDelete = async (context: Context) => { + const id = context.params?.id as string; + + if (!id) { + return { + status: 400, + body: "ID tidak diberikan", + }; + } + + const foto = await prisma.galleryFoto.findUnique({ + where: { id }, + include: { + imageGalleryFoto: true, + }, + }); + + if (!foto) { + return { + status: 404, + body: "Foto tidak ditemukan", + }; + } + + // Hapus file gambar dari filesystem jika ada + if (foto.imageGalleryFoto) { + try { + const filePath = path.join(foto.imageGalleryFoto.path, foto.imageGalleryFoto.name); + await fs.unlink(filePath); + await prisma.fileStorage.delete({ + where: { id: foto.imageGalleryFoto.id }, + }); + } catch (err) { + console.error("Gagal hapus gambar lama:", err); + } + } + + await prisma.galleryFoto.delete({ + where: { id }, + }); + + return { + status: 200, + body: "Foto berhasil dihapus", + }; +} + +export default galleryFotoDelete diff --git a/src/app/api/[[...slugs]]/_lib/desa/gallery/foto/find-many.ts b/src/app/api/[[...slugs]]/_lib/desa/gallery/foto/find-many.ts new file mode 100644 index 00000000..bd880117 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/desa/gallery/foto/find-many.ts @@ -0,0 +1,25 @@ +import prisma from "@/lib/prisma"; + +async function galleryFotoFindMany() { + try { + const data = await prisma.galleryFoto.findMany({ + where: { isActive: true }, + include: { + imageGalleryFoto: true, + }, + }); + + return { + success: true, + message: "Success fetch gallery foto", + data, + }; + } catch (e) { + console.error("Find many error:", e); + return { + success: false, + message: "Failed fetch gallery foto", + }; + } +} +export default galleryFotoFindMany \ No newline at end of file diff --git a/src/app/api/[[...slugs]]/_lib/desa/gallery/foto/findUnique.ts b/src/app/api/[[...slugs]]/_lib/desa/gallery/foto/findUnique.ts new file mode 100644 index 00000000..31f1f14b --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/desa/gallery/foto/findUnique.ts @@ -0,0 +1,49 @@ +import prisma from "@/lib/prisma"; + +export default async function galleryFotoFindUnique(request: Request) { + const url = new URL(request.url); + const pathSegments = url.pathname.split('/'); + const id = pathSegments[pathSegments.length - 1]; + + if (!id) { + return Response.json({ + success: false, + message: "ID tidak boleh kosong", + }, { status: 400 }); + } + + try { + if (typeof id !== 'string') { + return Response.json({ + success: false, + message: "ID tidak valid", + }, { status: 400 }); + } + + const data = await prisma.galleryFoto.findUnique({ + where: { id }, + include: { + imageGalleryFoto: true, + }, + }); + + if (!data) { + return Response.json({ + success: false, + message: "Gallery foto tidak ditemukan", + }, { status: 404 }); + } + + return Response.json({ + success: true, + message: "Success fetch gallery foto by ID", + data, + }, { status: 200 }); + } catch (e) { + console.error("Find by ID error:", e); + return Response.json({ + success: false, + message: "Gagal mengambil gallery foto: " + (e instanceof Error ? e.message : 'Unknown error'), + }, { status: 500 }); + } +} diff --git a/src/app/api/[[...slugs]]/_lib/desa/gallery/foto/index.ts b/src/app/api/[[...slugs]]/_lib/desa/gallery/foto/index.ts new file mode 100644 index 00000000..bb19aa97 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/desa/gallery/foto/index.ts @@ -0,0 +1,34 @@ +import Elysia, { t } from "elysia"; +import galleryFotoCreate from "./create"; +import galleryFotoDelete from "./del"; +import galleryFotoFindMany from "./find-many"; +import galleryFotoUpdate from "./updt"; +import galleryFotoFindUnique from "./findUnique"; + +const GalleryFoto = new Elysia({ prefix: "/gallery/foto", tags: ["Desa/Gallery/Foto"] }) + .get("/find-many", galleryFotoFindMany) + .get("/:id", async (context) => { + const response = await galleryFotoFindUnique(new Request(context.request)); + return response; + }) + .post("/create", galleryFotoCreate, { + body: t.Object({ + name: t.String(), + deskripsi: t.String(), + imagesId: t.String(), + }), + }) + .delete("/del/:id", galleryFotoDelete) + .put("/:id", async (context) => { + const response = await galleryFotoUpdate(context); + return response; + }, + { + body: t.Object({ + name: t.String(), + deskripsi: t.String(), + imagesId: t.String(), + }), + }) + +export default GalleryFoto \ No newline at end of file diff --git a/src/app/api/[[...slugs]]/_lib/desa/gallery/foto/updt.ts b/src/app/api/[[...slugs]]/_lib/desa/gallery/foto/updt.ts new file mode 100644 index 00000000..bd012f74 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/desa/gallery/foto/updt.ts @@ -0,0 +1,90 @@ +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.GalleryFotoGetPayload<{ + select: { + id: true; + name: true; + deskripsi: true; + imagesId: true; + }; +}>; + +async function galleryFotoUpdate(context: Context) { + try { + const id = context.params?.id; + const body = (await context.body) as Omit; + + const { name, deskripsi, imagesId } = body; + + if (!id) { + return new Response( + JSON.stringify({ success: false, message: "ID tidak diberikan" }), + { status: 400, headers: { "Content-Type": "application/json" } } + ); + } + + const existing = await prisma.galleryFoto.findUnique({ + where: { id }, + include: { + imageGalleryFoto: true, + }, + }); + + if (!existing) { + return new Response( + JSON.stringify({ + success: false, + message: "Gallery foto tidak ditemukan", + }), + { status: 404, headers: { "Content-Type": "application/json" } } + ); + } + + if (existing.imagesId && existing.imagesId !== imagesId) { + const oldImage = existing.imageGalleryFoto; + if (oldImage) { + try { + const filePath = path.join(oldImage.path, oldImage.name); + await fs.unlink(filePath); + await prisma.fileStorage.delete({ + where: { id: oldImage.id }, + }); + } catch (err) { + console.error("Gagal hapus gambar lama:", err); + } + } + } + + const updated = await prisma.galleryFoto.update({ + where: { id }, + data: { + name, + deskripsi, + imagesId, + }, + }); + + return new Response( + JSON.stringify({ + success: true, + message: "Success update gallery foto", + data: updated, + }), + { status: 200, headers: { "Content-Type": "application/json" } } + ); + } catch (error) { + console.error("Error updating gallery foto:", error); + return new Response( + JSON.stringify({ + success: false, + message: "Terjadi kesalahan saat mengupdate gallery foto", + }), + { status: 500, headers: { "Content-Type": "application/json" } } + ); + } +} +export default galleryFotoUpdate; diff --git a/src/app/api/[[...slugs]]/_lib/desa/gallery/video/create.ts b/src/app/api/[[...slugs]]/_lib/desa/gallery/video/create.ts new file mode 100644 index 00000000..b3b5a558 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/desa/gallery/video/create.ts @@ -0,0 +1,30 @@ +import prisma from "@/lib/prisma"; +import { Prisma } from "@prisma/client"; +import { Context } from "elysia"; + +type FormCreate = Prisma.GalleryVideoGetPayload<{ + select: { + name: true; + deskripsi: true; + linkVideo: true; + } +}> +async function galleryVideoCreate(context: Context) { + const body = context.body as FormCreate; + + await prisma.galleryVideo.create({ + data: { + name: body.name, + deskripsi: body.deskripsi, + linkVideo: body.linkVideo, + }, + }); + return { + success: true, + message: "Success create gallery video", + data: { + ...body, + }, + }; +} +export default galleryVideoCreate; \ No newline at end of file diff --git a/src/app/api/[[...slugs]]/_lib/desa/gallery/video/del.ts b/src/app/api/[[...slugs]]/_lib/desa/gallery/video/del.ts new file mode 100644 index 00000000..ae294f81 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/desa/gallery/video/del.ts @@ -0,0 +1,34 @@ +import prisma from "@/lib/prisma"; +import { Context } from "elysia"; + +const galleryVideoDelete = async (context: Context) => { + const id = context.params?.id as string; + + if (!id) { + return { + status: 400, + body: "ID tidak diberikan", + }; + } + + const galleryVideo = await prisma.galleryVideo.findUnique({ + where: { id }, + }); + + if (!galleryVideo) { + return { + status: 404, + body: "Gallery video tidak ditemukan", + }; + } + // Hapus gallery video dari database + await prisma.galleryVideo.delete({ + where: { id }, + }); + + return { + status: 200, + body: "Gallery video berhasil dihapus", + }; +} +export default galleryVideoDelete; \ No newline at end of file diff --git a/src/app/api/[[...slugs]]/_lib/desa/gallery/video/find-many.ts b/src/app/api/[[...slugs]]/_lib/desa/gallery/video/find-many.ts new file mode 100644 index 00000000..e171715e --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/desa/gallery/video/find-many.ts @@ -0,0 +1,22 @@ +import prisma from "@/lib/prisma"; + +async function galleryVideoFindMany() { + try { + const data = await prisma.galleryVideo.findMany({ + where: { isActive: true }, + }); + + return { + success: true, + message: "Success fetch gallery video", + data, + }; + } catch (e) { + console.error("Find many error:", e); + return { + success: false, + message: "Failed fetch gallery video", + }; + } +} +export default galleryVideoFindMany; \ No newline at end of file diff --git a/src/app/api/[[...slugs]]/_lib/desa/gallery/video/findUnique.ts b/src/app/api/[[...slugs]]/_lib/desa/gallery/video/findUnique.ts new file mode 100644 index 00000000..a7927b3a --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/desa/gallery/video/findUnique.ts @@ -0,0 +1,46 @@ +import prisma from "@/lib/prisma"; + +export default async function galleryVideoFindUnique(request: Request) { + const url = new URL(request.url); + const pathSegments = url.pathname.split('/'); + const id = pathSegments[pathSegments.length - 1]; + + if(!id) { + return Response.json({ + success: false, + message: "ID tidak boleh kosong", + }, { status: 400 }); + } + + try { + if (typeof id !== 'string') { + return Response.json({ + success: false, + message: "ID tidak valid", + }, { status: 400 }); + } + + const data = await prisma.galleryVideo.findUnique({ + where: { id }, + }); + + if (!data) { + return Response.json({ + success: false, + message: "Gallery video tidak ditemukan", + }, { status: 404 }); + } + + return Response.json({ + success: true, + message: "Success fetch gallery video by ID", + data, + }, { status: 200 }); + } catch (error) { + console.error("Find by ID error:", error); + return Response.json({ + success: false, + message: "Gagal mengambil gallery video: " + (error instanceof Error ? error.message : 'Unknown error'), + }, { status: 500 }); + } +} diff --git a/src/app/api/[[...slugs]]/_lib/desa/gallery/video/index.ts b/src/app/api/[[...slugs]]/_lib/desa/gallery/video/index.ts new file mode 100644 index 00000000..47c3fa27 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/desa/gallery/video/index.ts @@ -0,0 +1,39 @@ +import Elysia, { t } from "elysia"; +import galleryVideoFindMany from "./find-many"; +import galleryVideoFindUnique from "./findUnique"; +import galleryVideoCreate from "./create"; +import galleryVideoDelete from "./del"; +import galleryVideoUpdate from "./updt"; + +const GalleryVideo = new Elysia({ + prefix: "/gallery/video", + tags: ["Desa/Gallery/Video"], +}) + .get("/find-many", galleryVideoFindMany) + .get("/:id", async (context) => { + const response = await galleryVideoFindUnique(new Request(context.request)); + return response; + }) + .post("/create", galleryVideoCreate, { + body: t.Object({ + name: t.String(), + deskripsi: t.String(), + linkVideo: t.String(), + }), + }) + .delete("/del/:id", galleryVideoDelete) + .put( + "/:id", + async (context) => { + const response = await galleryVideoUpdate(context); + return response; + }, + { + body: t.Object({ + name: t.String(), + deskripsi: t.String(), + linkVideo: t.String(), + }), + } + ); +export default GalleryVideo; \ No newline at end of file diff --git a/src/app/api/[[...slugs]]/_lib/desa/gallery/video/updt.ts b/src/app/api/[[...slugs]]/_lib/desa/gallery/video/updt.ts new file mode 100644 index 00000000..553f8f6d --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/desa/gallery/video/updt.ts @@ -0,0 +1,85 @@ +import prisma from "@/lib/prisma"; +import { Prisma } from "@prisma/client"; +import { Context } from "elysia"; + +type FormUpdate = Prisma.GalleryVideoGetPayload<{ + select: { + id: true; + name: true; + deskripsi: true; + linkVideo: true; + } +}>; + +async function galleryVideoUpdate(context: Context) { + try { + const id = context.params?.id as string; + const body = (await context.body) as Omit; + + const { + name, + deskripsi, + linkVideo, + } = 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.galleryVideo.findUnique({ + where: {id} + }) + + if (!existing) { + return new Response(JSON.stringify({ + success: false, + message: "Gallery video tidak ditemukan", + }), { + status: 404, + headers: { + "Content-Type": "application/json", + }, + }); + } + + const updated = await prisma.galleryVideo.update({ + where: {id}, + data: { + name, + deskripsi, + linkVideo, + } + }) + + return new Response(JSON.stringify({ + success: true, + message: "Gallery video berhasil diupdate", + data: updated, + }), { + status: 200, + headers: { + "Content-Type": "application/json", + }, + }); + } catch (error) { + console.error("Update error:", error); + return new Response(JSON.stringify({ + success: false, + message: "Gagal mengupdate gallery video: " + (error instanceof Error ? error.message : 'Unknown error'), + }), { + status: 500, + headers: { + "Content-Type": "application/json", + }, + }); + } +} +export default galleryVideoUpdate; diff --git a/src/app/api/[[...slugs]]/_lib/desa/index.ts b/src/app/api/[[...slugs]]/_lib/desa/index.ts index fc8b25db..276970f0 100644 --- a/src/app/api/[[...slugs]]/_lib/desa/index.ts +++ b/src/app/api/[[...slugs]]/_lib/desa/index.ts @@ -3,11 +3,15 @@ import Berita from "./berita"; import Pengumuman from "./pengumuman"; import ProfileDesa from "./profile/profile_desa"; import PotensiDesa from "./potensi"; +import GalleryFoto from "./gallery/foto"; +import GalleryVideo from "./gallery/video"; const Desa = new Elysia({ prefix: "/api/desa", tags: ["Desa"] }) .use(Berita) .use(Pengumuman) .use(ProfileDesa) .use(PotensiDesa) + .use(GalleryFoto) + .use(GalleryVideo) export default Desa;