Fix Menu Gallery : Gallery Foto
Fix detail berita
This commit is contained in:
@@ -49,7 +49,7 @@ function Page() {
|
||||
|
||||
|
||||
return (
|
||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"} px={{ base: "md", md: 0 }}>
|
||||
<Stack pos={"relative"} bg={colors.Bg} pb={"xl"} gap={"xs"} px={{ base: "md", md: 0 }}>
|
||||
<Group px={{ base: "md", md: 100 }}>
|
||||
<NewsReader />
|
||||
</Group>
|
||||
|
||||
@@ -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 (
|
||||
<Box bg={colors.Bg}>
|
||||
<Box pt={33} px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Tampilkan dengan tab menu (untuk /semua atau /kategori)
|
||||
return <LayoutTabsBerita>{children}</LayoutTabsBerita>;
|
||||
}
|
||||
@@ -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<FileItem[]>([]);
|
||||
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<string, string> = {
|
||||
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 <Center>Memuat data...</Center>;
|
||||
}
|
||||
|
||||
if (files.length === 0) {
|
||||
return <Center>Tidak ada foto ditemukan</Center>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box pt={20} px={{ base: 'md', md: 100 }}>
|
||||
<SimpleGrid cols={{ base: 1, md: 3 }}>
|
||||
{files.map((file) => (
|
||||
<Paper
|
||||
key={file.id}
|
||||
mb={50}
|
||||
p="md"
|
||||
radius={26}
|
||||
bg={colors['white-trans-1']}
|
||||
style={{ height: '100%' }}
|
||||
>
|
||||
<Box style={{ height: '250px', overflow: 'hidden', borderRadius: '12px' }}>
|
||||
<Image
|
||||
src={file.link}
|
||||
alt={file.realName || file.name}
|
||||
height={250}
|
||||
width="100%"
|
||||
style={{ objectFit: 'cover', height: '100%', width: '100%' }}
|
||||
loading="lazy"
|
||||
/>
|
||||
</Box>
|
||||
<Stack gap="sm" py={10}>
|
||||
<Text fw="bold" fz={{ base: 'h4', md: 'h3' }}>
|
||||
{file.realName || file.name}
|
||||
</Text>
|
||||
<Text fz="sm" c="dimmed">
|
||||
{new Date(file.createdAt).toLocaleDateString('id-ID', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
<Center mt="xl">
|
||||
<Pagination total={totalPages} value={page} onChange={handlePageChange} />
|
||||
</Center>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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: () => <div>Memuat konten...</div>
|
||||
}
|
||||
);
|
||||
// Komponen kartu foto
|
||||
function FotoCard({ item }: { item: any }) {
|
||||
const router = useRouter();
|
||||
|
||||
const handleClick = () => {
|
||||
router.push(`/darmasaba/galeri/foto/${item.id}`);
|
||||
};
|
||||
|
||||
function PageContent() {
|
||||
return (
|
||||
<Suspense fallback={<div>Memuat...</div>}>
|
||||
<FotoContent />
|
||||
</Suspense>
|
||||
<Grid.Col span={{ base: 12, xs: 6, md: 4 }}>
|
||||
<Paper
|
||||
shadow="sm"
|
||||
radius="md"
|
||||
p={0}
|
||||
onClick={handleClick}
|
||||
style={{ cursor: 'pointer', transition: 'transform 0.2s' }}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.02)')}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')}
|
||||
>
|
||||
{item.imageGalleryFoto?.link ? (
|
||||
<Box
|
||||
pos="relative"
|
||||
style={{
|
||||
paddingBottom: '100%', // ✅ Ubah ke 1:1 (square) — atau sesuaikan
|
||||
overflow: 'hidden',
|
||||
borderRadius: '4px 4px 0 0',
|
||||
backgroundColor: '#f9f9f9', // ✅ background netral
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
radius="lg"
|
||||
src={item.imageGalleryFoto.link}
|
||||
alt={item.name || 'Foto Galeri'}
|
||||
p={10}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'contain', // ✅ Tampilkan utuh, jangan crop
|
||||
objectPosition: 'center', // rata tengah
|
||||
}}
|
||||
loading="lazy"
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<Center h={180} bg="gray.1">
|
||||
<IconPhoto size={40} color="gray" />
|
||||
</Center>
|
||||
)}
|
||||
|
||||
<Stack p="md" gap={4}>
|
||||
<Text fw={600} lineClamp={1}>
|
||||
{item.name || 'Tanpa Judul'}
|
||||
</Text>
|
||||
{item.deskripsi && (
|
||||
<Text fz="sm" c="dimmed" lineClamp={2} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||
)}
|
||||
<Text fz="xs" c="dimmed">
|
||||
{new Date(item.createdAt).toLocaleDateString('id-ID', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Grid.Col>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
return <PageContent />;
|
||||
// Komponen utama
|
||||
export default function GaleriFotoUser() {
|
||||
const [search] = useState('');
|
||||
return (
|
||||
<Box py="xl" px={{ base: 'md', md: 'lg' }}>
|
||||
{/* Header */}
|
||||
<Title order={2} c={colors['blue-button']} mb="lg">
|
||||
Galeri Foto Desa Darmasaba
|
||||
</Title>
|
||||
|
||||
{/* Daftar Foto */}
|
||||
<FotoList search={search} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<Grid mt="md">
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<Grid.Col key={i} span={{ base: 12, xs: 6, md: 4 }}>
|
||||
<Skeleton height={280} radius="md" />
|
||||
</Grid.Col>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
return (
|
||||
<Center py="xl">
|
||||
<Stack align="center" c="dimmed">
|
||||
<IconPhoto size={48} />
|
||||
<Text>Tidak ada foto ditemukan</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack mt="md" gap="xl">
|
||||
<Grid>
|
||||
{data.map((item) => (
|
||||
<FotoCard key={item.id} item={item} />
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
{/* Pagination */}
|
||||
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 3, search);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
total={totalPages}
|
||||
color="blue"
|
||||
radius="md"
|
||||
/>
|
||||
</Center>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<LayoutTabsGalery>
|
||||
{children}
|
||||
</LayoutTabsGalery>
|
||||
)
|
||||
// export default function LayoutGalery({ children }: { children: React.ReactNode }) {
|
||||
// return (
|
||||
// <LayoutTabsGalery>
|
||||
// {children}
|
||||
// </LayoutTabsGalery>
|
||||
// )
|
||||
// }
|
||||
|
||||
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 <LayoutTabsGalery>{children}</LayoutTabsGalery>;
|
||||
}
|
||||
@@ -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<Record<number, boolean>>({});
|
||||
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() {
|
||||
<Text fw="bold" fz="sm" lineClamp={1}>
|
||||
{v.name}
|
||||
</Text>
|
||||
<Spoiler
|
||||
showLabel={
|
||||
<Text fw="bold" fz="sm" c={colors['blue-button']}>
|
||||
Show more
|
||||
</Text>
|
||||
}
|
||||
hideLabel={
|
||||
<Text fw="bold" fz="sm" c={colors['blue-button']}>
|
||||
Hide details
|
||||
</Text>
|
||||
}
|
||||
expanded={expandedMap[k] || false}
|
||||
onExpandedChange={(val) => toggleExpanded(k, val)}
|
||||
<Text
|
||||
ta="justify"
|
||||
fz="sm"
|
||||
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
lineClamp={3}
|
||||
truncate="end"
|
||||
/>
|
||||
<Group justify={"right"}>
|
||||
<Button
|
||||
onClick={() => router.push(`/darmasaba/desa/galery/video/${v.id}`)}
|
||||
bg={colors['blue-button']}
|
||||
>
|
||||
<Text
|
||||
ta="justify"
|
||||
fz="sm"
|
||||
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
|
||||
style={{wordBreak: "break-word", whiteSpace: "normal"}}
|
||||
/>
|
||||
</Spoiler>
|
||||
Detail
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
197
src/app/darmasaba/(pages)/desa/galery/video/[id]/page.tsx
Normal file
197
src/app/darmasaba/(pages)/desa/galery/video/[id]/page.tsx
Normal file
@@ -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 (
|
||||
<Box py="xl" px={{ base: 'md', md: 'lg' }}>
|
||||
<Skeleton height={400} radius="md" />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<Box py="xl" px={{ base: 'md', md: 'lg' }}>
|
||||
<Alert
|
||||
icon={<IconInfoCircle size={20} />}
|
||||
title="Video tidak ditemukan"
|
||||
color="red"
|
||||
radius="md"
|
||||
>
|
||||
Video yang Anda cari tidak tersedia.
|
||||
</Alert>
|
||||
<Button
|
||||
leftSection={<IconArrowBack size={16} />}
|
||||
mt="md"
|
||||
onClick={() => router.push('/darmasaba/galeri/video')}
|
||||
variant="outline"
|
||||
>
|
||||
Kembali ke Galeri
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const embedUrl = data.linkVideo ? convertToEmbedUrl(data.linkVideo) : null;
|
||||
|
||||
return (
|
||||
<Box py="xl" px={{ base: 'md', md: 100 }}>
|
||||
{/* Tombol Kembali */}
|
||||
<Box >
|
||||
<BackButton />
|
||||
</Box>
|
||||
|
||||
{/* Header */}
|
||||
<Text
|
||||
ta="center"
|
||||
fz={{ base: 'xl', md: '2xl' }}
|
||||
fw={700}
|
||||
c={colors['blue-button']}
|
||||
mb="lg"
|
||||
>
|
||||
{data.name || 'Video Galeri Desa'}
|
||||
</Text>
|
||||
|
||||
{/* Konten Utama */}
|
||||
<Card
|
||||
shadow="sm"
|
||||
radius="md"
|
||||
p={{ base: 'md', md: 'xl' }}
|
||||
bg={colors['white-1']}
|
||||
>
|
||||
<Stack gap="lg">
|
||||
{/* Video */}
|
||||
{embedUrl ? (
|
||||
<Box
|
||||
pos="relative"
|
||||
style={{ paddingBottom: '56.25%', height: 0, overflow: 'hidden' }} // 16:9 aspect ratio
|
||||
>
|
||||
<iframe
|
||||
src={embedUrl}
|
||||
title={data.name}
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: 8,
|
||||
border: 'none',
|
||||
}}
|
||||
onError={() => setVideoError(true)}
|
||||
/>
|
||||
</Box>
|
||||
) : videoError ? (
|
||||
<Alert
|
||||
color="orange"
|
||||
icon={<IconVideo size={20} />}
|
||||
title="Gagal memuat video"
|
||||
radius="md"
|
||||
>
|
||||
Mohon maaf, video tidak dapat diputar.
|
||||
</Alert>
|
||||
) : (
|
||||
<Alert
|
||||
color="gray"
|
||||
icon={<IconInfoCircle size={20} />}
|
||||
title="Tidak ada video"
|
||||
radius="md"
|
||||
>
|
||||
Konten video belum tersedia.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Informasi Tambahan */}
|
||||
{data.createdAt && (
|
||||
<Group gap="xs" justify="center" wrap="nowrap">
|
||||
<ThemeIcon variant="light" size="sm" radius="xl">
|
||||
<IconInfoCircle size={14} />
|
||||
</ThemeIcon>
|
||||
<Text fz="sm" c="dimmed">
|
||||
Diunggah pada{' '}
|
||||
{new Date(data.createdAt).toLocaleDateString('id-ID', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})}
|
||||
</Text>
|
||||
</Group>
|
||||
)}
|
||||
|
||||
{/* Deskripsi */}
|
||||
{data.deskripsi && (
|
||||
<Paper p="md" bg="gray.0" radius="md">
|
||||
<Text
|
||||
fz="md"
|
||||
c="dark"
|
||||
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
|
||||
style={{
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: 'normal',
|
||||
lineHeight: 1.6,
|
||||
}}
|
||||
/>
|
||||
</Paper>
|
||||
)}
|
||||
</Stack>
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user