diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7fd500e0..9e3c2fb6 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -125,6 +125,7 @@ model ProgramInovasi { model MediaSosial { id String @id @default(cuid()) + name String image FileStorage @relation(fields: [imageId], references: [id]) imageId String iconUrl String? @db.VarChar(255) diff --git a/public/assets/images/sosmed/telephone-call.png b/public/assets/images/sosmed/telephone-call.png new file mode 100644 index 00000000..effdf779 Binary files /dev/null and b/public/assets/images/sosmed/telephone-call.png differ diff --git a/src/app/admin/(dashboard)/_state/landing-page/profile.ts b/src/app/admin/(dashboard)/_state/landing-page/profile.ts index 2e06c04a..4088acd9 100644 --- a/src/app/admin/(dashboard)/_state/landing-page/profile.ts +++ b/src/app/admin/(dashboard)/_state/landing-page/profile.ts @@ -404,11 +404,13 @@ const pejabatDesa = proxy({ }); const templateMediaSosial = z.object({ + name: z.string().min(3, "Nama minimal 3 karakter"), imageId: z.string().min(1, "Gambar wajib dipilih"), iconUrl: z.string().min(3, "Icon URL minimal 3 karakter"), }); type MediaSosialForm = { + name: string; imageId: string; iconUrl: string; }; @@ -420,6 +422,7 @@ const mediaSosial = proxy({ async create() { // Ensure all required fields are non-null const formData = { + name: mediaSosial.create.form.name || "", imageId: mediaSosial.create.form.imageId || "", iconUrl: mediaSosial.create.form.iconUrl || "", }; @@ -467,6 +470,12 @@ const mediaSosial = proxy({ }; }> | null, async load(id: string) { + if (!id) { + toast.warn("ID tidak valid"); + return null; + } + + mediaSosial.update.loading = true; try { const res = await fetch(`/api/landingpage/mediasosial/${id}`); if (res.ok) { @@ -523,7 +532,9 @@ const mediaSosial = proxy({ toast.warn("ID tidak valid"); return null; } - + + mediaSosial.update.loading = true; // ✅ Tambahkan ini di awal + try { const response = await fetch(`/api/landingpage/mediasosial/${id}`, { method: "GET", @@ -531,18 +542,20 @@ const mediaSosial = proxy({ "Content-Type": "application/json", }, }); + if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } - + const result = await response.json(); - + if (result?.success) { const data = result.data; this.id = data.id; this.form = { - imageId: data.imageId, - iconUrl: data.iconUrl, + name: data.name || "", + imageId: data.imageId || "", + iconUrl: data.iconUrl || "", }; return data; } else { @@ -552,10 +565,10 @@ const mediaSosial = proxy({ console.error((error as Error).message); toast.error("Terjadi kesalahan saat mengambil data media sosial"); } finally { - mediaSosial.update.loading = false; + mediaSosial.update.loading = false; // ✅ Supaya berhenti loading walau error } }, - + async update() { const cek = templateMediaSosial.safeParse(mediaSosial.update.form); if (!cek.success) { @@ -575,6 +588,7 @@ const mediaSosial = proxy({ "Content-Type": "application/json", }, body: JSON.stringify({ + name: this.form.name, imageId: this.form.imageId, iconUrl: this.form.iconUrl, }), diff --git a/src/app/admin/(dashboard)/landing-page/profile/media-sosial/[id]/edit/page.tsx b/src/app/admin/(dashboard)/landing-page/profile/media-sosial/[id]/edit/page.tsx index 69da2f21..b2d00170 100644 --- a/src/app/admin/(dashboard)/landing-page/profile/media-sosial/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/profile/media-sosial/[id]/edit/page.tsx @@ -1,11 +1,174 @@ -import React from 'react'; +/* eslint-disable react-hooks/exhaustive-deps */ +'use client' +import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile'; +import colors from '@/con/colors'; +import ApiFetch from '@/lib/api-fetch'; +import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; +import { Dropzone } from '@mantine/dropzone'; +import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; +import { useParams, useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; + +function EditMediaSosial() { + const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial) + const router = useRouter(); + const params = useParams(); + + const [previewImage, setPreviewImage] = useState(null); + const [file, setFile] = useState(null); + const [formData, setFormData] = useState({ + name: stateMediaSosial.update.form.name || "", + iconUrl: stateMediaSosial.update.form.iconUrl || "", + imageId: stateMediaSosial.update.form.imageId || "" + }) + + useEffect(() => { + const id = params?.id as string; + if (!id) return; + + const loadMediaSosial = async () => { + try { + const data = await stateMediaSosial.update.load(id); + + if (data) { + setFormData({ + name: data.name || "", + iconUrl: data.iconUrl || "", + imageId: data.imageId || "", + }); + // Tampilkan preview gambar + if (data.image?.link) { + setPreviewImage(data.image.link); + } + } + } catch (error) { + console.error("Error loading program inovasi:", error); + toast.error( + error instanceof Error ? error.message : "Gagal mengambil data program inovasi" + ); + } + } + + loadMediaSosial(); + }, [params?.id]); + + const handleSubmit = async () => { + try { + stateMediaSosial.update.form = { + ...stateMediaSosial.update.form, + name: formData.name, + iconUrl: formData.iconUrl, + 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"); + } + + // Update imageId in global state + stateMediaSosial.update.form.imageId = uploaded.id; + } + + await stateMediaSosial.update.update(); + toast.success("Media Sosial berhasil diperbarui!"); + router.push("/admin/landing-page/profile/media-sosial"); + } catch (error) { + console.error("Error updating media sosial:", error); + toast.error("Terjadi kesalahan saat memperbarui media sosial"); + } + }; -function Page() { return ( -
- Page -
+ + + + + + + + Edit Media Sosial + + Gambar + + { + const selectedFile = files[0]; // Ambil file pertama + if (selectedFile) { + setFile(selectedFile); + setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview + } + }} + onReject={() => toast.error('File tidak valid.')} + maxSize={5 * 1024 ** 2} // Maks 5MB + accept={{ 'image/*': [] }} + > + + + + + + + + + + + +
+ + Drag gambar ke sini atau klik untuk pilih file + + + Maksimal 5MB dan harus format gambar + +
+
+
+ + {/* Tampilkan preview kalau ada */} + {previewImage && ( + + Preview + + )} + +
+
+ setFormData({ ...formData, name: e.target.value })} + label={Nama Media Sosial / Nama Kontak} + placeholder='Masukkan nama media sosial' + /> + setFormData({ ...formData, iconUrl: e.target.value })} + label={Icon URL / No Telephone} + placeholder='Masukkan icon url' + /> + + + +
+
+
); } -export default Page; +export default EditMediaSosial; diff --git a/src/app/admin/(dashboard)/landing-page/profile/media-sosial/[id]/page.tsx b/src/app/admin/(dashboard)/landing-page/profile/media-sosial/[id]/page.tsx index 69da2f21..1cdfc9fc 100644 --- a/src/app/admin/(dashboard)/landing-page/profile/media-sosial/[id]/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/profile/media-sosial/[id]/page.tsx @@ -1,11 +1,107 @@ -import React from 'react'; +'use client' +import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; +import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile'; +import colors from '@/con/colors'; +import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; +import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; +import { useParams, useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; + +function DetailMediaSosial() { + const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial) + const [modalHapus, setModalHapus] = useState(false) + const [selectedId, setSelectedId] = useState(null) + const params = useParams() + const router = useRouter(); + + useShallowEffect(() => { + stateMediaSosial.findUnique.load(params?.id as string) + }, []) + + const handleHapus = () => { + if (selectedId) { + stateMediaSosial.delete.byId(selectedId) + setModalHapus(false) + setSelectedId(null) + router.push("/admin/landing-page/profile/media-sosial") + } + } + + if (!stateMediaSosial.findUnique.data) { + return ( + + + + ) + } -function Page() { return ( -
- Page -
+ + + + + + + Detail Media Sosial + + + + Nama Media Sosial / Nama Kontak + {stateMediaSosial.findUnique.data?.name} + + + Icon URL / No Telephone + {stateMediaSosial.findUnique.data?.iconUrl} + + + Gambar + + gambar + + + + + + + + + + + + + + {/* Modal Hapus */} + setModalHapus(false)} + onConfirm={handleHapus} + text="Apakah anda yakin ingin menghapus media sosial ini?" + /> + ); } -export default Page; +export default DetailMediaSosial; diff --git a/src/app/admin/(dashboard)/landing-page/profile/media-sosial/create/page.tsx b/src/app/admin/(dashboard)/landing-page/profile/media-sosial/create/page.tsx index 69da2f21..f4299976 100644 --- a/src/app/admin/(dashboard)/landing-page/profile/media-sosial/create/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/profile/media-sosial/create/page.tsx @@ -1,11 +1,148 @@ -import React from 'react'; +/* eslint-disable react-hooks/exhaustive-deps */ +'use client' +import colors from '@/con/colors'; +import ApiFetch from '@/lib/api-fetch'; +import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; +import { Dropzone } from '@mantine/dropzone'; +import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; +import profileLandingPageState from '../../../../_state/landing-page/profile'; -function Page() { +function CreateMediaSosial() { + const router = useRouter(); + const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial) + const [previewImage, setPreviewImage] = useState(null); + const [file, setFile] = useState(null); + + useEffect(() => { + stateMediaSosial.findMany.load(); + }, []); + + const resetForm = () => { + stateMediaSosial.create.form = { + name: "", + imageId: "", + iconUrl: "", + }; + setPreviewImage(null); + setFile(null); + }; + const handleSubmit = async () => { + if (!file) { + return toast.warn("Pilih file gambar terlebih dahulu"); + } + + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }) + + const uploaded = res.data?.data; + + if (!uploaded?.id) { + return toast.error("Gagal mengupload file"); + } + + stateMediaSosial.create.form.imageId = uploaded.id; + + await stateMediaSosial.create.create(); + + resetForm(); + router.push("/admin/landing-page/profile/media-sosial") + } return ( -
- Page -
+ + + + + + + + Create Media Sosial + + Gambar + + { + const selectedFile = files[0]; // Ambil file pertama + if (selectedFile) { + setFile(selectedFile); + setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview + } + }} + onReject={() => toast.error('File tidak valid.')} + maxSize={5 * 1024 ** 2} // Maks 5MB + accept={{ 'image/*': [] }} + > + + + + + + + + + + + +
+ + Drag gambar ke sini atau klik untuk pilih file + + + Maksimal 5MB dan harus format gambar + +
+
+
+ + {/* Tampilkan preview kalau ada */} + {previewImage && ( + + Preview + + )} + +
+
+ { + stateMediaSosial.create.form.name = val.target.value; + }} + label={Nama Media Sosial / Nama Kontak} + placeholder='Masukkan nama media sosial / nama kontak' + /> + { + stateMediaSosial.create.form.iconUrl = val.target.value; + }} + label={Link Media Sosial / No Telephone} + placeholder='Masukkan link media sosial / no telephone' + /> + + + +
+
+
); } -export default Page; +export default CreateMediaSosial; diff --git a/src/app/admin/(dashboard)/landing-page/profile/media-sosial/page.tsx b/src/app/admin/(dashboard)/landing-page/profile/media-sosial/page.tsx index c38efbfd..6c314735 100644 --- a/src/app/admin/(dashboard)/landing-page/profile/media-sosial/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/profile/media-sosial/page.tsx @@ -1,9 +1,93 @@ -import { Box, Title } from "@mantine/core"; +'use client' +import colors from '@/con/colors'; +import { Box, Button, Image, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; +import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; +import HeaderSearch from '../../../_com/header'; +import JudulList from '../../../_com/judulList'; +import profileLandingPageState from '../../../_state/landing-page/profile'; -export default function MediaSosial() { +function MediaSosial() { + const [search, setSearch] = useState(""); + return ( + + } + value={search} + onChange={(e) => setSearch(e.currentTarget.value)} + /> + + + ); +} + +function ListMediaSosial({ search }: { search: string }) { + const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial) + const router = useRouter(); + + useShallowEffect(() => { + stateMediaSosial.findMany.load() + }, []) + + const filteredData = (stateMediaSosial.findMany.data || []).filter(item => { + const keyword = search.toLowerCase(); return ( - - Media Sosial - + item.name.toLowerCase().includes(keyword) || + item.iconUrl?.toLowerCase().includes(keyword) + ); + }); + + if (!stateMediaSosial.findMany.data) { + return ( + + + ) -} \ No newline at end of file + } + + return ( + + + + + + + Nama Media Sosial / Nama Kontak + Image + Icon URL / No Telephone + Detail + + + + {filteredData.map((item) => ( + + {item.name} + + + + + + {item.iconUrl} + + + + + ))} + +
+
+
+ ); +} + +export default MediaSosial; diff --git a/src/app/admin/(dashboard)/landing-page/profile/program-inovasi/[id]/edit/page.tsx b/src/app/admin/(dashboard)/landing-page/profile/program-inovasi/[id]/edit/page.tsx index 3c56680d..169bbf6d 100644 --- a/src/app/admin/(dashboard)/landing-page/profile/program-inovasi/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/profile/program-inovasi/[id]/edit/page.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react-hooks/exhaustive-deps */ 'use client' import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile'; import colors from '@/con/colors'; @@ -52,7 +53,7 @@ function EditProgramInovasi() { } loadProgramInovasi(); - }, [params?.id, stateProgramInovasi.update]); + }, [params?.id]); const handleSubmit = async () => { try { diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/profile/media-sosial/create.ts b/src/app/api/[[...slugs]]/_lib/landing_page/profile/media-sosial/create.ts index 9e5f5c66..a545be3e 100644 --- a/src/app/api/[[...slugs]]/_lib/landing_page/profile/media-sosial/create.ts +++ b/src/app/api/[[...slugs]]/_lib/landing_page/profile/media-sosial/create.ts @@ -2,6 +2,7 @@ import prisma from "@/lib/prisma"; import { Context } from "elysia"; type FormCreate = { + name: string; imageId: string; iconUrl: string; }; @@ -12,6 +13,7 @@ export default async function mediaSosialCreate(context: Context) { try { const result = await prisma.mediaSosial.create({ data: { + name: body.name, imageId: body.imageId, iconUrl: body.iconUrl, }, diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/profile/media-sosial/index.ts b/src/app/api/[[...slugs]]/_lib/landing_page/profile/media-sosial/index.ts index f4de76e3..6972337f 100644 --- a/src/app/api/[[...slugs]]/_lib/landing_page/profile/media-sosial/index.ts +++ b/src/app/api/[[...slugs]]/_lib/landing_page/profile/media-sosial/index.ts @@ -19,6 +19,7 @@ const MediaSosial = new Elysia({ // ✅ Create .post("/create", MediaSosialCreate, { body: t.Object({ + name: t.String(), imageId: t.String(), iconUrl: t.String(), }), @@ -27,6 +28,7 @@ const MediaSosial = new Elysia({ // ✅ Update .put("/:id", MediaSosialUpdate, { body: t.Object({ + name: t.String(), imageId: t.Optional(t.String()), iconUrl: t.Optional(t.String()), }), diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/profile/media-sosial/updt.ts b/src/app/api/[[...slugs]]/_lib/landing_page/profile/media-sosial/updt.ts index 585d2a7f..5a5ca174 100644 --- a/src/app/api/[[...slugs]]/_lib/landing_page/profile/media-sosial/updt.ts +++ b/src/app/api/[[...slugs]]/_lib/landing_page/profile/media-sosial/updt.ts @@ -3,6 +3,7 @@ import prisma from "@/lib/prisma"; import { Context } from "elysia"; type FormUpdateMediaSosial = { + name?: string; imageId?: string; iconUrl?: string; }; @@ -23,6 +24,7 @@ export default async function mediaSosialUpdate(context: Context) { const updated = await prisma.mediaSosial.update({ where: { id }, data: { + name: body.name, imageId: body.imageId, iconUrl: body.iconUrl, },