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..df492b39 --- /dev/null +++ b/src/app/admin/(dashboard)/desa/gallery/foto/[id]/edit/page.tsx @@ -0,0 +1,303 @@ +/* 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 ApiFetch from "@/lib/api-fetch"; +import { + ActionIcon, + Box, + Button, + Group, + Image, + Loader, + 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 EditFoto() { + const FotoState = useProxy(stateGallery.foto); + const router = useRouter(); + const params = useParams(); + + const [previewImage, setPreviewImage] = useState(null); + const [file, setFile] = useState(null); + const [formData, setFormData] = useState({ + name: "", + deskripsi: "", + imagesId: "", + }); + + const [isSubmitting, setIsSubmitting] = useState(false); + + const [originalData, setOriginalData] = useState({ + name: "", + deskripsi: "", + imagesId: "", + imageUrl: "", + }); + + // Load kategori + Foto + useEffect(() => { + FotoState.findMany.load(); + + const loadFoto = async () => { + const id = params?.id as string; + if (!id) return; + + try { + const data = await FotoState.update.load(id); + if (data) { + setFormData({ + name: data.name || "", + deskripsi: data.deskripsi || "", + imagesId: data.imagesId || "", + }); + + setOriginalData({ + name: data.name || "", + deskripsi: data.deskripsi || "", + imagesId: data.imagesId || "", + imageUrl: data.imageGalleryFoto?.link || "" + }); + + 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 handleChange = (field: string, value: string) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }; + + const handleSubmit = async () => { + try { + setIsSubmitting(true); + // Update global state hanya sekali di sini + FotoState.update.form = { + ...FotoState.update.form, + ...formData, + }; + + 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"); + } finally { + setIsSubmitting(false); + } + }; + + const handleResetForm = () => { + setFormData({ + name: originalData.name, + deskripsi: originalData.deskripsi, + imagesId: originalData.imagesId, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info("Form dikembalikan ke data awal"); + }; + + return ( + + {/* Header */} + + + + Edit Foto + + + + {/* Form */} + + + handleChange("name", e.target.value)} + required + /> + + {/* Upload Gambar */} + + + Gambar Foto + + { + const selectedFile = files[0]; + if (selectedFile) { + setFile(selectedFile); + setPreviewImage(URL.createObjectURL(selectedFile)); + } + }} + onReject={() => + toast.error("File tidak valid, gunakan format gambar") + } + maxSize={5 * 1024 ** 2} + accept={{ "image/*": [] }} + radius="md" + p="xl" + > + + + + + + + + + + + + + Seret gambar atau klik untuk memilih file + + + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp + + + + + + {previewImage && ( + + Preview Gambar + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + + + )} + + + {/* Deskripsi */} + + + Deskripsi Foto + + + setFormData((prev) => ({ ...prev, deskripsi: htmlContent })) + } + /> + + + {/* Action */} + + {/* Tombol Batal */} + + + {/* Tombol Simpan */} + + + + + + ); +} + +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..cc9f5b27 --- /dev/null +++ b/src/app/admin/(dashboard)/desa/gallery/foto/[id]/page.tsx @@ -0,0 +1,175 @@ +'use client'; + +import { Box, Button, Group, Paper, Skeleton, Stack, Text, Alert } from '@mantine/core'; +import Image from 'next/image'; +import { useShallowEffect } from '@mantine/hooks'; +import { IconArrowBack, IconEdit, IconTrash, IconPhoto } from '@tabler/icons-react'; +import { useParams, useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; + +import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; +import colors from '@/con/colors'; +import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery'; + +function DetailFoto() { + const FotoState = useProxy(stateGallery.foto); + const [modalHapus, setModalHapus] = useState(false); + const [selectedId, setSelectedId] = useState(null); + const [imageError, setImageError] = useState(false); + 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 ( + + + + ); + } + + const data = FotoState.findUnique.data; + const imageUrl = data.imageGalleryFoto?.link; + + return ( + + + + + + + Detail Foto + + + + + + Judul Foto + {data.name || '-'} + + + + Deskripsi + + + + + Gambar + {imageUrl ? ( + + {data.name setImageError(true)} + /> + + ) : imageError ? ( + } + title="Gagal memuat gambar" + radius="md" + > + Gambar tidak dapat ditampilkan. + + ) : ( + Tidak ada gambar + )} + + + {/* Action Buttons */} + + + + + + + + + + + setModalHapus(false)} + onConfirm={handleHapus} + text="Apakah Anda yakin ingin menghapus foto ini?" + /> + + ); +} + +export default DetailFoto; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/desa/gallery/foto/create/page.tsx b/src/app/admin/(dashboard)/desa/gallery/foto/create/page.tsx new file mode 100644 index 00000000..82cd87ef --- /dev/null +++ b/src/app/admin/(dashboard)/desa/gallery/foto/create/page.tsx @@ -0,0 +1,228 @@ +'use client'; +import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; +import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery'; +import colors from '@/con/colors'; +import ApiFetch from '@/lib/api-fetch'; +import { + ActionIcon, + Box, + Button, + Group, + Paper, + Stack, + Text, + TextInput, + Title, + Loader, + Image +} from '@mantine/core'; +import { Dropzone } from '@mantine/dropzone'; +import { IconArrowBack, IconPhoto, IconUpload, IconX } 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 [isSubmitting, setIsSubmitting] = useState(false); + 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 () => { + try { + setIsSubmitting(true); + if (!file) { + return toast.warn('Silakan 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 mengunggah gambar, silakan coba lagi'); + } + + FotoState.create.form.imagesId = uploaded.id; + + await FotoState.create.create(); + + resetForm(); + router.push('/admin/desa/gallery/foto'); + } catch (error) { + console.error('Error creating foto:', error); + toast.error('Terjadi kesalahan saat membuat foto'); + } finally { + setIsSubmitting(false); + } + }; + + return ( + + {/* Header Back Button + Title */} + + + + Tambah Foto + + + + {/* Card Form */} + + + {/* Judul */} + { + FotoState.create.form.name = e.currentTarget.value; + }} + required + /> + + + + Gambar Berita + + { + const selectedFile = files[0]; + if (selectedFile) { + setFile(selectedFile); + setPreviewImage(URL.createObjectURL(selectedFile)); + } + }} + onReject={() => toast.error('File tidak valid, gunakan format gambar')} + maxSize={5 * 1024 ** 2} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} + radius="md" + p="xl" + > + + + + + + + + + + + + + Seret gambar atau klik untuk memilih file (maks 5MB) + + + + {previewImage && ( + + Preview Gambar + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + + + )} + + + {/* Deskripsi */} + + + Deskripsi Foto + + { + FotoState.create.form.deskripsi = val; + }} + /> + + + {/* Button Submit */} + + + + {/* Tombol Simpan */} + + + + + + ); +} + +export default CreateFoto; diff --git a/src/app/admin/(dashboard)/desa/gallery/foto/page.tsx b/src/app/admin/(dashboard)/desa/gallery/foto/page.tsx index ac66a2df..c3de59ff 100644 --- a/src/app/admin/(dashboard)/desa/gallery/foto/page.tsx +++ b/src/app/admin/(dashboard)/desa/gallery/foto/page.tsx @@ -1,157 +1,163 @@ -"use client"; -import stateFileStorage from "@/state/state-list-image"; +'use client' +import colors from '@/con/colors'; import { - ActionIcon, Box, - Card, - Flex, + Button, + Center, Group, - Image, Pagination, Paper, - SimpleGrid, + Skeleton, Stack, + Table, + TableTbody, + TableTd, + TableTh, + TableThead, + TableTr, Text, - TextInput, Title -} from "@mantine/core"; -import { useShallowEffect } from "@mantine/hooks"; -import { IconSearch, IconTrash, IconX } from "@tabler/icons-react"; -import { motion } from "framer-motion"; -import toast from "react-simple-toasts"; -import { useSnapshot } from "valtio"; - -export default function ListImage() { - const { list, total } = useSnapshot(stateFileStorage); - - useShallowEffect(() => { - stateFileStorage.load(); - }, []); - - let timeOut: NodeJS.Timer; +} from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; +import { IconDeviceImac, IconPlus, 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 stateGallery from '../../../_state/desa/gallery'; +function Foto() { + const [search, setSearch] = useState(""); return ( - - - - Galeri Foto - - } - rightSection={ - stateFileStorage.load()} - > - - - } - onChange={(e) => { - if (timeOut) clearTimeout(timeOut); - timeOut = setTimeout(() => { - stateFileStorage.load({ search: e.target.value }); - }, 300); - }} - /> - - - - {list && list.length > 0 ? ( - - {list.map((v, k) => ( - - - { - navigator.clipboard.writeText(v.url); - toast("Tautan foto berhasil disalin"); - }} - whileHover={{ scale: 1.05 }} - whileTap={{ scale: 0.95 }} - style={{ cursor: "pointer" }} - > - {v.name} - - - - - {v.name} - - - - - { - stateFileStorage - .del({ id: v.id }) - .finally(() => toast("Foto berhasil dihapus")); - }} - > - - - - - - ))} - - ) : ( - - Kosong - - Belum ada foto yang tersedia - - - )} - - - {total && total > 1 && ( - - { - stateFileStorage.load({ page }); - }} - /> - - - )} - + + } + value={search} + onChange={(e) => setSearch(e.currentTarget.value)} + /> + + ); } + +function ListFoto({ search }: { search: string }) { + const FotoState = useProxy(stateGallery.foto) + const router = useRouter(); + + const { + data, + page, + totalPages, + loading, + load, + } = FotoState.findMany; + + useShallowEffect(() => { + load(page, 10, search) + }, [page, search]) + + const filteredData = data || [] + + if (loading || !data) { + return ( + + + + ) + } + + return ( + + + + Daftar Foto + + + + + + + Judul Foto + Tanggal + Deskripsi + Aksi + + + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + {item.name} + + + + + + {new Date(item.createdAt).toLocaleDateString('id-ID', { + day: 'numeric', + month: 'long', + year: 'numeric', + })} + + + + + + + + + + + + + )) + ) : ( + + +
+ Tidak ada foto yang cocok +
+
+
+ )} +
+
+
+
+
+ { + load(newPage, 10) + window.scrollTo({ top: 0, behavior: 'smooth' }) + }} + total={totalPages} + mt="md" + mb="md" + color="blue" + radius="md" + /> +
+
+ ); +} + +export default Foto; diff --git a/src/app/darmasaba/(pages)/desa/berita/[kategori]/[id]/page.tsx b/src/app/darmasaba/(pages)/desa/berita/[kategori]/[id]/page.tsx index 858f739a..efcbd850 100644 --- a/src/app/darmasaba/(pages)/desa/berita/[kategori]/[id]/page.tsx +++ b/src/app/darmasaba/(pages)/desa/berita/[kategori]/[id]/page.tsx @@ -49,7 +49,7 @@ function Page() { return ( - + diff --git a/src/app/darmasaba/(pages)/desa/berita/layout.tsx b/src/app/darmasaba/(pages)/desa/berita/layout.tsx index 861cea95..83e56643 100644 --- a/src/app/darmasaba/(pages)/desa/berita/layout.tsx +++ b/src/app/darmasaba/(pages)/desa/berita/layout.tsx @@ -1,12 +1,44 @@ -// app/desa/berita/BeritaLayoutClient.tsx -'use client' -import dynamic from 'next/dynamic'; +// app/darmasaba/(pages)/desa/berita/layout.tsx +'use client'; +import { usePathname } from 'next/navigation'; +import { ReactNode } from 'react'; +import dynamic from 'next/dynamic'; +import { Box } from '@mantine/core'; +import BackButton from '../layanan/_com/BackButto'; +import colors from '@/con/colors'; const LayoutTabsBerita = dynamic( () => import('./_lib/layoutTabs'), { ssr: false } ); -export default function BeritaLayoutClient({ children }: { children: React.ReactNode }) { +export default function BeritaLayoutClient({ + children, +}: { + children: ReactNode; +}) { + const pathname = usePathname(); + + // Contoh path: + // - /darmasaba/desa/berita/semua → panjang 5 → list + // - /darmasaba/desa/berita/Pemerintahan → panjang 5 → list + // - /darmasaba/desa/berita/Pemerintahan/123 → panjang 6 → detail + + const segments = pathname.split('/').filter(Boolean); + const isDetailPage = segments.length === 5; // [darmasaba, desa, berita, kategori, id] + + if (isDetailPage) { + // Tampilkan tanpa tab menu + return ( + + + + + {children} + + ); + } + + // Tampilkan dengan tab menu (untuk /semua atau /kategori) return {children}; } \ No newline at end of file diff --git a/src/app/darmasaba/(pages)/desa/galery/foto/Content.tsx b/src/app/darmasaba/(pages)/desa/galery/foto/Content.tsx index 6daa122e..e69de29b 100644 --- a/src/app/darmasaba/(pages)/desa/galery/foto/Content.tsx +++ b/src/app/darmasaba/(pages)/desa/galery/foto/Content.tsx @@ -1,150 +0,0 @@ -'use client'; - -import colors from '@/con/colors'; -import { Box, Center, Image, Pagination, Paper, SimpleGrid, Stack, Text } from '@mantine/core'; -import { useCallback, useEffect, useState } from 'react'; -import ApiFetch from '@/lib/api-fetch'; - -interface FileItem { - id: string; - name: string; - link: string; - realName: string; - createdAt: string | Date; - category: string; - path: string; - mimeType: string; -} - -export default function FotoContent() { - const [files, setFiles] = useState([]); - const [loading, setLoading] = useState(true); - const [search, setSearch] = useState(''); - const [page, setPage] = useState(1); - const [totalPages, setTotalPages] = useState(1); - const limit = 9; // ✅ ambil 12 data per page - - const loadData = useCallback(async (pageNum: number, searchTerm: string) => { - setLoading(true); - try { - const query: Record = { - category: 'image', - page: pageNum.toString(), - limit: limit.toString(), - }; - if (searchTerm) query.search = searchTerm; - - const response = await ApiFetch.api.fileStorage.findMany.get({ query }); - - if (response.status === 200 && response.data) { - setFiles(response.data.data || []); - setTotalPages(response.data.meta?.totalPages || 1); - } else { - setFiles([]); - } - } catch (err) { - console.error('Load error:', err); - setFiles([]); - } finally { - setLoading(false); - } - }, []); - - // ✅ Initial load + update when URL/search changes - useEffect(() => { - const handleRouteChange = () => { - const urlParams = new URLSearchParams(window.location.search); - const urlSearch = urlParams.get('search') || ''; - const urlPage = parseInt(urlParams.get('page') || '1'); - setSearch(urlSearch); - setPage(urlPage); - loadData(urlPage, urlSearch); - }; - - const handleSearchUpdate = (e: Event) => { - const { search } = (e as CustomEvent).detail; - setSearch(search); - setPage(1); - loadData(1, search); - }; - - handleRouteChange(); - window.addEventListener('popstate', handleRouteChange); - window.addEventListener('searchUpdate', handleSearchUpdate as EventListener); - - return () => { - window.removeEventListener('popstate', handleRouteChange); - window.removeEventListener('searchUpdate', handleSearchUpdate as EventListener); - }; - }, [loadData]); - - // ✅ Update when page/search changes - useEffect(() => { - loadData(page, search); - }, [page, search, loadData]); - - const updateURL = (newSearch: string, newPage: number) => { - const url = new URL(window.location.href); - if (newSearch) url.searchParams.set('search', newSearch); - else url.searchParams.delete('search'); - if (newPage > 1) url.searchParams.set('page', newPage.toString()); - else url.searchParams.delete('page'); - window.history.pushState({}, '', url); - }; - - const handlePageChange = (newPage: number) => { - setPage(newPage); - updateURL(search, newPage); - }; - - if (loading && files.length === 0) { - return
Memuat data...
; - } - - if (files.length === 0) { - return
Tidak ada foto ditemukan
; - } - - return ( - - - {files.map((file) => ( - - - {file.realName - - - - {file.realName || file.name} - - - {new Date(file.createdAt).toLocaleDateString('id-ID', { - day: 'numeric', - month: 'long', - year: 'numeric', - })} - - - - ))} - -
- -
-
- ); -} diff --git a/src/app/darmasaba/(pages)/desa/galery/foto/page.tsx b/src/app/darmasaba/(pages)/desa/galery/foto/page.tsx index 4b9d287b..322e92c9 100644 --- a/src/app/darmasaba/(pages)/desa/galery/foto/page.tsx +++ b/src/app/darmasaba/(pages)/desa/galery/foto/page.tsx @@ -1,25 +1,168 @@ -'use client' +/* eslint-disable @typescript-eslint/no-explicit-any */ +'use client'; -import dynamic from 'next/dynamic'; -import { Suspense } from 'react'; +import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery'; +import colors from '@/con/colors'; +import { + Box, + Center, + Grid, + Image, + Pagination, + Paper, + Skeleton, + Stack, + Text, + Title, +} from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; +import { IconPhoto } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; -// ✅ Load komponen tanpa SSR -const FotoContent = dynamic( - () => import('./Content'), - { - ssr: false, - loading: () =>
Memuat konten...
- } -); +// Komponen kartu foto +function FotoCard({ item }: { item: any }) { + const router = useRouter(); + + const handleClick = () => { + router.push(`/darmasaba/galeri/foto/${item.id}`); + }; -function PageContent() { return ( - Memuat...}> - - + + (e.currentTarget.style.transform = 'scale(1.02)')} + onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')} + > + {item.imageGalleryFoto?.link ? ( + + {item.name + + ) : ( +
+ +
+ )} + + + + {item.name || 'Tanpa Judul'} + + {item.deskripsi && ( + + )} + + {new Date(item.createdAt).toLocaleDateString('id-ID', { + day: 'numeric', + month: 'short', + year: 'numeric', + })} + + +
+
); } -export default function Page() { - return ; +// Komponen utama +export default function GaleriFotoUser() { + const [search] = useState(''); + return ( + + {/* Header */} + + Galeri Foto Desa Darmasaba + + + {/* Daftar Foto */} + + + ); +} + +function FotoList({ search }: { search: string }) { + const FotoState = useProxy(stateGallery.foto); + + const { data, page, totalPages, loading, load } = FotoState.findMany; + + useShallowEffect(() => { + load(page, 3, search); // ✅ 9 item per halaman + }, [page, search]); + + if (loading) { + return ( + + {Array.from({ length: 3 }).map((_, i) => ( + + + + ))} + + ); + } + + if (!data || data.length === 0) { + return ( +
+ + + Tidak ada foto ditemukan + +
+ ); + } + + return ( + + + {data.map((item) => ( + + ))} + + + {/* Pagination */} + +
+ { + load(newPage, 3, search); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }} + total={totalPages} + color="blue" + radius="md" + /> +
+
+ ); } \ No newline at end of file diff --git a/src/app/darmasaba/(pages)/desa/galery/layout.tsx b/src/app/darmasaba/(pages)/desa/galery/layout.tsx index 005a2f5b..bfe22e2e 100644 --- a/src/app/darmasaba/(pages)/desa/galery/layout.tsx +++ b/src/app/darmasaba/(pages)/desa/galery/layout.tsx @@ -1,9 +1,36 @@ +'use client' +import { usePathname } from "next/navigation"; +import { ReactNode } from "react"; import LayoutTabsGalery from "./_lib/layoutTabs"; -export default function LayoutGalery({ children }: { children: React.ReactNode }) { - return ( - - {children} - - ) +// export default function LayoutGalery({ children }: { children: React.ReactNode }) { +// return ( +// +// {children} +// +// ) +// } + +export default function BeritaLayoutClient({ + children, +}: { + children: ReactNode; +}) { + const pathname = usePathname(); + + // Contoh path: + // - /darmasaba/desa/berita/semua → panjang 5 → list + // - /darmasaba/desa/berita/Pemerintahan → panjang 5 → list + // - /darmasaba/desa/berita/Pemerintahan/123 → panjang 6 → detail + + const segments = pathname.split('/').filter(Boolean); + const isDetailPage = segments.length === 5; // [darmasaba, desa, berita, kategori, id] + + if (isDetailPage) { + // Tampilkan tanpa tab menu + return <>{children} + } + + // Tampilkan dengan tab menu (untuk /semua atau /kategori) + return {children}; } \ No newline at end of file diff --git a/src/app/darmasaba/(pages)/desa/galery/video/Content.tsx b/src/app/darmasaba/(pages)/desa/galery/video/Content.tsx index 903a9c90..2e4599e9 100644 --- a/src/app/darmasaba/(pages)/desa/galery/video/Content.tsx +++ b/src/app/darmasaba/(pages)/desa/galery/video/Content.tsx @@ -4,26 +4,27 @@ import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery'; import colors from '@/con/colors'; import { Box, + Button, Center, + Group, Pagination, Paper, SimpleGrid, - Spoiler, Stack, - Text, + Text } from '@mantine/core'; -import { useCallback, useEffect, useState } from 'react'; +import { useTransitionRouter } from 'next-view-transitions'; +import { useCallback, useEffect } from 'react'; import { useSnapshot } from 'valtio'; export default function VideoContent() { - // ✅ expanded state per index - const [expandedMap, setExpandedMap] = useState>({}); const videoState = useSnapshot(stateGallery.video); + const router = useTransitionRouter() const { data, page, totalPages, loading } = videoState.findMany; // Handle search and pagination changes const loadData = useCallback((pageNum: number, searchTerm: string) => { - stateGallery.video.findMany.load(pageNum, 10, searchTerm.trim()); + stateGallery.video.findMany.load(pageNum, 3, searchTerm.trim()); }, []); // Initial load and URL change handler @@ -56,12 +57,6 @@ export default function VideoContent() { loadData(newPage, search); }; - const toggleExpanded = (index: number, value: boolean) => { - setExpandedMap((prev) => ({ - ...prev, - [index]: value, - })); - }; const dataVideo = data || []; @@ -110,27 +105,22 @@ export default function VideoContent() { {v.name} - - Show more - - } - hideLabel={ - - Hide details - - } - expanded={expandedMap[k] || false} - onExpandedChange={(val) => toggleExpanded(k, val)} + + + +
diff --git a/src/app/darmasaba/(pages)/desa/galery/video/[id]/page.tsx b/src/app/darmasaba/(pages)/desa/galery/video/[id]/page.tsx new file mode 100644 index 00000000..7000b863 --- /dev/null +++ b/src/app/darmasaba/(pages)/desa/galery/video/[id]/page.tsx @@ -0,0 +1,197 @@ +'use client'; + +import colors from '@/con/colors'; +import { + Alert, + Box, + Button, + Card, + Group, + Paper, + Skeleton, + Stack, + Text, + ThemeIcon, +} from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; +import { IconArrowBack, IconInfoCircle, IconVideo } from '@tabler/icons-react'; +import { useParams, useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; +import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery'; // pastikan state bisa dipakai di publik +import BackButton from '../../../layanan/_com/BackButto'; + +// Fungsi helper: aman dan tanpa spasi +function convertToEmbedUrl(youtubeUrl: string): string { + try { + const url = new URL(youtubeUrl); + let videoId = ''; + + if (url.hostname === 'youtu.be') { + videoId = url.pathname.slice(1); + } else if (url.hostname.includes('youtube.com')) { + videoId = url.searchParams.get('v') || ''; + } + + return videoId ? `https://www.youtube.com/embed/${videoId}` : youtubeUrl; + } catch { + return youtubeUrl; + } +} + +export default function DetailVideoUser() { + const params = useParams<{ id: string }>(); + const router = useRouter(); + const videoState = useProxy(stateGallery.video); + const [videoError, setVideoError] = useState(false); + + const id = Array.isArray(params.id) ? params.id[0] : params.id; + + useShallowEffect(() => { + if (id) { + videoState.findUnique.load(id); + } + }, [id]); + + const data = videoState.findUnique.data; + + if (!videoState.findUnique && !id) { + return ( + + + + ); + } + + if (!data) { + return ( + + } + title="Video tidak ditemukan" + color="red" + radius="md" + > + Video yang Anda cari tidak tersedia. + + + + ); + } + + const embedUrl = data.linkVideo ? convertToEmbedUrl(data.linkVideo) : null; + + return ( + + {/* Tombol Kembali */} + + + + + {/* Header */} + + {data.name || 'Video Galeri Desa'} + + + {/* Konten Utama */} + + + {/* Video */} + {embedUrl ? ( + +