diff --git a/package.json b/package.json index 7b9450f8..842af6b7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "desa-darmasaba", - "version": "0.1.4", + "version": "0.1.5", "private": true, "scripts": { "dev": "next dev --turbopack", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 1cd3f723..d2906934 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -94,6 +94,8 @@ model FileStorage { DesaDigital DesaDigital[] KolaborasiInovasi KolaborasiInovasi[] + + InfoTekno InfoTekno[] } //========================================= MENU PPID ========================================= // @@ -1372,3 +1374,16 @@ model KolaborasiInovasi { deletedAt DateTime @default(now()) isActive Boolean @default(true) } + +// ========================================= INFO TEKHNOLOGI TEPAT GUNA ========================================= // +model InfoTekno { + id String @id @default(cuid()) + name String + deskripsi 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) +} diff --git a/src/app/admin/(dashboard)/_state/inovasi/info-tekno.ts b/src/app/admin/(dashboard)/_state/inovasi/info-tekno.ts new file mode 100644 index 00000000..b6db48ec --- /dev/null +++ b/src/app/admin/(dashboard)/_state/inovasi/info-tekno.ts @@ -0,0 +1,216 @@ +import ApiFetch from "@/lib/api-fetch"; +import { Prisma } from "@prisma/client"; +import { toast } from "react-toastify"; +import { proxy } from "valtio"; +import { z } from "zod"; + +const templateForm = z.object({ + name: z.string().min(1).max(50), + deskripsi: z.string().min(1).max(5000), + imageId: z.string().min(1).max(50), +}); + +const defaultForm = { + name: "", + deskripsi: "", + imageId: "", +}; + +const infoTeknoState = proxy({ + create: { + form: { ...defaultForm }, + loading: false, + async create() { + const cek = templateForm.safeParse(infoTeknoState.create.form); + if (!cek.success) { + const err = `[${cek.error.issues + .map((v) => `${v.path.join(".")}`) + .join("\n")}] required`; + return toast.error(err); + } + + try { + infoTeknoState.create.loading = true; + const res = await ApiFetch.api.inovasi.infotekno["create"].post( + infoTeknoState.create.form + ); + if (res.status === 200) { + infoTeknoState.findMany.load(); + return toast.success("success create"); + } + console.log(res); + return toast.error("failed create"); + } catch (error) { + console.log((error as Error).message); + } finally { + infoTeknoState.create.loading = false; + } + }, + }, + findMany: { + data: null as + | Prisma.InfoTeknoGetPayload<{ + include: { + image: true; + }; + }>[] + | null, + async load() { + const res = await ApiFetch.api.inovasi.infotekno["find-many"].get(); + if (res.status === 200) { + infoTeknoState.findMany.data = res.data?.data ?? []; + } + }, + }, + findUnique: { + data: null as Prisma.InfoTeknoGetPayload<{ + include: { + image: true; + }; + }> | null, + async load(id: string) { + try { + const res = await fetch(`/api/inovasi/infotekno/${id}`); + if (res.ok) { + const data = await res.json(); + infoTeknoState.findUnique.data = data.data ?? null; + } else { + console.error("Failed to fetch data", res.status, res.statusText); + infoTeknoState.findUnique.data = null; + } + } catch (error) { + console.error("Error loading desa digital:", error); + infoTeknoState.findUnique.data = null; + } + }, + }, + delete: { + loading: false, + async byId(id: string) { + if (!id) return toast.warn("ID tidak valid"); + + try { + infoTeknoState.delete.loading = true; + const response = await fetch(`/api/inovasi/infotekno/del/${id}`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + }); + const result = await response.json(); + + if (response.ok) { + toast.success(result.message || "Info Tekno berhasil dihapus"); + await infoTeknoState.findMany.load(); + } else { + toast.error(result?.message || "Gagal menghapus info tekno"); + } + } catch (error) { + console.log((error as Error).message); + toast.error("Terjadi kesalahan saat menghapus info tekno"); + } finally { + infoTeknoState.delete.loading = false; + } + }, + }, + edit: { + id: "", + form: { ...defaultForm }, + loading: false, + + async load(id: string) { + if (!id) { + toast.warn("ID tidak valid"); + return null; + } + try { + const response = await fetch(`/api/inovasi/infotekno/${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, + imageId: data.imageId, + }; + return data; + } else { + throw new Error(result?.message || "Gagal memuat data"); + } + } catch (error) { + console.error("Error loading info tekno:", error); + toast.error( + error instanceof Error ? error.message : "Gagal memuat data" + ); + return null; + } + }, + async update() { + const cek = templateForm.safeParse(infoTeknoState.edit.form); + if (!cek.success) { + const err = `[${cek.error.issues + .map((v) => `${v.path.join(".")}`) + .join("\n")}] required`; + toast.error(err); + return false; + } + + try { + infoTeknoState.edit.loading = true; + const response = await fetch( + `/api/inovasi/infotekno/${infoTeknoState.edit.id}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: this.form.name, + deskripsi: this.form.deskripsi, + imageId: this.form.imageId, + }), + } + ); + + 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 info tekno"); + await infoTeknoState.findMany.load(); + return true; + } else { + throw new Error(result?.message || "Gagal update info tekno"); + } + } catch (error) { + console.error("Error updating info tekno:", error); + toast.error( + error instanceof Error + ? error.message + : "Terjadi kesalahan saat update info tekno" + ); + return false; + } finally { + infoTeknoState.edit.loading = false; + } + }, + reset() { + infoTeknoState.edit.id = ""; + infoTeknoState.edit.form = { ...defaultForm }; + }, + }, +}); +export default infoTeknoState; diff --git a/src/app/admin/(dashboard)/_state/inovasi/kolaborasi-inovasi.ts b/src/app/admin/(dashboard)/_state/inovasi/kolaborasi-inovasi.ts index 4f82d04d..5f7fd327 100644 --- a/src/app/admin/(dashboard)/_state/inovasi/kolaborasi-inovasi.ts +++ b/src/app/admin/(dashboard)/_state/inovasi/kolaborasi-inovasi.ts @@ -179,7 +179,7 @@ const kolaborasiInovasiState = proxy({ }, findUnique: { data: null as Prisma.KolaborasiInovasiGetPayload<{ - omit: { isActive: true }; + include: { image: true }; }> | null, async load(id: string) { try { diff --git a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/[id]/edit/page.tsx b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/[id]/edit/page.tsx new file mode 100644 index 00000000..842cab36 --- /dev/null +++ b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/[id]/edit/page.tsx @@ -0,0 +1,136 @@ +'use client' +/* eslint-disable react-hooks/exhaustive-deps */ +import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; +import infoTeknoState from '@/app/admin/(dashboard)/_state/inovasi/info-tekno'; +import colors from '@/con/colors'; +import ApiFetch from '@/lib/api-fetch'; +import { Box, Button, Center, FileInput, 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 EditInfoTeknologiTepatGuna() { + const stateInfoTekno = useProxy(infoTeknoState) + const router = useRouter() + const params = useParams() + const [previewImage, setPreviewImage] = useState(null) + const [file, setFile] = useState(null) + const [formData, setFormData] = useState({ + name: stateInfoTekno.findUnique.data?.name || '', + deskripsi: stateInfoTekno.findUnique.data?.deskripsi || '', + imageId: stateInfoTekno.findUnique.data?.imageId || '', + }) + + useEffect(() => { + const loadPenghargaan = async () => { + const id = params?.id as string; + if (!id) return; + + try { + const data = await stateInfoTekno.edit.load(id); + if (data) { + setFormData({ + name: data.name || '', + deskripsi: data.deskripsi || '', + imageId: data.imageId || '', + }); + + if (data?.image?.link) { + setPreviewImage(data.image.link); + } + } + } catch (error) { + console.error("Error loading info teknologi tepat guna:", error); + toast.error("Gagal memuat data info teknologi tepat guna"); + } + }; + + loadPenghargaan(); + }, [params?.id]); + + const handleSubmit = async () => { + try { + stateInfoTekno.edit.form = { + ...stateInfoTekno.edit.form, + name: formData.name, + deskripsi: formData.deskripsi, + imageId: formData.imageId, + } + + 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"); + } + + stateInfoTekno.edit.form.imageId = uploaded.id; + } + await stateInfoTekno.edit.update(); + toast.success("Info teknologi tepat guna berhasil diperbarui!"); + router.push("/admin/inovasi/info-teknologi-tepat-guna"); + } catch (error) { + console.error("Error updating info teknologi tepat guna:", error); + toast.error("Terjadi kesalahan saat memperbarui info teknologi tepat guna"); + } + } + + return ( + + + + + + + Edit Info Teknologi Tepat Guna + setFormData({ ...formData, name: e.target.value })} + label={Judul} + placeholder="masukkan judul" + /> + Upload Gambar Baru (Opsional)} + 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 + { + setFormData((prev) => ({ ...prev, deskripsi: htmlContent })); + stateInfoTekno.edit.form.deskripsi = htmlContent; + }} + /> + + + +
+
+
+ ); +} + +export default EditInfoTeknologiTepatGuna; diff --git a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/[id]/page.tsx new file mode 100644 index 00000000..42cf5bdd --- /dev/null +++ b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/[id]/page.tsx @@ -0,0 +1,107 @@ +'use client' +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'; +import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; +import infoTeknoState from '../../../_state/inovasi/info-tekno'; + +function DetailInfoTeknologiTepatGuna() { + const stateInfoTekno = useProxy(infoTeknoState) + const [modalHapus, setModalHapus] = useState(false); + const [selectedId, setSelectedId] = useState(null); + const router = useRouter() + const params = useParams() + + useShallowEffect(() => { + stateInfoTekno.findUnique.load(params?.id as string) + }, []) + + const handleHapus = () => { + if (selectedId) { + stateInfoTekno.delete.byId(selectedId) + setModalHapus(false) + setSelectedId(null) + router.push("/admin/inovasi/info-teknologi-tepat-guna") + } + } + + if (!stateInfoTekno.findUnique.data) { + return ( + + + + ) + } + + return ( + + + + + + + Detail Info Teknologi Tepat Guna + {stateInfoTekno.findUnique.data ? ( + + + + Judul + {stateInfoTekno.findUnique.data?.name} + + + Deskripsi + + + + Gambar + gambar + + + + + + + + ) : null} + + + + {/* Modal Konfirmasi Hapus */} + setModalHapus(false)} + onConfirm={handleHapus} + text='Apakah anda yakin ingin menghapus info teknologi tepat guna ini?' + /> + + ); +} + +export default DetailInfoTeknologiTepatGuna; diff --git a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/create/page.tsx b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/create/page.tsx index 8da1f73f..75e5faae 100644 --- a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/create/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/create/page.tsx @@ -1,44 +1,115 @@ 'use client' import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; +import ApiFetch from '@/lib/api-fetch'; +import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; -import { KeamananEditor } from '../../../keamanan/_com/keamananEditor'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; +import CreateEditor from '../../../_com/createEditor'; +import infoTeknoState from '../../../_state/inovasi/info-tekno'; function CreateInfoTeknologiTepatGuna() { - const router = useRouter(); + const stateInfoTekno = useProxy(infoTeknoState) + const [previewImage, setPreviewImage] = useState(null); + const [file, setFile] = useState(null); + const router = useRouter() + + const resetForm = () => { + stateInfoTekno.create.form = { + name: "", + deskripsi: "", + imageId: "", + } + setPreviewImage(null) + setFile(null) + } + + const handleSubmit = async () => { + if (!file) { + return toast.error("Silahkan pilih file gambar terlebih dahulu") + } + + try { + // Upload the image first + const uploadRes = await ApiFetch.api.fileStorage.create.post({ + file: file, + name: file.name + }) + + const uploaded = uploadRes.data?.data + if (!uploaded?.id) { + return toast.error("Gagal upload gambar") + } + + // Set the image ID in the form + stateInfoTekno.create.form.imageId = uploaded.id + + // Submit the form + const success = await stateInfoTekno.create.create() + + if (success) { + resetForm() + router.push("/admin/inovasi/info-teknologi-tepat-guna") + } + } catch (error) { + console.error("Error in handleSubmit:", error) + toast.error("Terjadi kesalahan saat menyimpan data") + } + + } return ( - - - + - Create Info Teknologi Tepat Guna - - Masukkan Image - - + Create Info Teknologi Tepat Guna Nama Info Teknologi Tepat Guna} - placeholder='Masukkan nama info teknologi tepat guna' + value={stateInfoTekno.create.form.name} + onChange={(val) => { + stateInfoTekno.create.form.name = val.target.value; + }} + label={Nama Info Teknologi Tepat Guna} + placeholder="masukkan nama info teknologi tepat guna" /> - Deskripsi Info Teknologi Tepat Guna - Deskripsi + { + stateInfoTekno.create.form.deskripsi = htmlContent; + }} /> - - - - + Upload Gambar Konten} + 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 ? ( + + ) : ( +
+ +
+ )} + +
-
+ ); } diff --git a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/detail/page.tsx b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/detail/page.tsx deleted file mode 100644 index 13733e19..00000000 --- a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/detail/page.tsx +++ /dev/null @@ -1,62 +0,0 @@ -'use client' -import colors from '@/con/colors'; -import { Box, Button, Paper, Stack, Flex, Text, Image } from '@mantine/core'; -import { IconArrowBack, IconX, IconEdit } from '@tabler/icons-react'; -import { useRouter } from 'next/navigation'; -import React from 'react'; -// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; - -function DetailInfoTeknologiTepatGuna() { - const router = useRouter(); - return ( - - - - - - - Detail Info Teknologi Tepat Guna - - - - - Nama Info Teknologi Tepat Guna - Test Judul - - - Gambar - gambar - - - Deskripsi - Test Deskripsi - - - - - - - - - - - - - {/* Modal Hapus - setModalHapus(false)} - onConfirm={handleHapus} - text="Apakah anda yakin ingin menghapus potensi ini?" - /> */} - - ); -} - -export default DetailInfoTeknologiTepatGuna; diff --git a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/edit/page.tsx b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/edit/page.tsx deleted file mode 100644 index c69b8370..00000000 --- a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/edit/page.tsx +++ /dev/null @@ -1,45 +0,0 @@ -'use client' -import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; -import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; -import { useRouter } from 'next/navigation'; -import { KeamananEditor } from '../../../keamanan/_com/keamananEditor'; - - -function EditInfoTeknologiTepatGuna() { - const router = useRouter(); - return ( - - - - - - - - Edit Info Teknologi Tepat Guna - - Masukkan Image - - - Nama Info Teknologi Tepat Guna} - placeholder='Masukkan nama info teknologi tepat guna' - /> - - Deskripsi Info Teknologi Tepat Guna - - - - - - - - - ); -} - -export default EditInfoTeknologiTepatGuna; diff --git a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/page.tsx b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/page.tsx index 0d842475..d6cd23b8 100644 --- a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/page.tsx @@ -1,26 +1,53 @@ '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, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } 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 { useRouter } from 'next/navigation'; +import infoTeknoState from '../../_state/inovasi/info-tekno'; function InfoTeknologiTepatGuna() { + const [search, setSearch] = useState(""); return ( } + value={search} + onChange={(e) => setSearch(e.currentTarget.value)} /> - + ); } -function ListInfoTeknologiTepatGuna() { - const router = useRouter(); +function ListInfoTeknologiTepatGuna({ search }: { search: string }) { + const state = useProxy(infoTeknoState) + const router = useRouter() + useShallowEffect(() => { + state.findMany.load() + }, []) + + const filteredData = (state.findMany.data || []).filter(item => { + const keyword = search.toLowerCase(); + return ( + item.name.toLowerCase().includes(keyword) || + item.deskripsi.toLowerCase().includes(keyword) + ); + }); + + if (!state.findMany.data) { + return ( + + + + ) + } return ( @@ -32,24 +59,26 @@ function ListInfoTeknologiTepatGuna() { Nama Info Teknologi Tepat Guna - Image - Deskripsi + Deskripsi Singkat Info Teknologi Tepat Guna Detail - + - - Info Teknologi Tepat Guna 1 - Image - Deskripsi + {filteredData.map((item) => ( + + {item.name} + + + - + ))} - + ); diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/[id]/edit/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/[id]/edit/page.tsx index b3553f4e..0955a093 100644 --- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/[id]/edit/page.tsx @@ -1,48 +1,185 @@ -'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, IconImageInPicture } from '@tabler/icons-react'; -import { useRouter } from 'next/navigation'; +/* eslint-disable react-hooks/exhaustive-deps */ +"use client"; +import EditEditor from "@/app/admin/(dashboard)/_com/editEditor"; +import colors from "@/con/colors"; +import ApiFetch from "@/lib/api-fetch"; +import { + Box, + Button, + Center, + FileInput, + 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"; +import kolaborasiInovasiState from "@/app/admin/(dashboard)/_state/inovasi/kolaborasi-inovasi"; function EditKolaborasiInovasi() { + const kolaborasiState = useProxy(kolaborasiInovasiState); const router = useRouter(); + const params = useParams(); + + const [previewImage, setPreviewImage] = useState(null); + const [file, setFile] = useState(null); + const [formData, setFormData] = useState({ + name: kolaborasiState.update.form.name || '', + deskripsi: kolaborasiState.update.form.deskripsi || '', + tahun: kolaborasiState.update.form.tahun || '', + slug: kolaborasiState.update.form.slug || '', + kolaborator: kolaborasiState.update.form.kolaborator || '', + imageId: kolaborasiState.update.form.imageId || '' + }); + + // Load berita by id saat pertama kali + useEffect(() => { + const loadKolaborasi = async () => { + const id = params?.id as string; + if (!id) return; + + try { + const data = await kolaborasiState.update.load(id); // akses langsung, bukan dari proxy + if (data) { + setFormData({ + name: data.name || '', + deskripsi: data.deskripsi || '', + tahun: data.tahun || '', + slug: data.slug || '', + kolaborator: data.kolaborator || '', + imageId: data.imageId || '', + }); + if (data.image) { + if (data?.image?.link) { + setPreviewImage(data.image.link); + } + } + } + } catch (error) { + console.error("Error loading berita:", error); + toast.error("Gagal memuat data berita"); + } + }; + + loadKolaborasi(); + }, [params?.id]); + + const handleSubmit = async () => { + + try { + // Update global state with form data + kolaborasiState.update.form = { + ...kolaborasiState.update.form, + name: formData.name, + deskripsi: formData.deskripsi, + tahun: Number(formData.tahun), + slug: formData.slug, + kolaborator: formData.kolaborator, + imageId: formData.imageId // Keep existing imageId if not changed + }; + + // Jika ada file baru, upload + 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 + kolaborasiState.update.form.imageId = uploaded.id; + } + + await kolaborasiState.update.submit(); + toast.success("Berita berhasil diperbarui!"); + router.push("/admin/inovasi/kolaborasi-inovasi"); + } catch (error) { + console.error("Error updating berita:", error); + toast.error("Terjadi kesalahan saat memperbarui berita"); + } + }; + return ( - - - + - Edit Kolaborasi Inovasi - - Masukkan Image - - + Edit Kolaborasi Inovasi Nama Kolaborasi Inovasi} - placeholder='Masukkan nama kolaborasi inovasi' + value={formData.name} + onChange={(e) => setFormData({ ...formData, name: e.target.value })} + label={Nama} + placeholder="masukkan nama" /> + Deskripsi Singkat Kolaborasi Inovasi} - placeholder='Masukkan deskripsi singkat kolaborasi inovasi' + value={formData.slug} + onChange={(e) => setFormData({ ...formData, slug: e.target.value })} + label={Deskripsi Singkat} + placeholder="masukkan deskripsi singkat" /> + + setFormData({ ...formData, tahun: e.target.value })} + label={Tahun} + placeholder="masukkan tahun" + /> + + setFormData({ ...formData, kolaborator: e.target.value })} + label={Kolaborator} + placeholder="masukkan kolaborator" + /> + + Upload Gambar Baru (Opsional)} + 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 Kolaborasi Inovasi - Konten + { + setFormData((prev) => ({ ...prev, deskripsi: htmlContent })); + kolaborasiState.update.form.deskripsi = htmlContent; + }} /> - - - -
+ +
-
+ ); } diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/[id]/page.tsx index b52fe1e8..e2d39d60 100644 --- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/[id]/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/[id]/page.tsx @@ -1,13 +1,45 @@ 'use client' +import { useProxy } from 'valtio/utils'; + +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 colors from '@/con/colors'; -import { Box, Button, Paper, Stack, Flex, Text, Image } from '@mantine/core'; -import { IconArrowBack, IconX, IconEdit } from '@tabler/icons-react'; -import { useRouter } from 'next/navigation'; -import React from 'react'; -// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; +import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; +import kolaborasiInovasiState from '../../../_state/inovasi/kolaborasi-inovasi'; function DetailKolaborasiInovasi() { - const router = useRouter(); + const kolaborasiState = useProxy(kolaborasiInovasiState) + const [modalHapus, setModalHapus] = useState(false) + const [selectedId, setSelectedId] = useState(null) + const params = useParams() + const router = useRouter() + + useShallowEffect(() => { + kolaborasiState.findUnique.load(params?.id as string) + }, []) + + + const handleHapus = () => { + if (selectedId) { + kolaborasiState.delete.byId(selectedId) + setModalHapus(false) + setSelectedId(null) + router.push("/admin/inovasi/kolaborasi-inovasi") + } + } + + if (!kolaborasiState.findUnique.data) { + return ( + + + + ) + } + return ( @@ -15,52 +47,76 @@ function DetailKolaborasiInovasi() { - + Detail Kolaborasi Inovasi - - - - - Nama Kolaborasi Inovasi - Test Judul - - - Gambar - gambar - - - Deskripsi Singkat - Test Deskripsi Singkat - - - Deskripsi - Test Deskripsi - - - - - - - - + + + ) : null} - {/* Modal Hapus + {/* Modal Konfirmasi Hapus */} setModalHapus(false)} onConfirm={handleHapus} - text="Apakah anda yakin ingin menghapus potensi ini?" - /> */} + text='Apakah anda yakin ingin menghapus kolaborasi inovasi ini?' + /> ); } -export default DetailKolaborasiInovasi; +export default DetailKolaborasiInovasi; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/create/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/create/page.tsx index 4bfdf210..5aa364f0 100644 --- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/create/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/create/page.tsx @@ -1,50 +1,155 @@ '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, IconImageInPicture } from '@tabler/icons-react'; +import { Box, Button, Center, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; +import { IconArrowBack, IconImageInPicture, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; +import { useProxy } from 'valtio/utils'; +import CreateEditor from '../../../_com/createEditor'; +import kolaborasiInovasiState from '../../../_state/inovasi/kolaborasi-inovasi'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; +import ApiFetch from '@/lib/api-fetch'; +import { Dropzone } from '@mantine/dropzone'; - -function CreateKolaborasiInovasi() { +function CreateProgramKreatifDesa() { + const stateCreate = useProxy(kolaborasiInovasiState) const router = useRouter(); + const [previewImage, setPreviewImage] = useState(null); + const [file, setFile] = useState(null); + + const resetForm = () => { + stateCreate.create.form = { + name: "", + tahun: 0, + slug: "", + deskripsi: "", + kolaborator: "", + imageId: "", + } + + setPreviewImage(null); + setFile(null); + } + + const handleSubmit = async () => { + if (!file) { + return toast.warn("Pilih file gambar terlebih dahulu"); + } + + // Upload gambar dulu + 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"); + } + + // Simpan ID gambar ke form + stateCreate.create.form.imageId = uploaded.id; + + // Submit data berita + await stateCreate.create.create(); + + // Reset form setelah submit + resetForm(); + router.push("/admin/inovasi/kolaborasi-inovasi") + } return ( - + - Create Kolaborasi Inovasi + Create Kolaborasi Inovasi + Nama Kolaborasi Inovasi} + placeholder="masukkan nama kolaborasi inovasi" + onChange={(val) => stateCreate.create.form.name = val.target.value} + /> + Tahun} + placeholder="masukkan tahun" + onChange={(val) => stateCreate.create.form.tahun = parseInt(val.target.value)} + /> + stateCreate.create.form.slug = e.currentTarget.value} + label={Deskripsi Singkat Kolaborasi Inovasi} + placeholder='Masukkan deskripsi singkat kolaborasi inovasi' + /> + stateCreate.create.form.kolaborator = e.currentTarget.value} + label={Kolaborator} + placeholder='Masukkan kolaborator' + /> - Masukkan Image - + + Gambar + + { + const newImages = files.map((file) => ({ + file, + preview: URL.createObjectURL(file), + label: '', + })); + setFile(newImages[0].file); + setPreviewImage(newImages[0].preview); // ← ini yang kurang + }} + + > + + + + + + + + + + +
+ + Drag images here or click to select files + + + Attach as many files as you like, each file should not exceed 5mb + +
+
+
+
+ {previewImage ? ( + + ) : ( +
+ +
+ )} +
- Nama Kolaborasi Inovasi} - placeholder='Masukkan nama kolaborasi inovasi' - /> - Deskripsi Singkat Kolaborasi Inovasi} - placeholder='Masukkan deskripsi singkat kolaborasi inovasi' - /> Deskripsi Kolaborasi Inovasi - stateCreate.create.form.deskripsi = htmlContent} /> - + -
+
-
+ ); } -export default CreateKolaborasiInovasi; +export default CreateProgramKreatifDesa; diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/page.tsx index ea59d136..60fc9a59 100644 --- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/page.tsx @@ -64,11 +64,11 @@ function ListKolaborasiInovasi({ search }: { search: string }) { - No - Nama Kolaborasi Inovasi - Tahun - Deskripsi Singkat - Detail + No + Nama Kolaborasi Inovasi + Tahun + Deskripsi Singkat + Detail
@@ -89,22 +89,22 @@ function ListKolaborasiInovasi({ search }: { search: string }) { - No - Nama Kolaborasi Inovasi - Tahun - Deskripsi Singkat - Detail + No + Nama Kolaborasi Inovasi + Tahun + Deskripsi Singkat + Detail {filteredData.map((item, index) => ( - {index + 1} - {item.name} - {item.tahun} - {item.slug} - - diff --git a/src/app/api/[[...slugs]]/_lib/inovasi/index.ts b/src/app/api/[[...slugs]]/_lib/inovasi/index.ts index b4ebd4bf..66a57c55 100644 --- a/src/app/api/[[...slugs]]/_lib/inovasi/index.ts +++ b/src/app/api/[[...slugs]]/_lib/inovasi/index.ts @@ -2,6 +2,7 @@ import Elysia from "elysia"; import DesaDigital from "./desa-digital"; import ProgramKreatif from "./program-kreatif"; import KolaborasiInovasi from "./kolaborasi-inovasi"; +import InfoTekno from "./info-teknologi"; const Inovasi = new Elysia({ prefix: "/api/inovasi", @@ -10,5 +11,6 @@ const Inovasi = new Elysia({ .use(DesaDigital) .use(ProgramKreatif) .use(KolaborasiInovasi) + .use(InfoTekno) export default Inovasi; diff --git a/src/app/api/[[...slugs]]/_lib/inovasi/info-teknologi/create.ts b/src/app/api/[[...slugs]]/_lib/inovasi/info-teknologi/create.ts new file mode 100644 index 00000000..ac7dbd1b --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/inovasi/info-teknologi/create.ts @@ -0,0 +1,30 @@ +import prisma from "@/lib/prisma"; +import { Prisma } from "@prisma/client"; +import { Context } from "elysia"; + +type FormCreate = Prisma.InfoTeknoGetPayload<{ + select: { + name: true; + deskripsi: true; + imageId: true; + } +}> +export default async function infoTeknoCreate(context: Context){ + const body = context.body as FormCreate; + + await prisma.infoTekno.create({ + data: { + name: body.name, + deskripsi: body.deskripsi, + imageId: body.imageId, + } + }) + + return { + success: true, + message: "Success create info teknologi", + data: { + ...body, + } + } +} \ No newline at end of file diff --git a/src/app/api/[[...slugs]]/_lib/inovasi/info-teknologi/del.ts b/src/app/api/[[...slugs]]/_lib/inovasi/info-teknologi/del.ts new file mode 100644 index 00000000..065471d9 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/inovasi/info-teknologi/del.ts @@ -0,0 +1,54 @@ +import prisma from "@/lib/prisma"; +import { Context } from "elysia"; +import fs from "fs/promises"; +import path from "path"; + +export default async function infoTeknoDelete(context: Context) { + const id = context.params?.id as string; + + if (!id) { + return { + status: 400, + body: "ID tidak diberikan", + }; + } + + const infoTekno = await prisma.infoTekno.findUnique({ + where: { id }, + include: { + image: true, + }, + }); + + if (!infoTekno) { + return { + status: 404, + body: "Info teknologi tidak ditemukan", + }; + } + + if (infoTekno.image) { + try { + const filePath = path.join( + infoTekno.image.path, + infoTekno.image.name + ); + await fs.unlink(filePath); + await prisma.fileStorage.delete({ + where: { id: infoTekno.image.id }, + }); + } catch (error) { + console.error("Gagal hapus file image:", error); + } + } + + await prisma.infoTekno.delete({ + where: { id }, + }); + + return { + success: true, + message: "Info teknologi berhasil dihapus", + status: 200, + }; +} diff --git a/src/app/api/[[...slugs]]/_lib/inovasi/info-teknologi/findMany.ts b/src/app/api/[[...slugs]]/_lib/inovasi/info-teknologi/findMany.ts new file mode 100644 index 00000000..90311299 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/inovasi/info-teknologi/findMany.ts @@ -0,0 +1,23 @@ +import prisma from "@/lib/prisma"; + +export default async function infoTeknoFindMany() { + try { + const data = await prisma.infoTekno.findMany({ + include: { + image: true, + }, + }); + + return { + success: true, + message: "Success fetch info teknologi", + data, + }; + } catch (error) { + console.error("Find many error:", error); + return { + success: false, + message: "Failed fetch info teknologi", + }; + } +} \ No newline at end of file diff --git a/src/app/api/[[...slugs]]/_lib/inovasi/info-teknologi/findUnique.ts b/src/app/api/[[...slugs]]/_lib/inovasi/info-teknologi/findUnique.ts new file mode 100644 index 00000000..eb97ee1c --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/inovasi/info-teknologi/findUnique.ts @@ -0,0 +1,49 @@ +import prisma from "@/lib/prisma"; + +export default async function infoTeknoFindUnique(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 ditemukan", + }, {status: 400}); + } + + try { + if (typeof id !== 'string') { + return Response.json({ + success: false, + message: "ID tidak valid", + }, {status: 400}); + } + + const data = await prisma.infoTekno.findUnique({ + where: { id }, + include: { + image: true, + }, + }); + + if (!data) { + return Response.json({ + success: false, + message: "Info teknologi tidak ditemukan", + }, {status: 404}); + } + + return Response.json({ + success: true, + message: "Success fetch info teknologi by ID", + data, + }, {status: 200}); + } catch (error) { + console.error("Find by ID error:", error); + return Response.json({ + success: false, + message: "Gagal mengambil info teknologi: " + (error instanceof Error ? error.message : 'Unknown error'), + }, {status: 500}); + } +} \ No newline at end of file diff --git a/src/app/api/[[...slugs]]/_lib/inovasi/info-teknologi/index.ts b/src/app/api/[[...slugs]]/_lib/inovasi/info-teknologi/index.ts new file mode 100644 index 00000000..3506b886 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/inovasi/info-teknologi/index.ts @@ -0,0 +1,39 @@ +import Elysia, { t } from "elysia"; +import infoTeknoCreate from "./create"; +import infoTeknoDelete from "./del"; +import infoTeknoUpdate from "./updt"; +import infoTeknoFindUnique from "./findUnique"; +import infoTeknoFindMany from "./findMany"; + +const InfoTekno = new Elysia({ + prefix: "/infotekno", + tags: ["Inovasi/Info Tekno"], +}) + .post("/create", infoTeknoCreate, { + body: t.Object({ + name: t.String(), + deskripsi: t.String(), + imageId: t.String(), + }), + }) + .get("/find-many", infoTeknoFindMany) + .get("/:id", async (context) => { + const response = await infoTeknoFindUnique(context.request); + return response; + }) + .delete("/del/:id", infoTeknoDelete) + .put( + "/:id", + async (context) => { + const response = await infoTeknoUpdate(context); + return response; + }, + { + body: t.Object({ + name: t.String(), + deskripsi: t.String(), + imageId: t.String(), + }), + } + ); +export default InfoTekno; diff --git a/src/app/api/[[...slugs]]/_lib/inovasi/info-teknologi/updt.ts b/src/app/api/[[...slugs]]/_lib/inovasi/info-teknologi/updt.ts new file mode 100644 index 00000000..263d7e81 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/inovasi/info-teknologi/updt.ts @@ -0,0 +1,103 @@ +import prisma from "@/lib/prisma"; +import { Prisma } from "@prisma/client"; +import { Context } from "elysia"; +import path from "path"; +import fs from "fs/promises"; + +type FormUpdate = Prisma.InfoTeknoGetPayload<{ + select: { + id: true, + name: true, + deskripsi: true + imageId: true + } +}> +export default async function infoTeknoUpdate(context: Context) { + try { + const id = context.params?.id as string; + const body = (await context.body) as Omit; + + const { + name, + deskripsi, + imageId + } = body; + + if (!id) { + return new Response(JSON.stringify({ + success: false, + message: "ID tidak ditemukan", + }), { + status: 400, + headers: { + 'Content-Type': 'application/json' + } + }) + } + + const existing = await prisma.infoTekno.findUnique({ + where: {id}, + include: { + image: true, + } + }) + + if (!existing) { + return new Response(JSON.stringify({ + success: false, + message: "Info tekno tidak ditemukan", + }), { + status: 404, + headers: { + 'Content-Type': 'application/json' + } + }) + } + + if (existing.imageId && 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.infoTekno.update({ + where: { id }, + data: { + name, + deskripsi, + imageId, + } + }) + + return new Response(JSON.stringify({ + success: true, + message: "Info teknologi berhasil diupdate", + data: updated, + }), { + status: 200, + headers: { + 'Content-Type': 'application/json' + } + }) + } catch (error) { + console.error("Error updating info teknologi:", error); + return new Response(JSON.stringify({ + success: false, + message: "Terjadi kesalahan saat mengupdate info teknologi", + }), { + status: 500, + headers: { + 'Content-Type': 'application/json' + } + }) + } + } \ No newline at end of file diff --git a/src/app/api/[[...slugs]]/_lib/inovasi/kolaborasi-inovasi/findUnique.ts b/src/app/api/[[...slugs]]/_lib/inovasi/kolaborasi-inovasi/findUnique.ts index 72872731..5349bdcb 100644 --- a/src/app/api/[[...slugs]]/_lib/inovasi/kolaborasi-inovasi/findUnique.ts +++ b/src/app/api/[[...slugs]]/_lib/inovasi/kolaborasi-inovasi/findUnique.ts @@ -15,6 +15,9 @@ export default async function kolaborasiInovasiFindUnique(context: Context) { try { const kolaborasiInovasi = await prisma.kolaborasiInovasi.findUnique({ where: { id }, + include: { + image: true, + } }); if (!kolaborasiInovasi) {