diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c1bbd2d3..0f57a8c9 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -61,7 +61,8 @@ model FileStorage { isActive Boolean @default(true) link String category String // "image" / "document" / "audio" / "other" - Berita Berita[] + Berita Berita[] @relation("BeritaFeaturedImage") + BeritaImages Berita[] @relation("BeritaImages") PotensiDesa PotensiDesa[] Posyandu Posyandu[] StrukturPPID StrukturPPID[] @@ -612,15 +613,19 @@ model Berita { id String @id @default(cuid()) judul String deskripsi String - image FileStorage? @relation(fields: [imageId], references: [id]) + image FileStorage? @relation("BeritaFeaturedImage", fields: [imageId], references: [id]) imageId String? + images FileStorage[] @relation("BeritaImages") content String @db.Text + linkVideo String? @db.VarChar(500) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime @default(now()) isActive Boolean @default(true) kategoriBerita KategoriBerita? @relation(fields: [kategoriBeritaId], references: [id]) kategoriBeritaId String? + + @@index([kategoriBeritaId]) } model KategoriBerita { diff --git a/src/app/admin/(dashboard)/_state/desa/berita.ts b/src/app/admin/(dashboard)/_state/desa/berita.ts index 0a7dc17e..b4ba99b8 100644 --- a/src/app/admin/(dashboard)/_state/desa/berita.ts +++ b/src/app/admin/(dashboard)/_state/desa/berita.ts @@ -12,6 +12,8 @@ const templateForm = z.object({ content: z.string().min(3, "Content minimal 3 karakter"), kategoriBeritaId: z.string().nonempty(), imageId: z.string().nonempty(), + imageIds: z.array(z.string()), + linkVideo: z.string().optional(), }); // 2. Default value form berita (hindari uncontrolled input) @@ -21,6 +23,8 @@ const defaultForm = { imageId: "", content: "", kategoriBeritaId: "", + imageIds: [] as string[], + linkVideo: "", }; // 4. Berita proxy @@ -62,14 +66,7 @@ const berita = proxy({ // State untuk berita utama (hanya 1) findMany: { - data: null as - | Prisma.BeritaGetPayload<{ - include: { - image: true; - kategoriBerita: true; - }; - }>[] - | null, + data: null as any[] | null, page: 1, totalPages: 1, loading: false, @@ -79,14 +76,14 @@ const berita = proxy({ berita.findMany.loading = true; berita.findMany.page = page; berita.findMany.search = search; - + try { const query: any = { page, limit }; if (search) query.search = search; if (kategori) query.kategori = kategori; - + const res = await ApiFetch.api.desa.berita["find-many"].get({ query }); - + if (res.status === 200 && res.data?.success) { berita.findMany.data = res.data.data ?? []; berita.findMany.totalPages = res.data.totalPages ?? 1; @@ -103,18 +100,19 @@ const berita = proxy({ const elapsed = Date.now() - startTime; const minDelay = 300; const delay = elapsed < minDelay ? minDelay - elapsed : 0; - + setTimeout(() => { berita.findMany.loading = false; }, delay); } - }, + }, }, findUnique: { data: null as Prisma.BeritaGetPayload<{ include: { image: true; + images: true; kategoriBerita: true; }; }> | null, @@ -199,6 +197,8 @@ const berita = proxy({ content: data.content, kategoriBeritaId: data.kategoriBeritaId || "", imageId: data.imageId || "", + imageIds: data.images?.map((img: any) => img.id) || [], + linkVideo: data.linkVideo || "", }; return data; // Return the loaded data } else { @@ -237,6 +237,8 @@ const berita = proxy({ content: this.form.content, kategoriBeritaId: this.form.kategoriBeritaId || null, imageId: this.form.imageId, + imageIds: this.form.imageIds, + linkVideo: this.form.linkVideo, }), }); diff --git a/src/app/admin/(dashboard)/desa/berita/list-berita/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/berita/list-berita/[id]/edit/page.tsx index 414e2749..b98162f5 100644 --- a/src/app/admin/(dashboard)/desa/berita/list-berita/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/desa/berita/list-berita/[id]/edit/page.tsx @@ -9,6 +9,8 @@ import { ActionIcon, Box, Button, + Card, + Grid, Group, Image, Paper, @@ -17,7 +19,7 @@ import { Text, TextInput, Title, - Loader + Loader, } from "@mantine/core"; import { Dropzone } from "@mantine/dropzone"; import { @@ -25,19 +27,51 @@ import { IconPhoto, IconUpload, IconX, + IconVideo, + IconTrash, } 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 { convertYoutubeUrlToEmbed } from '@/app/admin/(dashboard)/desa/gallery/lib/youtube-utils'; + +interface ExistingImage { + id: string; + link: string; + name: string; +} + +interface BeritaData { + id: string; + judul: string; + deskripsi: string; + content: string; + kategoriBeritaId: string | null; + imageId: string | null; + image?: { link: string } | null; + images?: ExistingImage[]; + linkVideo?: string | null; +} function EditBerita() { const beritaState = useProxy(stateDashboardBerita); const router = useRouter(); const params = useParams(); + // Featured image state const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); + + // Gallery images state + const [existingGalleryImages, setExistingGalleryImages] = useState([]); + const [galleryFiles, setGalleryFiles] = useState([]); + const [galleryPreviews, setGalleryPreviews] = useState([]); + + // YouTube link state + const [youtubeLink, setYoutubeLink] = useState(''); + const [originalYoutubeLink, setOriginalYoutubeLink] = useState(''); + const [formData, setFormData] = useState({ judul: "", deskripsi: "", @@ -48,9 +82,17 @@ function EditBerita() { const [isSubmitting, setIsSubmitting] = useState(false); + const [originalData, setOriginalData] = useState({ + judul: "", + deskripsi: "", + kategoriBeritaId: "", + content: "", + imageId: "", + imageUrl: "" + }); + // Helper function to check if HTML content is empty const isHtmlEmpty = (html: string) => { - // Remove all HTML tags and check if there's any text content const textContent = html.replace(/<[^>]*>/g, '').trim(); return textContent === ''; }; @@ -61,21 +103,12 @@ function EditBerita() { formData.judul?.trim() !== '' && formData.kategoriBeritaId !== '' && !isHtmlEmpty(formData.deskripsi) && - (file !== null || originalData.imageId !== '') && // Either a new file is selected or an existing image exists + (file !== null || originalData.imageId !== '') && !isHtmlEmpty(formData.content) ); }; - const [originalData, setOriginalData] = useState({ - judul: "", - deskripsi: "", - kategoriBeritaId: "", - content: "", - imageId: "", - imageUrl: "" - }); - - // Load kategori + berita + // Load data useEffect(() => { beritaState.kategoriBerita.findMany.load(); @@ -84,7 +117,7 @@ function EditBerita() { if (!id) return; try { - const data = await stateDashboardBerita.berita.edit.load(id); + const data = await stateDashboardBerita.berita.edit.load(id) as BeritaData | null; if (data) { setFormData({ judul: data.judul || "", @@ -106,6 +139,17 @@ function EditBerita() { if (data?.image?.link) { setPreviewImage(data.image.link); } + + // Load gallery images + if (data?.images && data.images.length > 0) { + setExistingGalleryImages(data.images); + } + + // Load YouTube link + if (data?.linkVideo) { + setYoutubeLink(data.linkVideo); + setOriginalYoutubeLink(data.linkVideo); + } } } catch (error) { console.error("Error loading berita:", error); @@ -120,27 +164,59 @@ function EditBerita() { setFormData((prev) => ({ ...prev, [field]: value })); }; + const handleGalleryDrop = (files: File[]) => { + const maxImages = 10; + const currentCount = existingGalleryImages.length + galleryFiles.length; + const availableSlots = maxImages - currentCount; + + if (availableSlots <= 0) { + toast.warn('Maksimal 10 gambar untuk galeri'); + return; + } + + const newFiles = files.slice(0, availableSlots); + + if (newFiles.length === 0) { + toast.warn('Tidak ada slot tersisa untuk gambar galeri'); + return; + } + + setGalleryFiles([...galleryFiles, ...newFiles]); + + const newPreviews = newFiles.map((f) => URL.createObjectURL(f)); + setGalleryPreviews([...galleryPreviews, ...newPreviews]); + }; + + const removeGalleryImage = (index: number, isExisting: boolean = false) => { + if (isExisting) { + setExistingGalleryImages(existingGalleryImages.filter((_, i) => i !== index)); + } else { + setGalleryFiles(galleryFiles.filter((_, i) => i !== index)); + setGalleryPreviews(galleryPreviews.filter((_, i) => i !== index)); + } + }; + const handleSubmit = async () => { if (!formData.judul?.trim()) { toast.error('Judul wajib diisi'); return; } - + if (!formData.kategoriBeritaId) { toast.error('Kategori wajib dipilih'); return; } - + if (isHtmlEmpty(formData.deskripsi)) { toast.error('Deskripsi singkat wajib diisi'); return; } - + if (!file && !originalData.imageId) { - toast.error('Gambar wajib dipilih'); + toast.error('Gambar utama wajib dipilih'); return; } - + if (isHtmlEmpty(formData.content)) { toast.error('Konten wajib diisi'); return; @@ -148,12 +224,14 @@ function EditBerita() { try { setIsSubmitting(true); - // Update global state hanya sekali di sini + + // Update global state beritaState.berita.edit.form = { ...beritaState.berita.edit.form, ...formData, }; + // Upload new featured image if changed if (file) { const res = await ApiFetch.api.fileStorage.create.post({ file, @@ -162,12 +240,33 @@ function EditBerita() { const uploaded = res.data?.data; if (!uploaded?.id) { - return toast.error("Gagal upload gambar"); + return toast.error("Gagal upload gambar utama"); } beritaState.berita.edit.form.imageId = uploaded.id; } + // Upload new gallery images + const newGalleryIds: string[] = []; + for (const galleryFile of galleryFiles) { + const galleryRes = await ApiFetch.api.fileStorage.create.post({ + file: galleryFile, + name: galleryFile.name, + }); + const galleryUploaded = galleryRes.data?.data; + if (galleryUploaded?.id) { + newGalleryIds.push(galleryUploaded.id); + } + } + + // Combine existing (not removed) and new gallery images + const remainingExistingIds = existingGalleryImages.map(img => img.id); + beritaState.berita.edit.form.imageIds = [...remainingExistingIds, ...newGalleryIds]; + + // Set YouTube link + const embedLink = convertYoutubeUrlToEmbed(youtubeLink); + beritaState.berita.edit.form.linkVideo = embedLink || ''; + await beritaState.berita.edit.update(); toast.success("Berita berhasil diperbarui!"); router.push("/admin/desa/berita/list-berita"); @@ -189,9 +288,12 @@ function EditBerita() { }); setPreviewImage(originalData.imageUrl || null); setFile(null); + setYoutubeLink(originalYoutubeLink); toast.info("Form dikembalikan ke data awal"); }; + const embedLink = convertYoutubeUrlToEmbed(youtubeLink); + return ( {/* Header */} @@ -219,6 +321,7 @@ function EditBerita() { style={{ border: "1px solid #e0e0e0" }} > + {/* Judul */} + {/* Kategori */} + {/* Deskripsi */} Deskripsi Singkat @@ -194,9 +248,10 @@ export default function CreateBerita() { /> + {/* Featured Image */} - Gambar Berita + Gambar Utama (Featured) { @@ -232,17 +287,11 @@ export default function CreateBerita() { Preview Gambar - - {/* Tombol hapus (pojok kanan atas) */} @@ -265,6 +312,102 @@ export default function CreateBerita() { )} + {/* Gallery Images */} + + + Galeri Gambar (Opsional - Maksimal 10) + + toast.error('File tidak valid, gunakan format gambar')} + maxSize={5 * 1024 ** 2} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} + radius="md" + p="md" + multiple + > + + + + + + + + + + + + + Seret gambar atau klik untuk menambahkan ke galeri + + + + {galleryPreviews.length > 0 && ( + + {galleryPreviews.map((preview, index) => ( + + + {`Gallery + removeGalleryImage(index)} + style={{ boxShadow: '0 2px 6px rgba(0,0,0,0.15)' }} + > + + + + + ))} + + )} + + + {/* YouTube Video */} + + + Link Video YouTube (Opsional) + + setYoutubeLink(e.currentTarget.value)} + leftSection={} + rightSection={ + youtubeLink && ( + setYoutubeLink('')} + > + + + ) + } + /> + {embedLink && ( + +