Fix Konsisten teks di tampilan mobile dan desktop

Fix QC Kak Inno tgl 10 Des
Fix QC Kak Ayu tgl 10 Des
This commit is contained in:
2025-12-11 17:58:03 +08:00
parent 242ea86f77
commit a00481152c
43 changed files with 1725 additions and 1093 deletions

View File

@@ -1,5 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client';
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita'; import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
import { import {
Badge, Badge,
@@ -51,10 +51,14 @@ export default function Content({ kategori }: { kategori: string }) {
<Container size="xl" px={{ base: 'md', md: 'xl' }}> <Container size="xl" px={{ base: 'md', md: 'xl' }}>
{/* === Berita Utama === */} {/* === Berita Utama === */}
{featuredState.loading ? ( {featuredState.loading ? (
<Center><Skeleton h={400} /></Center> <Center>
<Skeleton h={400} />
</Center>
) : featured ? ( ) : featured ? (
<Box mb={50}> <Box mb={50}>
<Text fz="h2" fw={700} mb="md">Berita Utama</Text> <Title order={2} mb="md">
Berita Utama
</Title>
<Paper shadow="md" radius="md" withBorder> <Paper shadow="md" radius="md" withBorder>
<Grid gutter={0}> <Grid gutter={0}>
<GridCol span={{ base: 12, md: 6 }}> <GridCol span={{ base: 12, md: 6 }}>
@@ -74,13 +78,29 @@ export default function Content({ kategori }: { kategori: string }) {
<Badge color="blue" variant="light" mb="md"> <Badge color="blue" variant="light" mb="md">
{featured.kategoriBerita?.name || kategori} {featured.kategoriBerita?.name || kategori}
</Badge> </Badge>
<Title order={2} mb="md">{featured.judul}</Title> <Title order={3} mb="md">
<Text c="dimmed" lineClamp={3} mb="md" dangerouslySetInnerHTML={{ __html: featured.deskripsi }} /> {featured.judul}
</Title>
<Text
c="dimmed"
lineClamp={3}
mb="md"
style={{ lineHeight: 1.6 }}
dangerouslySetInnerHTML={{ __html: featured.deskripsi }}
/>
</div> </div>
<Group justify="apart" mt="auto"> <Group justify="apart" mt="auto">
<Group gap="xs"> <Group gap="xs">
<IconCalendar size={18} /> <IconCalendar size={18} />
<Text size="sm"> <Text
fz={{ base: 'xs', md: 'sm' }}
c="dimmed"
lh={1.5}
style={{
fontSize: '0.875rem',
lineHeight: '1.5rem',
}}
>
{new Date(featured.createdAt).toLocaleDateString('id-ID', { {new Date(featured.createdAt).toLocaleDateString('id-ID', {
day: 'numeric', day: 'numeric',
month: 'long', month: 'long',
@@ -91,7 +111,9 @@ export default function Content({ kategori }: { kategori: string }) {
<Button <Button
variant="light" variant="light"
rightSection={<IconArrowRight size={16} />} rightSection={<IconArrowRight size={16} />}
onClick={() => router.push(`/darmasaba/desa/berita/${kategori}/${featured.id}`)} onClick={() =>
router.push(`/darmasaba/desa/berita/${kategori}/${featured.id}`)
}
> >
Baca Selengkapnya Baca Selengkapnya
</Button> </Button>
@@ -105,19 +127,29 @@ export default function Content({ kategori }: { kategori: string }) {
{/* === Daftar Berita === */} {/* === Daftar Berita === */}
<Box mt={50}> <Box mt={50}>
<Title order={2} mb="md">Daftar Berita</Title> <Title order={2} mb="md">
Daftar Berita
</Title>
<Divider mb="xl" /> <Divider mb="xl" />
{state.findMany.loading ? ( {state.findMany.loading ? (
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl"> <SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl">
{Array(3).fill(0).map((_, i) => ( {Array(3)
<Skeleton key={i} h={300} radius="md" /> .fill(0)
))} .map((_, i) => (
<Skeleton key={i} h={300} radius="md" />
))}
</SimpleGrid> </SimpleGrid>
) : paginatedNews.length === 0 ? ( ) : paginatedNews.length === 0 ? (
<Text c="dimmed" ta="center">Belum ada berita di kategori &quot;{kategori}&quot;.</Text> <Text c="dimmed" ta="center" fz={{ base: 'sm', md: 'md' }} lh={1.5}>
Belum ada berita di kategori &quot;{kategori}&quot;.
</Text>
) : ( ) : (
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl" verticalSpacing="xl"> <SimpleGrid
cols={{ base: 1, sm: 2, lg: 3 }}
spacing="xl"
verticalSpacing="xl"
>
{paginatedNews.map((item) => ( {paginatedNews.map((item) => (
<Card <Card
key={item.id} key={item.id}
@@ -125,19 +157,51 @@ export default function Content({ kategori }: { kategori: string }) {
p="lg" p="lg"
radius="md" radius="md"
withBorder withBorder
onClick={() => router.push(`/darmasaba/desa/berita/${kategori}/${item.id}`)} onClick={() =>
router.push(`/darmasaba/desa/berita/${kategori}/${item.id}`)
}
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
> >
<Card.Section> <Card.Section>
<Image src={item.image?.link} height={200} alt={item.judul} fit="cover" loading="lazy"/> <Image
src={item.image?.link}
height={200}
alt={item.judul}
fit="cover"
loading="lazy"
/>
</Card.Section> </Card.Section>
<Badge color="blue" variant="light" mt="md"> <Badge color="blue" variant="light" mt="md">
{item.kategoriBerita?.name || kategori} {item.kategoriBerita?.name || kategori}
</Badge> </Badge>
<Text fw={600} size="lg" mt="sm" lineClamp={2}>{item.judul}</Text> <Title
<Text size="sm" c="dimmed" lineClamp={3} style={{wordBreak: "break-word", whiteSpace: "normal"}} mt="xs" dangerouslySetInnerHTML={{ __html: item.deskripsi }} /> order={4}
mt="sm"
fz={{ base: 'sm', md: 'md' }}
style={{ lineHeight: 1.4 }}
lineClamp={2}
>
{item.judul}
</Title>
<Text
fz={{ base: 'xs', md: 'sm' }}
c="dimmed"
lineClamp={3}
style={{
wordBreak: 'break-word',
whiteSpace: 'normal',
lineHeight: 1.5,
}}
mt="xs"
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
/>
<Group justify="apart" mt="md" gap="xs"> <Group justify="apart" mt="md" gap="xs">
<Text size="xs" c="dimmed"> <Text
fz={{ base: 'xs', md: 'xs' }}
c="dimmed"
lh={1.4}
style={{ fontSize: '0.75rem', lineHeight: '1.125rem' }}
>
{new Date(item.createdAt).toLocaleDateString('id-ID', { {new Date(item.createdAt).toLocaleDateString('id-ID', {
day: 'numeric', day: 'numeric',
month: 'short', month: 'short',

View File

@@ -3,18 +3,16 @@
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita'; import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
import NewsReader from '@/app/darmasaba/_com/NewsReader'; import NewsReader from '@/app/darmasaba/_com/NewsReader';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Center, Container, Group, Image, Skeleton, Stack, Text } from '@mantine/core'; import { Box, Center, Container, Group, Image, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useParams } from 'next/navigation'; import { useParams } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function Page() { function Page() {
const params = useParams<{ id: string }>(); const params = useParams<{ id: string }>();
const id = Array.isArray(params.id) ? params.id[0] : params.id; const id = Array.isArray(params.id) ? params.id[0] : params.id;
const state = useProxy(stateDashboardBerita.berita) const state = useProxy(stateDashboardBerita.berita);
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
const loadData = async () => { const loadData = async () => {
@@ -27,9 +25,9 @@ function Page() {
} finally { } finally {
setLoading(false); setLoading(false);
} }
} };
loadData() loadData();
}, [id]) }, [id]);
if (loading) { if (loading) {
return ( return (
@@ -47,41 +45,49 @@ function Page() {
); );
} }
return ( return (
<Stack pos={"relative"} bg={colors.Bg} pb={"xl"} gap={"xs"} 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 }}> <Group px={{ base: 'md', md: 100 }}>
<NewsReader /> <NewsReader />
</Group> </Group>
<Container w={{ base: "100%", md: "50%" }} > <Container w={{ base: '100%', md: '50%' }}>
<Box pb={20}> <Box pb={20}>
<Text id='news-title' ta={"center"} fz={"2.4rem"} c={colors["blue-button"]} fw={"bold"}> <Title
{state.findUnique.data?.judul} id="news-title"
</Text> order={1}
<Text ta="center"
ta={"center"} c={colors['blue-button']}
fw={"bold"} fw="bold"
fz={"1.5rem"} lh={{ base: 1.2, md: 1.25 }}
>
{state.findUnique.data.judul}
</Title>
<Title
order={2}
ta="center"
fw="bold"
fz={{ base: 'md', md: 'lg' }}
lh={{ base: 1.3, md: 1.35 }}
> >
Informasi dan Pelayanan Administrasi Digital Informasi dan Pelayanan Administrasi Digital
</Text> </Title>
</Box> </Box>
<Image src={state.findUnique.data?.image?.link || ''} alt='' w={"100%"} loading="lazy" /> <Image src={state.findUnique.data.image?.link || ''} alt="" w="100%" loading="lazy" />
</Container> </Container>
<Box px={{ base: "md", md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
<Stack gap={"xs"}> <Stack gap="xs">
<Text <Text
id='news-content' id="news-content"
py={20} py={20}
fz={{ base: "sm", md: "lg" }} fz={{ base: 'sm', md: 'md' }}
lh={{ base: 1.6, md: 1.8 }} // ✅ line-height lebih rapat dan responsif lh={{ base: 1.6, md: 1.8 }}
ta="justify" ta="justify"
style={{ style={{
wordBreak: "break-word", wordBreak: 'break-word',
whiteSpace: "normal", whiteSpace: 'normal',
}} }}
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: state.findUnique.data?.content || "", __html: state.findUnique.data.content || '',
}} }}
/> />
</Stack> </Stack>

View File

@@ -16,35 +16,30 @@ function Semua() {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const router = useTransitionRouter(); const router = useTransitionRouter();
// Ambil parameter langsung dari URL
const search = searchParams.get('search') || ''; const search = searchParams.get('search') || '';
const page = parseInt(searchParams.get('page') || '1'); const page = parseInt(searchParams.get('page') || '1');
// Gunakan proxy untuk state global
const state = useProxy(stateDashboardBerita.berita); const state = useProxy(stateDashboardBerita.berita);
const featured = useProxy(stateDashboardBerita.berita.findFirst); const featured = useProxy(stateDashboardBerita.berita.findFirst);
const loadingGrid = state.findMany.loading; const loadingGrid = state.findMany.loading;
const loadingFeatured = featured.loading; const loadingFeatured = featured.loading;
// Load berita utama sekali saja
useEffect(() => { useEffect(() => {
if (!featured.data && !loadingFeatured) { if (!featured.data && !loadingFeatured) {
stateDashboardBerita.berita.findFirst.load(); stateDashboardBerita.berita.findFirst.load();
} }
}, [featured.data, loadingFeatured]); }, [featured.data, loadingFeatured]);
// Load berita terbaru tiap page / search berubah
useEffect(() => { useEffect(() => {
const limit = 3; const limit = 3;
state.findMany.load(page, limit, search); state.findMany.load(page, limit, search);
}, [page, search]); }, [page, search]);
// Handler pagination → langsung update URL
const handlePageChange = (newPage: number) => { const handlePageChange = (newPage: number) => {
const url = new URLSearchParams(searchParams.toString()); const url = new URLSearchParams(searchParams.toString());
if (search) url.set('search', search); if (search) url.set('search', search);
if (newPage > 1) url.set('page', newPage.toString()); if (newPage > 1) url.set('page', newPage.toString());
else url.delete('page'); // biar page=1 ga muncul di URL else url.delete('page');
router.replace(`?${url.toString()}`); router.replace(`?${url.toString()}`);
}; };
@@ -61,7 +56,7 @@ function Semua() {
<Center><Skeleton h={400} /></Center> <Center><Skeleton h={400} /></Center>
) : featuredData ? ( ) : featuredData ? (
<Box mb={50}> <Box mb={50}>
<Text fz="h2" fw={700} mb="md">Berita Utama</Text> <Title order={2} mb="md">Berita Utama</Title>
<Paper shadow="md" radius="md" withBorder> <Paper shadow="md" radius="md" withBorder>
<Grid gutter={0}> <Grid gutter={0}>
<GridCol span={{ base: 12, md: 6 }}> <GridCol span={{ base: 12, md: 6 }}>
@@ -81,13 +76,24 @@ function Semua() {
<Badge color="blue" variant="light" mb="md"> <Badge color="blue" variant="light" mb="md">
{featuredData.kategoriBerita?.name || 'Berita'} {featuredData.kategoriBerita?.name || 'Berita'}
</Badge> </Badge>
<Title order={2} mb="md">{featuredData.judul}</Title> <Title order={3} mb="md">{featuredData.judul}</Title>
<Text c="dimmed" lineClamp={3} mb="md" dangerouslySetInnerHTML={{ __html: featuredData.deskripsi }} /> <Text
c="dimmed"
lineClamp={3}
mb="md"
dangerouslySetInnerHTML={{ __html: featuredData.deskripsi }}
fz={{ base: 'sm', md: 'md' }}
lh={{ base: 1.5, md: 1.6 }}
/>
</div> </div>
<Group justify="apart" mt="auto"> <Group justify="apart" mt="auto">
<Group gap="xs"> <Group gap="xs">
<IconCalendar size={18} /> <IconCalendar size={18} />
<Text size="sm"> <Text
c="dimmed"
fz={{ base: 'xs', md: 'sm' }}
lh={{ base: 1.4, md: 1.5 }}
>
{new Date(featuredData.createdAt).toLocaleDateString('id-ID', { {new Date(featuredData.createdAt).toLocaleDateString('id-ID', {
day: 'numeric', day: 'numeric',
month: 'long', month: 'long',
@@ -124,7 +130,9 @@ function Semua() {
))} ))}
</SimpleGrid> </SimpleGrid>
) : paginatedNews.length === 0 ? ( ) : paginatedNews.length === 0 ? (
<Text c="dimmed" ta="center">Tidak ada berita ditemukan.</Text> <Text c="dimmed" ta="center" fz={{ base: 'sm', md: 'md' }} lh={{ base: 1.5, md: 1.6 }}>
Tidak ada berita ditemukan.
</Text>
) : ( ) : (
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl" verticalSpacing="xl"> <SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl" verticalSpacing="xl">
{paginatedNews.map((item) => ( {paginatedNews.map((item) => (
@@ -143,11 +151,24 @@ function Semua() {
{item.kategoriBerita?.name || 'Berita'} {item.kategoriBerita?.name || 'Berita'}
</Badge> </Badge>
<Text fw={600} size="lg" mt="sm" lineClamp={2}>{item.judul}</Text> <Title order={4} mt="sm" lineClamp={2}>
<Text size="sm" c="dimmed" lineClamp={3} mt="xs" dangerouslySetInnerHTML={{ __html: item.deskripsi }} /> {item.judul}
</Title>
<Text
c="dimmed"
lineClamp={3}
mt="xs"
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
fz={{ base: 'xs', md: 'sm' }}
lh={{ base: 1.5, md: 1.6 }}
/>
<Flex align="center" justify="apart" mt="md" gap="xs"> <Flex align="center" justify="apart" mt="md" gap="xs">
<Text size="xs" c="dimmed"> <Text
c="dimmed"
fz={{ base: 'xs', md: 'xs' }}
lh={{ base: 1.4, md: 1.4 }}
>
{new Date(item.createdAt).toLocaleDateString('id-ID', { {new Date(item.createdAt).toLocaleDateString('id-ID', {
day: 'numeric', day: 'numeric',
month: 'short', month: 'short',

View File

@@ -17,17 +17,11 @@ import {
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconPhoto } from '@tabler/icons-react'; import { IconPhoto } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
// Komponen kartu foto // Komponen kartu foto
function FotoCard({ item }: { item: any }) { function FotoCard({ item }: { item: any }) {
const router = useRouter();
const handleClick = () => {
router.push(`/darmasaba/galeri/foto/${item.id}`);
};
return ( return (
<Grid.Col span={{ base: 12, xs: 6, md: 4 }}> <Grid.Col span={{ base: 12, xs: 6, md: 4 }}>
@@ -35,19 +29,19 @@ function FotoCard({ item }: { item: any }) {
shadow="sm" shadow="sm"
radius="md" radius="md"
p={0} p={0}
onClick={handleClick} style={{ transition: 'transform 0.2s' }}
style={{ cursor: 'pointer', transition: 'transform 0.2s' }}
onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.02)')} onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.02)')}
onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')} onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')}
> >
{item.imageGalleryFoto?.link ? ( {item.imageGalleryFoto?.link ? (
<Box <Box
pos="relative" pos="relative"
style={{ style={{
paddingBottom: '100%', // ✅ Ubah ke 1:1 (square) — atau sesuaikan paddingBottom: '100%',
overflow: 'hidden', overflow: 'hidden',
borderRadius: '4px 4px 0 0', borderRadius: '4px 4px 0 0',
backgroundColor: '#f9f9f9', // ✅ background netral backgroundColor: '#f9f9f9',
}} }}
> >
<Image <Image
@@ -61,8 +55,8 @@ function FotoCard({ item }: { item: any }) {
left: 0, left: 0,
width: '100%', width: '100%',
height: '100%', height: '100%',
objectFit: 'contain', // ✅ Tampilkan utuh, jangan crop objectFit: 'contain',
objectPosition: 'center', // rata tengah objectPosition: 'center',
}} }}
loading="lazy" loading="lazy"
/> />
@@ -74,13 +68,23 @@ function FotoCard({ item }: { item: any }) {
)} )}
<Stack p="md" gap={4}> <Stack p="md" gap={4}>
<Text fw={600} lineClamp={1}> <Text fw={600} lineClamp={1} fz={{ base: 'sm', md: 'md' }} lh={{ base: '1.4', md: '1.5' }}>
{item.name || 'Tanpa Judul'} {item.name || 'Tanpa Judul'}
</Text> </Text>
{item.deskripsi && ( {item.deskripsi && (
<Text fz="sm" c="dimmed" lineClamp={2} dangerouslySetInnerHTML={{ __html: item.deskripsi }} /> <Text
fz={{ base: 'xs', md: 'sm' }}
c="dimmed"
lineClamp={2}
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
lh={{ base: '1.4', md: '1.5' }}
/>
)} )}
<Text fz="xs" c="dimmed"> <Text
fz={{ base: 11, md: 'xs' }}
c="dimmed"
lh={{ base: '1.3', md: '1.4' }}
>
{new Date(item.createdAt).toLocaleDateString('id-ID', { {new Date(item.createdAt).toLocaleDateString('id-ID', {
day: 'numeric', day: 'numeric',
month: 'short', month: 'short',
@@ -99,7 +103,7 @@ export default function GaleriFotoUser() {
return ( return (
<Box py="xl" px={{ base: 'md', md: 'lg' }}> <Box py="xl" px={{ base: 'md', md: 'lg' }}>
{/* Header */} {/* Header */}
<Title order={2} c={colors['blue-button']} mb="lg"> <Title order={2} c={colors['blue-button']} mb="lg" ta="center">
Galeri Foto Desa Darmasaba Galeri Foto Desa Darmasaba
</Title> </Title>
@@ -115,7 +119,7 @@ function FotoList({ search }: { search: string }) {
const { data, page, totalPages, loading, load } = FotoState.findMany; const { data, page, totalPages, loading, load } = FotoState.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 3, search); // ✅ 9 item per halaman load(page, 3, search);
}, [page, search]); }, [page, search]);
if (loading) { if (loading) {
@@ -135,7 +139,9 @@ function FotoList({ search }: { search: string }) {
<Center py="xl"> <Center py="xl">
<Stack align="center" c="dimmed"> <Stack align="center" c="dimmed">
<IconPhoto size={48} /> <IconPhoto size={48} />
<Text>Tidak ada foto ditemukan</Text> <Text fz={{ base: 'sm', md: 'md' }} lh={{ base: '1.4', md: '1.5' }}>
Tidak ada foto ditemukan
</Text>
</Stack> </Stack>
</Center> </Center>
); );
@@ -150,19 +156,18 @@ function FotoList({ search }: { search: string }) {
</Grid> </Grid>
{/* Pagination */} {/* Pagination */}
<Center>
<Center> <Pagination
<Pagination value={page}
value={page} onChange={(newPage) => {
onChange={(newPage) => { load(newPage, 3, search);
load(newPage, 3, search); window.scrollTo({ top: 0, behavior: 'smooth' });
window.scrollTo({ top: 0, behavior: 'smooth' }); }}
}} total={totalPages}
total={totalPages} color="blue"
color="blue" radius="md"
radius="md" />
/> </Center>
</Center>
</Stack> </Stack>
); );
} }

View File

@@ -11,7 +11,8 @@ import {
Paper, Paper,
SimpleGrid, SimpleGrid,
Stack, Stack,
Text Text,
Title
} from '@mantine/core'; } from '@mantine/core';
import { useTransitionRouter } from 'next-view-transitions'; import { useTransitionRouter } from 'next-view-transitions';
import { useCallback, useEffect } from 'react'; import { useCallback, useEffect } from 'react';
@@ -19,15 +20,13 @@ import { useSnapshot } from 'valtio';
export default function VideoContent() { export default function VideoContent() {
const videoState = useSnapshot(stateGallery.video); const videoState = useSnapshot(stateGallery.video);
const router = useTransitionRouter() const router = useTransitionRouter();
const { data, page, totalPages, loading } = videoState.findMany; const { data, page, totalPages, loading } = videoState.findMany;
// Handle search and pagination changes
const loadData = useCallback((pageNum: number, searchTerm: string) => { const loadData = useCallback((pageNum: number, searchTerm: string) => {
stateGallery.video.findMany.load(pageNum, 3, searchTerm.trim()); stateGallery.video.findMany.load(pageNum, 3, searchTerm.trim());
}, []); }, []);
// Initial load and URL change handler
useEffect(() => { useEffect(() => {
const handleRouteChange = () => { const handleRouteChange = () => {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
@@ -57,13 +56,14 @@ export default function VideoContent() {
loadData(newPage, search); loadData(newPage, search);
}; };
const dataVideo = data || []; const dataVideo = data || [];
if (loading && !data) { if (loading && !data) {
return ( return (
<Box py={10}> <Box py={10}>
<Text>Memuat Video...</Text> <Text fz={{ base: 'sm', md: 'md' }} c="dimmed" ta="center">
Memuat Video...
</Text>
</Box> </Box>
); );
} }
@@ -78,55 +78,71 @@ export default function VideoContent() {
p="md" p="md"
radius={26} radius={26}
bg={colors['white-trans-1']} bg={colors['white-trans-1']}
w={{ base: '100%', md: '100%' }} w="100%"
> >
<Box> <Center>
<Center> <Box
<Box component="iframe"
component="iframe" src={convertToEmbedUrl(v.linkVideo)}
src={convertToEmbedUrl(v.linkVideo)} width="100%"
width="100%" height={300}
height={300} allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowFullScreen
allowFullScreen style={{ borderRadius: 8 }}
style={{ borderRadius: 8 }} />
/> </Center>
</Center>
</Box> <Stack gap="sm" py={10}>
<Box> {/* Tanggal: Caption */}
<Stack gap="sm" py={10}> <Text
<Text fz="sm" c="dimmed"> fz={{ base: 12, md: 14 }}
{new Date(v.createdAt).toLocaleDateString('id-ID', { c="dimmed"
day: 'numeric', ta="left"
month: 'long', >
year: 'numeric', {new Date(v.createdAt).toLocaleDateString('id-ID', {
})} day: 'numeric',
</Text> month: 'long',
<Text fw="bold" fz="sm" lineClamp={1}> year: 'numeric',
{v.name} })}
</Text> </Text>
<Text
ta="justify" {/* Judul Video: Subsection (H3) */}
fz="sm" <Title
dangerouslySetInnerHTML={{ __html: v.deskripsi }} order={3}
style={{ wordBreak: "break-word", whiteSpace: "normal" }} c="dark"
lineClamp={3} ta="left"
truncate="end" lh={1.3}
/> style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}
<Group justify={"right"}> >
<Button {v.name}
</Title>
{/* Deskripsi: Body kecil */}
<Text
ta="justify"
fz={{ base: 13, md: 14 }}
c="dimmed"
style={{ wordBreak: 'break-word' }}
lineClamp={3}
>
<span dangerouslySetInnerHTML={{ __html: v.deskripsi }} />
</Text>
<Group justify="right">
<Button
onClick={() => router.push(`/darmasaba/desa/galery/video/${v.id}`)} onClick={() => router.push(`/darmasaba/desa/galery/video/${v.id}`)}
bg={colors['blue-button']} bg={colors['blue-button']}
fz={{ base: 'sm', md: 'md' }}
> >
Detail Detail
</Button> </Button>
</Group> </Group>
</Stack> </Stack>
</Box>
</Paper> </Paper>
</Box> </Box>
))} ))}
</SimpleGrid> </SimpleGrid>
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
@@ -140,7 +156,6 @@ export default function VideoContent() {
); );
} }
// ✅ Fix: convert YouTube URL ke embed
function convertToEmbedUrl(youtubeUrl: string): string { function convertToEmbedUrl(youtubeUrl: string): string {
try { try {
const url = new URL(youtubeUrl); const url = new URL(youtubeUrl);

View File

@@ -12,16 +12,17 @@ import {
Stack, Stack,
Text, Text,
ThemeIcon, ThemeIcon,
Title,
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconInfoCircle, IconVideo } from '@tabler/icons-react'; import { IconArrowBack, IconInfoCircle, IconVideo } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery'; // pastikan state bisa dipakai di publik import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
import BackButton from '../../../layanan/_com/BackButto'; import BackButton from '../../../layanan/_com/BackButto';
// Fungsi helper: aman dan tanpa spasi
function convertToEmbedUrl(youtubeUrl: string): string { function convertToEmbedUrl(youtubeUrl: string): string {
try { try {
const url = new URL(youtubeUrl); const url = new URL(youtubeUrl);
@@ -72,7 +73,9 @@ export default function DetailVideoUser() {
color="red" color="red"
radius="md" radius="md"
> >
Video yang Anda cari tidak tersedia. <Text fz={{ base: 'sm', md: 'md' }} c="red.9">
Video yang Anda cari tidak tersedia.
</Text>
</Alert> </Alert>
<Button <Button
leftSection={<IconArrowBack size={16} />} leftSection={<IconArrowBack size={16} />}
@@ -91,20 +94,20 @@ export default function DetailVideoUser() {
return ( return (
<Box py="xl" px={{ base: 'md', md: 100 }}> <Box py="xl" px={{ base: 'md', md: 100 }}>
{/* Tombol Kembali */} {/* Tombol Kembali */}
<Box > <Box>
<BackButton /> <BackButton />
</Box> </Box>
{/* Header */} {/* Header - Dijadikan Title */}
<Text <Title
order={1}
ta="center" ta="center"
fz={{ base: 'xl', md: '2xl' }}
fw={700}
c={colors['blue-button']} c={colors['blue-button']}
mb="lg" mb="lg"
lh={{ base: 1.2, md: 1.25 }}
> >
{data.name || 'Video Galeri Desa'} {data.name || 'Video Galeri Desa'}
</Text> </Title>
{/* Konten Utama */} {/* Konten Utama */}
<Card <Card
@@ -118,7 +121,7 @@ export default function DetailVideoUser() {
{embedUrl ? ( {embedUrl ? (
<Box <Box
pos="relative" pos="relative"
style={{ paddingBottom: '56.25%', height: 0, overflow: 'hidden' }} // 16:9 aspect ratio style={{ paddingBottom: '56.25%', height: 0, overflow: 'hidden' }}
> >
<iframe <iframe
src={embedUrl} src={embedUrl}
@@ -144,7 +147,9 @@ export default function DetailVideoUser() {
title="Gagal memuat video" title="Gagal memuat video"
radius="md" radius="md"
> >
Mohon maaf, video tidak dapat diputar. <Text fz={{ base: 'xs', md: 'sm' }} c="orange.9">
Mohon maaf, video tidak dapat diputar.
</Text>
</Alert> </Alert>
) : ( ) : (
<Alert <Alert
@@ -153,7 +158,9 @@ export default function DetailVideoUser() {
title="Tidak ada video" title="Tidak ada video"
radius="md" radius="md"
> >
Konten video belum tersedia. <Text fz={{ base: 'xs', md: 'sm' }} c="dimmed">
Konten video belum tersedia.
</Text>
</Alert> </Alert>
)} )}
@@ -163,7 +170,11 @@ export default function DetailVideoUser() {
<ThemeIcon variant="light" size="sm" radius="xl"> <ThemeIcon variant="light" size="sm" radius="xl">
<IconInfoCircle size={14} /> <IconInfoCircle size={14} />
</ThemeIcon> </ThemeIcon>
<Text fz="sm" c="dimmed"> <Text
fz={{ base: 'xs', md: 'sm' }}
c="dimmed"
lh={{ base: 1.4, md: 1.5 }}
>
Diunggah pada{' '} Diunggah pada{' '}
{new Date(data.createdAt).toLocaleDateString('id-ID', { {new Date(data.createdAt).toLocaleDateString('id-ID', {
weekday: 'long', weekday: 'long',
@@ -179,8 +190,9 @@ export default function DetailVideoUser() {
{data.deskripsi && ( {data.deskripsi && (
<Paper p="md" bg="gray.0" radius="md"> <Paper p="md" bg="gray.0" radius="md">
<Text <Text
fz="md" fz={{ base: 'sm', md: 'md' }}
c="dark" c="dark"
ta={"justify"}
dangerouslySetInnerHTML={{ __html: data.deskripsi }} dangerouslySetInnerHTML={{ __html: data.deskripsi }}
style={{ style={{
wordBreak: 'break-word', wordBreak: 'break-word',

View File

@@ -3,7 +3,7 @@
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa'; import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { ActionIcon, Box, Divider, Flex, Group, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; import { ActionIcon, Box, Divider, Flex, Group, Skeleton, Stack, Text, Title, Tooltip } from '@mantine/core';
import { IconBrandFacebook, IconBrandInstagram, IconBrandTwitter, IconBrandWhatsapp } from '@tabler/icons-react'; import { IconBrandFacebook, IconBrandInstagram, IconBrandTwitter, IconBrandWhatsapp } from '@tabler/icons-react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
@@ -39,30 +39,38 @@ function PelayananPendudukNonPermanent() {
) : ( ) : (
<Stack gap="xl"> <Stack gap="xl">
<Box> <Box>
<Text fz={{ base: "xl", md: "2xl" }} fw={700} lh={1.3} c="dark"> <Title
order={1}
fz={{ base: 'lg', md: 'xl' }}
fw={700}
lh={{ base: 1.3, md: 1.3 }}
c="dark"
>
{data?.name || "Judul belum tersedia"} {data?.name || "Judul belum tersedia"}
</Text> </Title>
</Box> </Box>
<Box> <Box>
{data?.deskripsi ? ( {data?.deskripsi ? (
<Text <Text
fz={{ base: "sm", md: "md" }} fz={{ base: 'sm', md: 'md' }}
lh={1.7} lh={{ base: 1.6, md: 1.7 }}
ta="justify" ta="justify"
c="dimmed" c="black"
dangerouslySetInnerHTML={{ __html: data?.deskripsi }} dangerouslySetInnerHTML={{ __html: data?.deskripsi }}
style={{wordBreak: "break-word", whiteSpace: "normal"}} style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/> />
) : ( ) : (
<Text fz="sm" c="gray">Deskripsi belum tersedia.</Text> <Text fz="xs" c="gray">
Deskripsi belum tersedia.
</Text>
)} )}
</Box> </Box>
<Divider color={colors["blue-button"]} size="sm" /> <Divider color={colors["blue-button"]} size="sm" />
<Flex justify="space-between" align="center" wrap="wrap" gap="md"> <Flex justify="space-between" align="center" wrap="wrap" gap="md">
<Text fz={{ base: "xs", md: "sm" }} c="dimmed"> <Text fz={{ base: 'xs', md: 'sm' }} lh={{ base: 1.4, md: 1.5 }} c="black">
25 Mei 2021 Darmasaba 25 Mei 2021 Darmasaba
</Text> </Text>
<Group gap="md"> <Group gap="md">

View File

@@ -47,7 +47,7 @@ function PelayananPerizinanBerusaha() {
return ( return (
<Center mih={300}> <Center mih={300}>
<Stack align="center" gap="sm"> <Stack align="center" gap="sm">
<Text fz="lg" fw={500} c="dimmed"> <Text fz={{ base: 'md', md: 'lg' }} fw={500} c="dimmed" lh="sm">
Belum ada informasi layanan yang tersedia Belum ada informasi layanan yang tersedia
</Text> </Text>
<Button component="a" href="https://oss.go.id" target="_blank" radius="xl"> <Button component="a" href="https://oss.go.id" target="_blank" radius="xl">
@@ -67,10 +67,10 @@ function PelayananPerizinanBerusaha() {
) : ( ) : (
<Stack gap="lg"> <Stack gap="lg">
<Box> <Box>
<Title order={2} fw={700} fz={{ base: 22, md: 32 }} mb="sm"> <Title order={2} fw={700} mb="sm">
Perizinan Berusaha Berbasis Risiko melalui OSS Perizinan Berusaha Berbasis Risiko melalui OSS
</Title> </Title>
<Text fz={{ base: 'sm', md: 'md' }} c="dimmed"> <Text fz={{ base: 'sm', md: 'md' }} c="black" lh="sm">
Sistem Online Single Submission (OSS) untuk pendaftaran NIB Sistem Online Single Submission (OSS) untuk pendaftaran NIB
</Text> </Text>
</Box> </Box>
@@ -83,13 +83,13 @@ function PelayananPerizinanBerusaha() {
/> />
<Box> <Box>
<Text fw={600} mb="sm" fz={{ base: 'sm', md: 'lg' }}> <Title order={3} fw={600} mb="sm">
Alur pendaftaran NIB: Alur pendaftaran NIB:
</Text> </Title>
<Stepper <Stepper
active={active} active={active}
onStepClick={(step) => { onStepClick={(step) => {
if (step <= active) { // Only allow clicking on previous or current steps if (step <= active) {
setActive(step); setActive(step);
} }
}} }}
@@ -102,28 +102,42 @@ function PelayananPerizinanBerusaha() {
}} }}
> >
<StepperStep label="Langkah 1" description="Daftar Akun"> <StepperStep label="Langkah 1" description="Daftar Akun">
<Text fz="sm">Membuat akun di portal OSS</Text> <Text fz={{ base: 'xs', md: 'sm' }} lh="sm">
Membuat akun di portal OSS
</Text>
</StepperStep> </StepperStep>
<StepperStep label="Langkah 2" description="Isi Data Perusahaan"> <StepperStep label="Langkah 2" description="Isi Data Perusahaan">
<Text fz="sm">Lengkapi informasi perusahaan, data pemegang saham, dan alamat</Text> <Text fz={{ base: 'xs', md: 'sm' }} lh="sm">
Lengkapi informasi perusahaan, data pemegang saham, dan alamat
</Text>
</StepperStep> </StepperStep>
<StepperStep label="Langkah 3" description="Pilih KBLI"> <StepperStep label="Langkah 3" description="Pilih KBLI">
<Text fz="sm">Menentukan kode KBLI sesuai jenis usaha</Text> <Text fz={{ base: 'xs', md: 'sm' }} lh="sm">
Menentukan kode KBLI sesuai jenis usaha
</Text>
</StepperStep> </StepperStep>
<StepperStep label="Langkah 4" description="Unggah Dokumen"> <StepperStep label="Langkah 4" description="Unggah Dokumen">
<Text fz="sm">Unggah akta pendirian, surat izin, dan dokumen wajib lainnya</Text> <Text fz={{ base: 'xs', md: 'sm' }} lh="sm">
Unggah akta pendirian, surat izin, dan dokumen wajib lainnya
</Text>
</StepperStep> </StepperStep>
<StepperStep label="Langkah 5" description="Verifikasi Instansi"> <StepperStep label="Langkah 5" description="Verifikasi Instansi">
<Text fz="sm">Menunggu verifikasi dan persetujuan dari pihak berwenang</Text> <Text fz={{ base: 'xs', md: 'sm' }} lh="sm">
Menunggu verifikasi dan persetujuan dari pihak berwenang
</Text>
</StepperStep> </StepperStep>
<StepperStep label="Langkah 6" description="Terbit NIB"> <StepperStep label="Langkah 6" description="Terbit NIB">
<Text fz="sm">Menerima NIB sebagai identitas resmi usaha</Text> <Text fz={{ base: 'xs', md: 'sm' }} lh="sm">
Menerima NIB sebagai identitas resmi usaha
</Text>
</StepperStep> </StepperStep>
<StepperCompleted> <StepperCompleted>
<Center> <Center>
<Stack align="center" gap="xs"> <Stack align="center" gap="xs">
<IconCheck size={40} color="green" /> <IconCheck size={40} color="green" />
<Text fz="sm" fw={500}>Proses pendaftaran selesai</Text> <Text fz={{ base: 'xs', md: 'sm' }} fw={500} lh="sm">
Proses pendaftaran selesai
</Text>
</Stack> </Stack>
</Center> </Center>
</StepperCompleted> </StepperCompleted>
@@ -159,7 +173,7 @@ function PelayananPerizinanBerusaha() {
)} )}
</Box> </Box>
<Text fz="sm" ta="justify" c="dimmed" mt="md"> <Text fz={{ base: 'xs', md: 'sm' }} ta="justify" c="black" lh="sm" mt="md">
Catatan: Persyaratan dan prosedur dapat berubah sewaktu-waktu. Untuk informasi resmi terbaru, silakan kunjungi situs{' '} Catatan: Persyaratan dan prosedur dapat berubah sewaktu-waktu. Untuk informasi resmi terbaru, silakan kunjungi situs{' '}
<a href="https://oss.go.id/" target="_blank" rel="noopener noreferrer"> <a href="https://oss.go.id/" target="_blank" rel="noopener noreferrer">
oss.go.id oss.go.id

View File

@@ -2,7 +2,7 @@
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa'; import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { BackgroundImage, Box, Button, Center, Group, Pagination, SimpleGrid, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; import { BackgroundImage, Box, Button, Center, Group, Pagination, SimpleGrid, Skeleton, Stack, Text, Title, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconFileDescription, IconInfoCircle } from '@tabler/icons-react'; import { IconFileDescription, IconInfoCircle } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@@ -35,7 +35,7 @@ function PelayananSuratKeterangan({ search }: { search: string }) {
<Center py="xl"> <Center py="xl">
<Stack align="center" gap="xs"> <Stack align="center" gap="xs">
<IconFileDescription size={40} stroke={1.5} color={colors["blue-button"]} /> <IconFileDescription size={40} stroke={1.5} color={colors["blue-button"]} />
<Text c="dimmed" ta="center"> <Text c="dimmed" ta="center" fz={{ base: 'sm', md: 'md' }} lh="sm">
Tidak ada layanan surat keterangan yang ditemukan Tidak ada layanan surat keterangan yang ditemukan
</Text> </Text>
</Stack> </Stack>
@@ -48,9 +48,9 @@ function PelayananSuratKeterangan({ search }: { search: string }) {
<Group justify="space-between" align="center" mb="md"> <Group justify="space-between" align="center" mb="md">
<Group gap="xs"> <Group gap="xs">
<IconFileDescription size={28} stroke={1.8} /> <IconFileDescription size={28} stroke={1.8} />
<Text fz={{ base: "h4", md: "h2" }} fw={700}> <Title order={2} c="black">
Layanan Surat Keterangan Layanan Surat Keterangan
</Text> </Title>
</Group> </Group>
<Tooltip label="Pilih layanan surat keterangan sesuai kebutuhan Anda" withArrow> <Tooltip label="Pilih layanan surat keterangan sesuai kebutuhan Anda" withArrow>
<IconInfoCircle size={22} stroke={1.8} /> <IconInfoCircle size={22} stroke={1.8} />
@@ -82,15 +82,15 @@ function PelayananSuratKeterangan({ search }: { search: string }) {
style={{ borderRadius: 16 }} style={{ borderRadius: 16 }}
/> />
<Stack justify="space-between" h="100%" gap="md" p="lg" pos="relative"> <Stack justify="space-between" h="100%" gap="md" p="lg" pos="relative">
<Text <Title
order={3}
c="white" c="white"
fw={600}
fz="lg"
ta="center" ta="center"
lineClamp={2} lineClamp={2}
lh="sm"
> >
{v.name} {v.name}
</Text> </Title>
<Group justify="center"> <Group justify="center">
<Button <Button
size="md" size="md"

View File

@@ -42,9 +42,10 @@ function PelayananTelunjukSaktiDesa() {
return ( return (
<Box> <Box>
<Title order={2} mb="lg" fz={{ base: 22, md: 28 }} fw={700} style={{ lineHeight: 1.4 }}> <Title order={2} mb="lg" fw={700} style={{ lineHeight: 1.3 }} ta="left">
Layanan Telunjuk Sakti Desa <br /> Layanan Telunjuk Sakti Desa
<Text span c="dimmed" fz="lg" fw={400}> <Text span c="black" fz={{ base: 'sm', md: 'md' }} fw={400} style={{ lineHeight: 1.5 }}>
{' '}
Terwujudnya sistem administrasi kependudukan terintegrasi berbasis elektronik, cerdas, dan aman Terwujudnya sistem administrasi kependudukan terintegrasi berbasis elektronik, cerdas, dan aman
</Text> </Text>
</Title> </Title>
@@ -53,7 +54,7 @@ function PelayananTelunjukSaktiDesa() {
<Skeleton h={400} radius="lg" /> <Skeleton h={400} radius="lg" />
) : data.length === 0 ? ( ) : data.length === 0 ? (
<Card shadow="sm" radius="lg" withBorder> <Card shadow="sm" radius="lg" withBorder>
<Text c="dimmed" ta="center" py="xl"> <Text c="black" ta="center" py="xl" fz={{ base: 'sm', md: 'md' }} lh={1.5}>
Belum ada layanan tersedia untuk saat ini Belum ada layanan tersedia untuk saat ini
</Text> </Text>
</Card> </Card>
@@ -72,9 +73,9 @@ function PelayananTelunjukSaktiDesa() {
}} }}
> >
<Stack gap="sm"> <Stack gap="sm">
<Text fw={700} fz="lg" lh={1.4}> <Title order={3} fw={700} lh={1.3}>
{v.name} {v.name}
</Text> </Title>
<Flex gap="xs" align="center"> <Flex gap="xs" align="center">
<IconExternalLink size={18} stroke={1.5} /> <IconExternalLink size={18} stroke={1.5} />
<Text <Text
@@ -82,7 +83,7 @@ function PelayananTelunjukSaktiDesa() {
href={v.link} href={v.link}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
fz="sm" fz={{ base: 'xs', md: 'sm' }}
c="blue" c="blue"
td="underline" td="underline"
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}

View File

@@ -1,58 +1,94 @@
'use client' 'use client'
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman'; import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Container, Group, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { Box, Container, Group, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { useParams } from 'next/navigation'; import { useParams } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import BackButton from '../../../layanan/_com/BackButto';
import NewsReader from '@/app/darmasaba/_com/NewsReader'; import NewsReader from '@/app/darmasaba/_com/NewsReader';
import BackButton from '../../../layanan/_com/BackButto';
function Page() { function Page() {
const detail = useProxy(stateDesaPengumuman.pengumuman.findUnique) const detail = useProxy(stateDesaPengumuman.pengumuman.findUnique);
const params = useParams();
const params = useParams()
useShallowEffect(() => { useShallowEffect(() => {
stateDesaPengumuman.pengumuman.findUnique.load(params?.id as string) stateDesaPengumuman.pengumuman.findUnique.load(params?.id as string);
}, []) }, []);
if (!detail.data) { if (!detail.data) {
return ( return (
<Box> <Box>
<Skeleton h={400} /> <Skeleton h={400} />
</Box> </Box>
) );
} }
return ( return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="md"> <Stack pos="relative" bg={colors.Bg} py="xl" gap="md">
{/* Header */} {/* Header */}
<Box px={{ base: "md", md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
<BackButton /> <BackButton />
</Box> </Box>
<Container size="lg" px="md"> <Container size="lg" px="md">
<Group> <Group>
<NewsReader /> <NewsReader />
</Group> </Group>
<Stack gap="xs" >
<Group justify={"space-between"} align={"center"}> <Stack gap="xs">
<Text fz={{ base: "2rem", md: "2rem" }} c={colors["blue-button"]} fw="bold" > <Group justify="space-between" align="flex-start" wrap="wrap">
{detail.data?.judul} <Title
order={1}
c={colors['blue-button']}
fz={{ base: 28, md: 36 }}
style={{
wordBreak: 'break-word',
flex: '1 1 auto',
minWidth: 0
}}
>
{detail.data?.judul}
</Title>
<Paper bg={colors['blue-button']} p={8} style={{ flexShrink: 0 }}>
<Text c={colors['white-1']} fz={{ base: 'xs', md: 'sm' }} lh={1.2}>
{detail.data?.CategoryPengumuman?.name}
</Text> </Text>
<Group justify='end'> </Paper>
<Paper bg={colors['blue-button']} p={5}> </Group>
<Text c={colors['white-1']}>{detail.data?.CategoryPengumuman?.name}</Text>
</Paper> <Paper
</Group> bg={colors['white-1']}
</Group> p="md"
<Paper bg={colors["white-1"]} p="md"> w="100%"
<Text px="lg" id='news-content' fz={"md"} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: detail.data?.content }} /> mih={{ base: 200, md: 300 }}
<Text px="lg" fz={"md"} c={colors["blue-button"]} fw="bold" > >
<Text
px="lg"
id="news-content"
fz={{ base: 14, md: 16 }}
lh={{ base: 1.6, md: 1.6 }}
style={{
wordBreak: 'break-word',
whiteSpace: 'normal',
width: '100%'
}}
dangerouslySetInnerHTML={{ __html: detail.data?.content }}
/>
<Text
px="lg"
fz={{ base: 12, md: 14 }}
c={colors['blue-button']}
fw="bold"
lh={{ base: 1.4, md: 1.4 }}
mt="md"
>
{new Date(detail.data?.createdAt).toLocaleDateString('id-ID', { {new Date(detail.data?.createdAt).toLocaleDateString('id-ID', {
weekday: 'long', weekday: 'long',
day: 'numeric', day: 'numeric',
month: 'long', month: 'long',
year: 'numeric' year: 'numeric',
})} })}
</Text> </Text>
</Paper> </Paper>

View File

@@ -2,14 +2,13 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman'; import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Container, Group, Paper, Stack, Text } from '@mantine/core'; import { Box, Container, Group, Paper, Stack, Text, Title } from '@mantine/core';
import { IconCalendar } from '@tabler/icons-react'; import { IconCalendar } from '@tabler/icons-react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import BackButton from '../../layanan/_com/BackButto'; import BackButton from '../../layanan/_com/BackButto';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useParams } from 'next/navigation'; import { useParams } from 'next/navigation';
function Page() { function Page() {
const unwrappedParams = useParams(); const unwrappedParams = useParams();
const kategoriState = useProxy(stateDesaPengumuman); const kategoriState = useProxy(stateDesaPengumuman);
@@ -26,45 +25,82 @@ function Page() {
<Box px={{ base: "md", md: 100 }}> <Box px={{ base: "md", md: 100 }}>
<BackButton /> <BackButton />
</Box> </Box>
<Container size="lg" px="md" >
<Stack align="center" gap="0" > <Container size="lg" px="md">
<Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" ta="center"> <Stack align="center" gap="xs">
<Title
order={1}
c={colors["blue-button"]}
ta="center"
style={{ fontWeight: 'bold' }}
>
{categoryName.split('-').map(word => {categoryName.split('-').map(word =>
word.charAt(0).toUpperCase() + word.slice(1) word.charAt(0).toUpperCase() + word.slice(1)
).join(' ')} ).join(' ')}
</Text> </Title>
<Text ta="center" px="md" pb={10}> <Text
ta="center"
px="md"
pb="sm"
fz={{ base: 'sm', md: 'md' }}
lh={{ base: 1.5, md: 1.6 }}
c="dimmed"
>
Informasi dan pengumuman resmi terkait {categoryName.split('-').join(' ')} Informasi dan pengumuman resmi terkait {categoryName.split('-').join(' ')}
</Text> </Text>
</Stack> </Stack>
</Container> </Container>
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
{!kategoriState.pengumuman.findMany.data?.length ? ( {!kategoriState.pengumuman.findMany.data?.length ? (
<Paper p="lg" radius="md" shadow="md" bg={colors["white-1"]}> <Paper p="lg" radius="md" shadow="md" bg={colors["white-1"]}>
Tidak ada pengumuman yang ditemukan <Text
fz={{ base: 'sm', md: 'md' }}
ta="center"
c="dimmed"
>
Tidak ada pengumuman yang ditemukan
</Text>
</Paper> </Paper>
) : kategoriState.pengumuman.findMany.data?.map((v, k) => { ) : (
return ( kategoriState.pengumuman.findMany.data?.map((v, k) => (
<Paper mb={10} key={k} withBorder p="lg" radius="md" shadow="md" bg={colors["white-1"]}> <Paper
<Text fz={'h3'}>{v.judul}</Text> mb="md"
<Group style={{ color: 'black' }} pb={20}> key={k}
withBorder
p="lg"
radius="md"
shadow="md"
bg={colors["white-1"]}
>
<Title order={3}>{v.judul}</Title>
<Group style={{ color: 'black' }} pb="sm">
<Group gap="xs"> <Group gap="xs">
<IconCalendar size={18} /> <IconCalendar size={18} />
<Text size="sm"> <Text
{v.createdAt ? new Date(v.createdAt).toLocaleDateString('id-ID', { fz={{ base: 'xs', md: 'sm' }}
day: 'numeric', lh={{ base: 1.4, md: 1.5 }}
month: 'long', >
year: 'numeric', {v.createdAt
}) : 'No date available'} ? new Date(v.createdAt).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'long',
year: 'numeric',
})
: 'No date available'}
</Text> </Text>
</Group> </Group>
</Group> </Group>
<Text ta={'justify'}> <Text
ta="justify"
fz={{ base: 'sm', md: 'md' }}
lh={{ base: 1.6, md: 1.7 }}
>
{v.deskripsi} {v.deskripsi}
</Text> </Text>
</Paper> </Paper>
) ))
})} )}
</Box> </Box>
</Stack> </Stack>
); );

View File

@@ -9,7 +9,6 @@ import {
Center, Center,
Container, Container,
Divider, Divider,
Flex,
Grid, Grid,
GridCol, GridCol,
Group, Group,
@@ -22,7 +21,7 @@ import {
Text, Text,
TextInput, TextInput,
Title, Title,
UnstyledButton, UnstyledButton
} from '@mantine/core'; } from '@mantine/core';
import { IconCalendar, IconClock, IconSearch } from '@tabler/icons-react'; import { IconCalendar, IconClock, IconSearch } from '@tabler/icons-react';
import { useTransitionRouter } from 'next-view-transitions'; import { useTransitionRouter } from 'next-view-transitions';
@@ -98,10 +97,14 @@ function Page() {
<Container size="lg" px="md"> <Container size="lg" px="md">
<Stack align="center" gap="0"> <Stack align="center" gap="0">
<Text fz={{ base: '2rem', md: '3.4rem' }} c={colors['blue-button']} fw="bold" ta="center"> <Title
order={1}
c={colors['blue-button']}
ta="center"
>
Pengumuman Desa Darmasaba Pengumuman Desa Darmasaba
</Text> </Title>
<Text ta="center" px="md" pb={10}> <Text ta="center" px="md" pb={10} fz={{ base: 'sm', md: 'md' }} lh="sm">
Informasi dan pengumuman resmi terkait kegiatan dan kebijakan Desa Darmasaba Informasi dan pengumuman resmi terkait kegiatan dan kebijakan Desa Darmasaba
</Text> </Text>
</Stack> </Stack>
@@ -126,17 +129,17 @@ function Page() {
withCloseButton={false} withCloseButton={false}
title={item.CategoryPengumuman?.name || 'Pengumuman'} title={item.CategoryPengumuman?.name || 'Pengumuman'}
> >
<Stack gap={"xs"}> <Stack gap="xs">
<Text fz="sm" fw="bold" c="black" style={{ textTransform: 'uppercase' }}> <Text fz={{ base: 'sm', md: 'sm' }} fw="bold" c="black" style={{ textTransform: 'uppercase' }}>
{item.judul} {item.judul}
</Text> </Text>
<Text ta="justify" fz="sm" c="black" lineClamp={3} dangerouslySetInnerHTML={{ __html: item.deskripsi }} /> <Text ta="justify" fz={{ base: 'xs', md: 'sm' }} c="black" lineClamp={3} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</Stack> </Stack>
<Flex pt={20} gap="md" justify="space-between"> <Group pt={20} gap="md" justify="space-between">
<Group style={{ color: 'black' }}> <Group style={{ color: 'black' }}>
<Group gap="xs"> <Group gap="xs">
<IconCalendar size={18} /> <IconCalendar size={18} />
<Text size="sm"> <Text fz={{ base: 'xs', md: 'sm' }}>
{new Date(item.createdAt).toLocaleDateString('id-ID', { {new Date(item.createdAt).toLocaleDateString('id-ID', {
weekday: 'long', weekday: 'long',
day: 'numeric', day: 'numeric',
@@ -147,7 +150,7 @@ function Page() {
</Group> </Group>
<Group gap="xs"> <Group gap="xs">
<IconClock size={18} /> <IconClock size={18} />
<Text size="sm"> <Text fz={{ base: 'xs', md: 'sm' }}>
{new Date(item.createdAt).toLocaleTimeString('id-ID', { {new Date(item.createdAt).toLocaleTimeString('id-ID', {
hour: '2-digit', hour: '2-digit',
minute: '2-digit', minute: '2-digit',
@@ -157,11 +160,11 @@ function Page() {
</Group> </Group>
</Group> </Group>
<Anchor variant="transparent" href={`/darmasaba/desa/pengumuman/${item.CategoryPengumuman?.name}/${item.id}`}> <Anchor variant="transparent" href={`/darmasaba/desa/pengumuman/${item.CategoryPengumuman?.name}/${item.id}`}>
<Text fs="unset" c={colors['blue-button']} fz="sm"> <Text fs="unset" c={colors['blue-button']} fz={{ base: 'xs', md: 'sm' }}>
Baca Selengkapnya Baca Selengkapnya
</Text> </Text>
</Anchor> </Anchor>
</Flex> </Group>
</Notification> </Notification>
)) ))
)} )}
@@ -169,19 +172,19 @@ function Page() {
<Paper p="md"> <Paper p="md">
<Stack gap="xs"> <Stack gap="xs">
<Text fw="bold" fz="lg" c={colors['blue-button']}> <Title order={3} c={colors['blue-button']}>
Kategori Kategori
</Text> </Title>
{stateDesaPengumuman.category.findMany.data?.map((v: any, k) => { {stateDesaPengumuman.category.findMany.data?.map((v: any, k) => {
const count = v._count?.pengumumans || 0; const count = v._count?.pengumumans || 0;
return ( return (
<UnstyledButton component={Link} href={`/darmasaba/desa/pengumuman/${v.name}`} key={k}> <UnstyledButton component={Link} href={`/darmasaba/desa/pengumuman/${v.name}`} key={k}>
<Paper bg={colors['BG-trans']} p={5}> <Paper bg={colors['BG-trans']} p={5}>
<Group px={3} justify="space-between"> <Group px={3} justify="space-between">
<Text fz="md" c="black"> <Text fz={{ base: 'sm', md: 'md' }} c="black">
{v.name} {v.name}
</Text> </Text>
<Text fz="md" c="black"> <Text fz={{ base: 'sm', md: 'md' }} c="black">
{count} {count}
</Text> </Text>
</Group> </Group>
@@ -200,7 +203,7 @@ function Page() {
<Divider mb={10} color={colors['blue-button']} /> <Divider mb={10} color={colors['blue-button']} />
<Grid> <Grid>
<GridCol span={{ base: 12, md: 8 }}> <GridCol span={{ base: 12, md: 8 }}>
<Title order={3}>Daftar Pengumuman</Title> <Title order={2}>Daftar Pengumuman</Title>
</GridCol> </GridCol>
<GridCol span={{ base: 12, md: 4 }}> <GridCol span={{ base: 12, md: 4 }}>
<TextInput <TextInput
@@ -210,6 +213,7 @@ function Page() {
w="100%" w="100%"
value={searchInput} value={searchInput}
onChange={(e) => setSearchInput(e.target.value)} onChange={(e) => setSearchInput(e.target.value)}
fz={{ base: 'sm', md: 'md' }}
/> />
</GridCol> </GridCol>
</Grid> </Grid>
@@ -223,7 +227,9 @@ function Page() {
</SimpleGrid> </SimpleGrid>
) : !state.findMany.data?.length ? ( ) : !state.findMany.data?.length ? (
<Notification withCloseButton={false} h={100}> <Notification withCloseButton={false} h={100}>
Tidak ada pengumuman yang ditemukan <Text fz={{ base: 'sm', md: 'md' }} ta="center">
Tidak ada pengumuman yang ditemukan
</Text>
</Notification> </Notification>
) : ( ) : (
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg" verticalSpacing="lg"> <SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg" verticalSpacing="lg">
@@ -231,26 +237,26 @@ function Page() {
<Paper key={item.id} p="md" withBorder radius="md" h="100%"> <Paper key={item.id} p="md" withBorder radius="md" h="100%">
<Stack h="100%" justify="space-between"> <Stack h="100%" justify="space-between">
<div> <div>
<Text fw={600} c={colors['blue-button']} mb={5}> <Text fw={600} c={colors['blue-button']} mb={5} fz={{ base: 'sm', md: 'md' }}>
{item.CategoryPengumuman?.name || 'Pengumuman'} {item.CategoryPengumuman?.name || 'Pengumuman'}
</Text> </Text>
<Text fz="lg" fw={700} mb="sm" lineClamp={2} style={{ textTransform: 'uppercase' }}> <Text fw={700} mb="sm" lineClamp={2} style={{ textTransform: 'uppercase' }} fz={{ base: 'sm', md: 'lg' }}>
{item.judul} {item.judul}
</Text> </Text>
<Text <Text
fz="sm"
c="dimmed" c="dimmed"
lineClamp={4} lineClamp={4}
dangerouslySetInnerHTML={{ __html: item.deskripsi }} dangerouslySetInnerHTML={{ __html: item.deskripsi }}
mb="md" mb="md"
style={{wordBreak: "break-word", whiteSpace: "normal"}} style={{ wordBreak: "break-word", whiteSpace: "normal" }}
fz={{ base: 'xs', md: 'sm' }}
/> />
</div> </div>
<div> <div>
<Group mb="sm" c="dimmed"> <Group mb="sm" c="dimmed">
<Group gap={5}> <Group gap={5}>
<IconCalendar size={16} /> <IconCalendar size={16} />
<Text size="xs"> <Text fz={{ base: 'xs', md: 'xs' }}>
{new Date(item.createdAt).toLocaleDateString('id-ID', { {new Date(item.createdAt).toLocaleDateString('id-ID', {
day: 'numeric', day: 'numeric',
month: 'short', month: 'short',
@@ -260,19 +266,19 @@ function Page() {
</Group> </Group>
<Group gap={5}> <Group gap={5}>
<IconClock size={16} /> <IconClock size={16} />
<Text size="xs"> <Text fz={{ base: 'xs', md: 'xs' }}>
{new Date(item.createdAt).toLocaleTimeString('id-ID', { {new Date(item.createdAt).toLocaleTimeString('id-ID', {
hour: '2-digit', hour: '2-digit',
minute: '2-digit', minute: '2-digit',
})} })}
</Text> </Text>
</Group> </Group>
<Anchor variant="transparent" href={`/darmasaba/desa/pengumuman/${item.CategoryPengumuman?.name}/${item.id}`}>
<Text fw={600} c={colors['blue-button']} fz={{ base: 'sm', md: 'sm' }}>
Baca Selengkapnya
</Text>
</Anchor>
</Group> </Group>
<Anchor variant="transparent" href={`/darmasaba/desa/pengumuman/${item.CategoryPengumuman?.name}/${item.id}`}>
<Text fw={600} c={colors['blue-button']} size="sm">
Baca Selengkapnya
</Text>
</Anchor>
</div> </div>
</Stack> </Stack>
</Paper> </Paper>
@@ -289,6 +295,7 @@ function Page() {
siblings={1} siblings={1}
boundaries={1} boundaries={1}
withEdges withEdges
fz={{ base: 'xs', md: 'sm' }}
/> />
</Center> </Center>
</Stack> </Stack>

View File

@@ -9,6 +9,7 @@ import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import BackButton from '../../layanan/_com/BackButto'; import BackButton from '../../layanan/_com/BackButto';
function Page() { function Page() {
const params = useParams<{ id: string }>(); const params = useParams<{ id: string }>();
const id = Array.isArray(params.id) ? params.id[0] : params.id; const id = Array.isArray(params.id) ? params.id[0] : params.id;
@@ -35,7 +36,9 @@ function Page() {
<Center h="80vh"> <Center h="80vh">
<Stack align="center" gap="md"> <Stack align="center" gap="md">
<Loader size="lg" color="blue" /> <Loader size="lg" color="blue" />
<Text c="dimmed" fz="sm">Sedang memuat informasi...</Text> <Text c="dimmed" fz={{ base: 'xs', md: 'sm' }} ta="center">
Sedang memuat informasi...
</Text>
</Stack> </Stack>
</Center> </Center>
); );
@@ -46,28 +49,31 @@ function Page() {
<Center h="80vh"> <Center h="80vh">
<Stack align="center" gap="sm"> <Stack align="center" gap="sm">
<IconMoodSad size={64} stroke={1.5} color="var(--mantine-color-blue-6)" /> <IconMoodSad size={64} stroke={1.5} color="var(--mantine-color-blue-6)" />
<Title order={3}>Data Tidak Ditemukan</Title> <Title order={3} ta="center">
<Text c="dimmed" fz="sm">Mohon periksa kembali atau coba beberapa saat lagi</Text> Data Tidak Ditemukan
</Title>
<Text c="dimmed" fz={{ base: 'xs', md: 'sm' }} ta="center">
Mohon periksa kembali atau coba beberapa saat lagi
</Text>
</Stack> </Stack>
</Center> </Center>
); );
} }
return ( return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="xl" px={{ base: "md", md: 0 }}> <Stack pos="relative" bg={colors.Bg} py="xl" gap="xl" px={{ base: 'md', md: 0 }}>
<Box px={{ base: "md", md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
<BackButton /> <BackButton />
</Box> </Box>
<Container w={{ base: "100%", md: "60%" }}> <Container w={{ base: '100%', md: '60%' }}>
<Paper radius="2xl" shadow="lg" p="xl" withBorder> <Paper radius="2xl" shadow="lg" p="xl" withBorder>
<Stack gap="lg" align="center"> <Stack gap="lg" align="center">
<Title ta="center" fz={{ base: "2rem", md: "3rem" }} c={colors["blue-button"]} fw={800}> <Title order={1} ta="center" c={colors['blue-button']} fw={800}>
{state.findUnique.data?.name} {state.findUnique.data?.name}
</Title> </Title>
<Text ta="center" fw={600} fz={{ base: "md", md: "lg" }} c="dimmed"> <Text ta="center" fw={600} fz={{ base: 'md', md: 'lg' }} c="dimmed">
Informasi & Pelayanan Potensi Desa Digital Informasi & Pelayanan Potensi Desa Digital
</Text> </Text>
{/* ✅ Bagian gambar dibuat konsisten tanpa CSS manual */}
<Box <Box
w="100%" w="100%"
h={{ base: 220, md: 400 }} h={{ base: 220, md: 400 }}
@@ -87,7 +93,15 @@ function Page() {
radius="lg" radius="lg"
/> />
</Box> </Box>
<Text py="md" fz={{ base: "sm", md: "md" }} ta="justify" lh={1.8} dangerouslySetInnerHTML={{ __html: state.findUnique.data?.deskripsi || 'Belum ada deskripsi untuk potensi desa ini.' }} /> <Text
py="md"
fz={{ base: 'sm', md: 'md' }}
ta="justify"
lh={{ base: 1.6, md: 1.8 }}
dangerouslySetInnerHTML={{
__html: state.findUnique.data?.content || 'Belum ada deskripsi untuk potensi desa ini.',
}}
/>
</Stack> </Stack>
</Paper> </Paper>
</Container> </Container>

View File

@@ -2,7 +2,7 @@
'use client' 'use client'
import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi'; import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { BackgroundImage, Box, Button, Center, Flex, Group, Paper, SimpleGrid, Skeleton, Stack, Text } from '@mantine/core'; import { BackgroundImage, Box, Button, Flex, Group, Paper, SimpleGrid, Skeleton, Stack, Text, Title } from '@mantine/core';
import { IconEye } from '@tabler/icons-react'; import { IconEye } from '@tabler/icons-react';
import { useTransitionRouter } from 'next-view-transitions'; import { useTransitionRouter } from 'next-view-transitions';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -41,10 +41,10 @@ function Page() {
<Box px={{ base: "md", md: 100 }}> <Box px={{ base: "md", md: 100 }}>
<Flex justify="space-between" align="center" direction={{ base: "column", md: "row" }} gap="lg"> <Flex justify="space-between" align="center" direction={{ base: "column", md: "row" }} gap="lg">
<Stack gap="sm" maw={600}> <Stack gap="sm" maw={600}>
<Text fz={{ base: "2rem", md: "3rem" }} fw={900} c={colors["blue-button"]} lh={1.2}> <Title order={1} fz={{ base: 28, md: 36 }} lh={1.2} c={colors["blue-button"]}>
Potensi Desa Darmasaba Potensi Desa Darmasaba
</Text> </Title>
<Text fz="lg" ta="justify"> <Text fz={{ base: 14, md: 16 }} lh={1.6} ta="justify">
Temukan berbagai potensi unggulan, peluang, dan daya tarik yang menjadikan Desa Darmasaba istimewa. Temukan berbagai potensi unggulan, peluang, dan daya tarik yang menjadikan Desa Darmasaba istimewa.
</Text> </Text>
</Stack> </Stack>
@@ -58,18 +58,18 @@ function Page() {
> >
<Flex justify="center" align="center" gap="xl"> <Flex justify="center" align="center" gap="xl">
<Box> <Box>
<Text ta="center" fz="2rem" fw={800} c="white"> <Text ta="center" fz={{ base: 20, md: 32 }} fw={800} c="white" lh={1.2}>
{data?.filter(item => item.kategori?.nama.toLowerCase() !== 'wisata').length || 0} {data?.filter(item => item.kategori?.nama.toLowerCase() !== 'wisata').length || 0}
</Text> </Text>
<Text ta="center" fz="sm" c="white" fw={500}> <Text ta="center" fz={{ base: 12, md: 14 }} c="white" fw={500}>
Potensi Potensi
</Text> </Text>
</Box> </Box>
<Box> <Box>
<Text ta="center" fz="2rem" fw={800} c="white"> <Text ta="center" fz={{ base: 20, md: 32 }} fw={800} c="white" lh={1.2}>
{data?.filter(item => item.kategori?.nama.toLowerCase() === 'wisata').length || 0} {data?.filter(item => item.kategori?.nama.toLowerCase() === 'wisata').length || 0}
</Text> </Text>
<Text ta="center" fz="sm" c="white" fw={500}> <Text ta="center" fz={{ base: 12, md: 14 }} c="white" fw={500}>
Wisata Wisata
</Text> </Text>
</Box> </Box>
@@ -98,7 +98,6 @@ function Page() {
transition: 'transform 0.3s ease' transition: 'transform 0.3s ease'
}} }}
> >
{/* Overlay with smooth transition */}
<Box <Box
pos="absolute" pos="absolute"
inset={0} inset={0}
@@ -112,7 +111,6 @@ function Page() {
/> />
<Stack justify="space-between" h="100%" gap="md" p="lg" pos="relative"> <Stack justify="space-between" h="100%" gap="md" p="lg" pos="relative">
{/* Kategori badge - always visible */}
<Group> <Group>
<Paper <Paper
radius="lg" radius="lg"
@@ -121,15 +119,12 @@ function Page() {
shadow="md" shadow="md"
withBorder withBorder
bg="rgba(255,255,255,0.9)" bg="rgba(255,255,255,0.9)"
style={{ style={{ transition: 'all 0.3s ease' }}
transition: 'all 0.3s ease'
}}
> >
<Text fz="sm" fw={600}>{v.kategori?.nama}</Text> <Text fz={{ base: 11, md: 14 }} fw={600}>{v.kategori?.nama}</Text>
</Paper> </Paper>
</Group> </Group>
{/* Nama potensi - visible on hover */}
<Box <Box
style={{ style={{
opacity: hoveredId === v.id ? 1 : 0, opacity: hoveredId === v.id ? 1 : 0,
@@ -138,19 +133,19 @@ function Page() {
pointerEvents: hoveredId === v.id ? 'auto' : 'none' pointerEvents: hoveredId === v.id ? 'auto' : 'none'
}} }}
> >
<Text <Title
order={3}
fw={800} fw={800}
c="white" c="white"
fz="xl" fz={{ base: 18, md: 20 }}
ta="center" ta="center"
lineClamp={2} lineClamp={2}
lh={1.3} lh={1.3}
> >
{v.name} {v.name}
</Text> </Title>
</Box> </Box>
{/* Button - visible on hover */}
<Group <Group
justify="center" justify="center"
style={{ style={{
@@ -169,23 +164,21 @@ function Page() {
gradient={{ from: colors["blue-button"], to: "#4dabf7", deg: 45 }} gradient={{ from: colors["blue-button"], to: "#4dabf7", deg: 45 }}
onClick={() => router.push(`/darmasaba/desa/potensi/${v.id}`)} onClick={() => router.push(`/darmasaba/desa/potensi/${v.id}`)}
> >
Lihat Detail <Text c={'white'} fz={{ base: 12, md: 14 }} fw={500}>Lihat Detail</Text>
</Button> </Button>
</Group> </Group>
</Stack> </Stack>
</BackgroundImage> </BackgroundImage>
)) ))
) : ( ) : (
<Center h={240}> <Stack align="center" gap="xs">
<Stack align="center" gap="xs"> <Text fz={{ base: 14, md: 16 }} fw={600} c="dimmed">
<Text fz="lg" fw={600} c="dimmed"> Belum ada potensi desa
Belum ada potensi desa </Text>
</Text> <Text fz={{ base: 12, md: 14 }} c="dimmed">
<Text fz="sm" c="dimmed"> Data potensi akan tampil di sini setelah tersedia.
Data potensi akan tampil di sini setelah tersedia. </Text>
</Text> </Stack>
</Stack>
</Center>
)} )}
</SimpleGrid> </SimpleGrid>
</Box> </Box>

View File

@@ -26,7 +26,6 @@ function DetailPegawaiUser() {
statePegawai.findUnique.load(params?.id as string); statePegawai.findUnique.load(params?.id as string);
}, []); }, []);
if (!statePegawai.findUnique.data) { if (!statePegawai.findUnique.data) {
return ( return (
<Stack py="lg"> <Stack py="lg">
@@ -41,7 +40,7 @@ function DetailPegawaiUser() {
<Box px={{ base: 'md', md: 100 }} py="xl"> <Box px={{ base: 'md', md: 100 }} py="xl">
{/* Back button */} {/* Back button */}
<Group mb="lg" px={{ base: 'md', md: 100 }}> <Group mb="lg" px={{ base: 'md', md: 100 }}>
<BackButton/> <BackButton />
</Group> </Group>
<Paper <Paper
@@ -69,11 +68,17 @@ function DetailPegawaiUser() {
/> />
{/* Nama & Jabatan */} {/* Nama & Jabatan */}
<Stack align="center" gap={2}> <Stack align="center" gap={4}>
<Title order={3} fw={700} c={colors['blue-button']}> {/* Title utama → H2 karena ini judul profil */}
<Title order={2} c={colors['blue-button']} lh={1.2}>
{data.namaLengkap || '-'} {data.gelarAkademik || ''} {data.namaLengkap || '-'} {data.gelarAkademik || ''}
</Title> </Title>
<Text fz="sm" c="dimmed">
<Text
fz={{ base: 'sm', md: 'md' }}
lh={1.4}
c="dimmed"
>
{data.posisi?.nama || 'Posisi tidak tersedia'} {data.posisi?.nama || 'Posisi tidak tersedia'}
</Text> </Text>
</Stack> </Stack>
@@ -82,7 +87,11 @@ function DetailPegawaiUser() {
<Divider my="lg" /> <Divider my="lg" />
{/* Informasi Detail */} {/* Informasi Detail */}
<Stack gap="md"> <Stack gap="lg">
<Title order={3} lh={1.3}>
Informasi Pegawai
</Title>
<InfoRow label="Email" value={data.email} /> <InfoRow label="Email" value={data.email} />
<InfoRow label="Telepon" value={data.telepon} /> <InfoRow label="Telepon" value={data.telepon} />
<InfoRow label="Alamat" value={data.alamat} multiline /> <InfoRow label="Alamat" value={data.alamat} multiline />
@@ -91,10 +100,10 @@ function DetailPegawaiUser() {
value={ value={
data.tanggalMasuk data.tanggalMasuk
? new Date(data.tanggalMasuk).toLocaleDateString('id-ID', { ? new Date(data.tanggalMasuk).toLocaleDateString('id-ID', {
day: '2-digit', day: '2-digit',
month: 'long', month: 'long',
year: 'numeric', year: 'numeric',
}) })
: '-' : '-'
} }
/> />
@@ -123,11 +132,18 @@ function InfoRow({
}) { }) {
return ( return (
<Box> <Box>
<Text fz="sm" fw={600} c="dark"> <Text
fz={{ base: 'sm', md: 'md' }}
fw={600}
lh={1.3}
c="dark"
>
{label} {label}
</Text> </Text>
<Text <Text
fz="sm" fz={{ base: 'sm', md: 'md' }}
lh={1.5}
c={valueColor || 'dimmed'} c={valueColor || 'dimmed'}
style={{ style={{
whiteSpace: multiline ? 'normal' : 'nowrap', whiteSpace: multiline ? 'normal' : 'nowrap',

View File

@@ -36,11 +36,12 @@ import { useTransitionRouter } from 'next-view-transitions'
import { OrganizationChart } from 'primereact/organizationchart' import { OrganizationChart } from 'primereact/organizationchart'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { useProxy } from 'valtio/utils' import { useProxy } from 'valtio/utils'
import './struktur.css'
import BackButton from '../_com/BackButto'
import { useMediaQuery } from '@mantine/hooks'
export default function StrukturPerangkatDesa() { import './struktur.css'
import { useMediaQuery } from '@mantine/hooks'
import BackButton from '../_com/BackButto'
export default function Page() {
return ( return (
<Box <Box
style={{ style={{
@@ -59,10 +60,11 @@ export default function StrukturPerangkatDesa() {
ta="center" ta="center"
c={colors['blue-button']} c={colors['blue-button']}
fz={{ base: 28, md: 36, lg: 44 }} fz={{ base: 28, md: 36, lg: 44 }}
lh={{ base: 1.05, md: 1.03 }}
> >
Struktur Perangkat Desa Struktur Perangkat Desa
</Title> </Title>
<Text ta="center" c="black" maw={800}> <Text ta="center" c="black" maw={800} fz={{ base: 13, md: 15 }} lh={1.45}>
Gambaran visual peran dan pegawai yang ditugaskan. Arahkan kursor Gambaran visual peran dan pegawai yang ditugaskan. Arahkan kursor
untuk melihat detail atau klik node untuk fokus tampilan. untuk melihat detail atau klik node untuk fokus tampilan.
</Text> </Text>
@@ -105,8 +107,8 @@ function StrukturPerangkatDesaNode() {
<Center py={48}> <Center py={48}>
<Stack align="center" gap="sm"> <Stack align="center" gap="sm">
<Loader size="lg" /> <Loader size="lg" />
<Text fw={600}>Memuat struktur organisasi</Text> <Text fw={600} fz={{ base: 15, md: 16 }} lh={1.2}>Memuat struktur organisasi</Text>
<Text c="dimmed" size="sm"> <Text c="dimmed" fz={{ base: 12, md: 13 }} lh={1.4}>
Mengambil data pegawai dan posisi. Mohon tunggu sebentar. Mengambil data pegawai dan posisi. Mohon tunggu sebentar.
</Text> </Text>
</Stack> </Stack>
@@ -132,10 +134,10 @@ function StrukturPerangkatDesaNode() {
<Center> <Center>
<IconUsers size={56} /> <IconUsers size={56} />
</Center> </Center>
<Title order={3} mt="md"> <Title order={3} mt="md" fz={{ base: 16, md: 18 }} lh={1.15}>
Data pegawai belum tersedia Data pegawai belum tersedia
</Title> </Title>
<Text c="dimmed" mt="xs"> <Text c="dimmed" mt="xs" fz={{ base: 13, md: 14 }} lh={1.4}>
Belum ada data pegawai yang tercatat untuk PPID. Belum ada data pegawai yang tercatat untuk PPID.
</Text> </Text>
<Group justify="center" mt="lg"> <Group justify="center" mt="lg">
@@ -232,11 +234,18 @@ function StrukturPerangkatDesaNode() {
{/* 🔍 Controls */} {/* 🔍 Controls */}
<Paper <Paper
shadow="xs" shadow="xs"
w={{
base: '100%', // Mobile: 100%
sm: '40%', // Tablet: 95%
md: '39%', // Desktop: 70%
lg: '38%', // Desktop L: 60%
xl: '37%', // 4K: 50%
'2xl': '36%', // Ultra-wide: 45%
}}
p="md" p="md"
radius="md" radius="md"
style={{ style={{
background: colors['blue-button'], background: colors['blue-button'], // ⬅️ penting
width: '100%', // ⬅️ penting
maxWidth: '100%', // ⬅️ penting maxWidth: '100%', // ⬅️ penting
overflowX: 'auto' // ⬅️ untuk mencegah overflow overflowX: 'auto' // ⬅️ untuk mencegah overflow
}} }}
@@ -269,30 +278,33 @@ function StrukturPerangkatDesaNode() {
fontSize: '0.875rem', fontSize: '0.875rem',
padding: '6px 12px', padding: '6px 12px',
minHeight: 'auto', minHeight: 'auto',
flexShrink: 0, // 👈 PENTING: mencegah tab mengecil flexShrink: 0,
}, },
}} }}
style={{ width: '100%' }} // 👈 penting
> >
<TabsList <TabsList
style={{ style={{
display: 'flex', display: 'flex',
overflowX: 'auto', overflowX: 'auto',
overflowY: 'hidden', // 👈 tambahkan ini overflowY: 'hidden',
gap: '4px', gap: '4px',
paddingBottom: '4px', paddingBottom: '4px',
flexWrap: 'nowrap', flexWrap: 'nowrap',
WebkitOverflowScrolling: 'touch', // 👈 smooth scroll di iOS WebkitOverflowScrolling: 'touch',
scrollbarWidth: 'thin', // 👈 scrollbar tipis di Firefox scrollbarWidth: 'thin',
msOverflowStyle: '-ms-autohiding-scrollbar', // 👈 untuk IE/Edge msOverflowStyle: '-ms-autohiding-scrollbar',
maxWidth: '100%',
scrollBehavior: 'smooth', // 👈 smooth scroll
}} }}
> >
<TabsTab <TabsTab
value="zoom-out" value="zoom-out"
onClick={handleZoomOut} onClick={handleZoomOut}
leftSection={<IconZoomOut size={16} />} leftSection={<IconZoomOut size={16} />}
style={{ flexShrink: 0 }} // 👈 pastikan tidak mengecil style={{ flexShrink: 0 }}
> >
Zoom Out <Text fz={{ base: 12, sm: 13 }} lh={1} ta="center">Zoom Out</Text>
</TabsTab> </TabsTab>
<Box <Box
@@ -301,7 +313,6 @@ function StrukturPerangkatDesaNode() {
px={12} px={12}
py={6} py={6}
style={{ style={{
fontSize: 14,
fontWeight: 700, fontWeight: 700,
borderRadius: '6px', borderRadius: '6px',
minWidth: 60, minWidth: 60,
@@ -310,10 +321,12 @@ function StrukturPerangkatDesaNode() {
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
flexShrink: 0, flexShrink: 0,
whiteSpace: 'nowrap', // 👈 mencegah text wrap whiteSpace: 'nowrap',
}} }}
> >
{Math.round(scale * 100)}% <Text fz={{ base: 12, sm: 13 }} lh={1} c={colors['blue-button']}>
{Math.round(scale * 100)}%
</Text>
</Box> </Box>
<TabsTab <TabsTab
@@ -322,7 +335,7 @@ function StrukturPerangkatDesaNode() {
leftSection={<IconZoomIn size={16} />} leftSection={<IconZoomIn size={16} />}
style={{ flexShrink: 0 }} style={{ flexShrink: 0 }}
> >
Zoom In <Text fz={{ base: 12, sm: 13 }} lh={1} ta="center">Zoom In</Text>
</TabsTab> </TabsTab>
<TabsTab <TabsTab
@@ -330,7 +343,7 @@ function StrukturPerangkatDesaNode() {
onClick={resetZoom} onClick={resetZoom}
style={{ flexShrink: 0 }} style={{ flexShrink: 0 }}
> >
Reset <Text fz={{ base: 12, sm: 13 }} lh={1} ta="center">Reset</Text>
</TabsTab> </TabsTab>
<TabsTab <TabsTab
@@ -345,7 +358,9 @@ function StrukturPerangkatDesaNode() {
} }
style={{ flexShrink: 0 }} style={{ flexShrink: 0 }}
> >
{isFullscreen ? 'Exit' : 'Fullscreen'} <Text fz={{ base: 12, sm: 13 }} lh={1} ta="center">
{isFullscreen ? 'Exit' : 'Fullscreen'}
</Text>
</TabsTab> </TabsTab>
</TabsList> </TabsList>
</Tabs> </Tabs>
@@ -451,17 +466,17 @@ function NodeCard({ node, router }: any) {
{/* Name */} {/* Name */}
<Text <Text
fw={700} fw={700}
size="sm"
ta="center" ta="center"
c={colors['blue-button']} c={colors['blue-button']}
lineClamp={2} lineClamp={2}
fz={{ base: 13, md: 15 }}
lh={1.2}
style={{ style={{
minHeight: 40, minHeight: 40,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
wordBreak: 'break-word', wordBreak: 'break-word',
lineHeight: 1.3,
}} }}
> >
{name} {name}
@@ -469,18 +484,18 @@ function NodeCard({ node, router }: any) {
{/* Title/Position */} {/* Title/Position */}
<Text <Text
size="xs"
c="dimmed" c="dimmed"
ta="center" ta="center"
fw={500} fw={500}
lineClamp={2} lineClamp={2}
fz={{ base: 12, md: 13 }}
lh={1.3}
style={{ style={{
minHeight: 32, minHeight: 32,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
wordBreak: 'break-word', wordBreak: 'break-word',
lineHeight: 1.2,
}} }}
> >
{title} {title}
@@ -496,14 +511,14 @@ function NodeCard({ node, router }: any) {
mt={8} mt={8}
radius="md" radius="md"
onClick={() => onClick={() =>
router.push(`/darmasaba/desa/profile/struktur-perangkat-desa/${node.data.id}`) router.push(`/darmasaba/desa/profil/struktur-perangkat-desa/${node.data.id}`)
} }
style={{ style={{
height: 32, height: 32,
fontWeight: 600, fontWeight: 600,
}} }}
> >
Lihat Detail <Text fz={{ base: 12, md: 13 }} lh={1} ta="center">Lihat Detail</Text>
</Button> </Button>
)} )}
</Stack> </Stack>

View File

@@ -2,7 +2,7 @@
'use client' 'use client'
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile' import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'
import colors from '@/con/colors' import colors from '@/con/colors'
import { Box, Center, Image, Paper, Skeleton, Stack, Text } from '@mantine/core' import { Box, Center, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core'
import { useEffect } from 'react' import { useEffect } from 'react'
import { useProxy } from 'valtio/utils' import { useProxy } from 'valtio/utils'
@@ -26,6 +26,8 @@ function LambangDesa() {
return ( return (
<Box> <Box>
<Stack align="center" gap="lg"> <Stack align="center" gap="lg">
{/* HEADER */}
<Box pb="lg"> <Box pb="lg">
<Center> <Center>
<Image <Image
@@ -36,17 +38,20 @@ function LambangDesa() {
loading="lazy" loading="lazy"
/> />
</Center> </Center>
<Text
{/* TITLE - H1 */}
<Title
order={1}
c={colors['blue-button']} c={colors['blue-button']}
ta="center" ta="center"
fw={800}
fz={{ base: 28, md: 40 }}
mt="sm" mt="sm"
style={{ letterSpacing: '-0.5px' }} style={{ letterSpacing: '-0.5px' }}
> >
Lambang Desa Lambang Desa
</Text> </Title>
</Box> </Box>
{/* DESKRIPSI */}
<Paper <Paper
p="xl" p="xl"
radius="xl" radius="xl"
@@ -58,15 +63,20 @@ function LambangDesa() {
borderColor: '#e0e9ff', borderColor: '#e0e9ff',
}} }}
> >
<Text <Text
fz={{ base: '1.125rem', md: '1.375rem' }} fz={{ base: 'sm', md: 'md' }} // Body text mobile & desktop
lh={1.8} lh={1.7}
c="dark" c="dark"
ta="justify" ta="justify"
style={{ fontWeight: 400, wordBreak: "break-word", whiteSpace: "normal", }} style={{
dangerouslySetInnerHTML={{ __html: data.deskripsi }} fontWeight: 400,
/> wordBreak: "break-word",
whiteSpace: "normal",
}}
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
/>
</Paper> </Paper>
</Stack> </Stack>
</Box> </Box>
) )

View File

@@ -2,7 +2,7 @@
'use client' 'use client'
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Card, Center, Group, Image, Loader, Paper, Stack, Text } from '@mantine/core'; import { Box, Card, Center, Group, Image, Loader, Paper, Stack, Text, Title } from '@mantine/core';
import { IconPhoto } from '@tabler/icons-react'; import { IconPhoto } from '@tabler/icons-react';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
@@ -21,7 +21,9 @@ function MaskotDesa() {
<Center mih={500}> <Center mih={500}>
<Stack align="center" gap="sm"> <Stack align="center" gap="sm">
<Loader size="lg" color="blue" /> <Loader size="lg" color="blue" />
<Text c="dimmed" fz="sm">Sedang memuat data maskot desa...</Text> <Text c="dimmed" fz={{ base: 'xs', md: 'sm' }}>
Sedang memuat data maskot desa...
</Text>
</Stack> </Stack>
</Center> </Center>
); );
@@ -31,8 +33,21 @@ function MaskotDesa() {
<Box> <Box>
<Stack align="center" gap="xl"> <Stack align="center" gap="xl">
<Stack align="center" gap={10}> <Stack align="center" gap={10}>
<Image src="/pudak-icon.png" alt="Ikon Desa" w={{ base: 160, md: 240 }} loading="lazy"/> <Image
<Text c={colors['blue-button']} ta="center" fw={700} fz={{ base: 28, md: 36 }}>Maskot Desa</Text> src="/pudak-icon.png"
alt="Ikon Desa"
w={{ base: 160, md: 240 }}
loading="lazy"
/>
{/* Page Title */}
<Title
order={1}
ta="center"
c={colors['blue-button']}
>
Maskot Desa
</Title>
</Stack> </Stack>
<Paper <Paper
@@ -42,48 +57,60 @@ function MaskotDesa() {
withBorder withBorder
style={{ background: 'linear-gradient(145deg, #ffffff, #f8f9fa)' }} style={{ background: 'linear-gradient(145deg, #ffffff, #f8f9fa)' }}
> >
{/* Body Description */}
<Text <Text
fz={{ base: 'sm', md: 'lg' }} fz={{ base: 'sm', md: 'lg' }}
lh={1.7} lh={1.7}
ta="justify" ta="justify"
c="dark" c="dark"
dangerouslySetInnerHTML={{ __html: data.deskripsi }} dangerouslySetInnerHTML={{ __html: data.deskripsi }}
style={{wordBreak: "break-word", whiteSpace: "normal"}} style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
/> />
<Group justify="center" gap="lg" mt="lg"> <Group justify="center" gap="lg" mt="lg">
{data.images.length > 0 ? ( {data.images.length > 0 ? (
data.images.map((img, index) => ( data.images.map((img, index) => (
<Card <Card
key={index} key={index}
radius="lg" radius="lg"
shadow="md" shadow="md"
withBorder withBorder
w={220} w={220}
p="sm" p="sm"
style={{ style={{
transition: 'transform 200ms ease, box-shadow 200ms ease', transition: 'transform 200ms ease, box-shadow 200ms ease',
}} }}
className="hover:scale-105 hover:shadow-lg" className="hover:scale-105 hover:shadow-lg"
>
<Image
src={img.image.link}
alt={img.label}
w="100%"
h={200}
fit="cover"
radius="md"
loading="lazy"
/>
{/* Image Label */}
<Text
ta="center"
mt="sm"
fw={600}
fz={{ base: 'xs', md: 'sm' }}
c="dark"
> >
<Image {img.label}
src={img.image.link} </Text>
alt={img.label} </Card>
w="100%"
h={200}
fit="cover"
radius="md"
loading="lazy"
/>
<Text ta="center" mt="sm" fw={600} fz="sm" c="dark">
{img.label}
</Text>
</Card>
)) ))
) : ( ) : (
<Stack align="center" gap="xs" mt="lg"> <Stack align="center" gap="xs" mt="lg">
<IconPhoto size={48} stroke={1.5} color="gray" /> <IconPhoto size={48} stroke={1.5} color="gray" />
<Text c="dimmed" fz="sm">Belum ada gambar maskot yang ditambahkan</Text>
<Text c="dimmed" fz={{ base: 'xs', md: 'sm' }}>
Belum ada gambar maskot yang ditambahkan
</Text>
</Stack> </Stack>
)} )}
</Group> </Group>

View File

@@ -1,35 +1,15 @@
'use client' 'use client'
import { ActionIcon, Box, Flex, Paper, SimpleGrid, Stack, Text } from '@mantine/core'; import { ActionIcon, Box, Flex, Paper, SimpleGrid, Stack, Text, Title } from '@mantine/core';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { IconSparkles } from '@tabler/icons-react'; import { IconSparkles } from '@tabler/icons-react';
import colors from '@/con/colors'; import colors from '@/con/colors';
const dataText = [ const dataText = [
{ { id: 1, title: "Santun", description: "Pelayanan ramah, penuh empati, sopan, dan beretika." },
id: 1, { id: 2, title: "Adaptif", description: "Cepat menyesuaikan diri terhadap perubahan dan selalu proaktif." },
title: "Santun", { id: 3, title: "Inovatif", description: "Berani menciptakan pembaruan dan ide-ide kreatif." },
description: "Pelayanan ramah, penuh empati, sopan, dan beretika." { id: 4, title: "Profesional", description: "Berpengetahuan luas, terampil, dan bertanggung jawab." },
}, { id: 5, title: "Gesit", description: "Cekatan, sigap, dan penuh inisiatif dalam bekerja." },
{
id: 2,
title: "Adaptif",
description: "Cepat menyesuaikan diri terhadap perubahan dan selalu proaktif."
},
{
id: 3,
title: "Inovatif",
description: "Berani menciptakan pembaruan dan ide-ide kreatif."
},
{
id: 4,
title: "Profesional",
description: "Berpengetahuan luas, terampil, dan bertanggung jawab."
},
{
id: 5,
title: "Gesit",
description: "Cekatan, sigap, dan penuh inisiatif dalam bekerja."
},
]; ];
const letters = ["S", "I", "G", "A", "P"]; const letters = ["S", "I", "G", "A", "P"];
@@ -38,11 +18,14 @@ function MotoDesa() {
return ( return (
<Box px={{ base: "md", md: "xl" }}> <Box px={{ base: "md", md: "xl" }}>
<Stack align="center" gap="lg"> <Stack align="center" gap="lg">
{/* Page Title */}
<Box> <Box>
<Text <Title
order={1}
ta="center" ta="center"
fw={800} fw={800}
fz={{ base: "2rem", md: "2.8rem" }} fz={{ base: 28, md: 36 }}
lh={{ base: 1.2, md: 1.3 }}
style={{ style={{
background: "linear-gradient(90deg, #0D5594FF, #094678FF)", background: "linear-gradient(90deg, #0D5594FF, #094678FF)",
WebkitBackgroundClip: "text", WebkitBackgroundClip: "text",
@@ -50,9 +33,10 @@ function MotoDesa() {
}} }}
> >
Moto Desa Darmasaba Moto Desa Darmasaba
</Text> </Title>
</Box> </Box>
{/* Letter Icons */}
<Flex gap={30} pb={40} pt={10} wrap="wrap" justify="center"> <Flex gap={30} pb={40} pt={10} wrap="wrap" justify="center">
{letters.map((letter, i) => ( {letters.map((letter, i) => (
<motion.div <motion.div
@@ -71,7 +55,7 @@ function MotoDesa() {
backdropFilter: "blur(6px)", backdropFilter: "blur(6px)",
}} }}
> >
<Text c="white" fw={800} fz="xl"> <Text c="white" fw={800} fz={{ base: 20, md: 24 }}>
{letter} {letter}
</Text> </Text>
</ActionIcon> </ActionIcon>
@@ -79,6 +63,7 @@ function MotoDesa() {
))} ))}
</Flex> </Flex>
{/* Values Card */}
<Paper <Paper
radius="lg" radius="lg"
p="xl" p="xl"
@@ -90,19 +75,22 @@ function MotoDesa() {
> >
<SimpleGrid cols={{ base: 1, md: 2 }} spacing="xl"> <SimpleGrid cols={{ base: 1, md: 2 }} spacing="xl">
{dataText.map((v) => ( {dataText.map((v) => (
<motion.div <motion.div key={v.id} whileHover={{ scale: 1.02 }} transition={{ duration: 0.2 }}>
key={v.id}
whileHover={{ scale: 1.02 }}
transition={{ duration: 0.2 }}
>
<Stack gap={4}> <Stack gap={4}>
{/* Section Title */}
<Flex align="center" gap="sm"> <Flex align="center" gap="sm">
<IconSparkles size={20} color={colors['blue-button']} /> <IconSparkles size={20} color={colors['blue-button']} />
<Text fw={700} fz={{ base: "lg", md: "xl" }} c={colors['blue-button']}> <Title
order={3}
fw={700}
fz={{ base: 20, md: 24 }}
c={colors['blue-button']}
>
{v.title} {v.title}
</Text> </Title>
</Flex> </Flex>
<Text fz={{ base: "sm", md: "md" }} c="gray.7"> {/* Body Text */}
<Text fz={{ base: 14, md: 16 }} lh={{ base: 1.5, md: 1.6 }} c="gray.7">
{v.description} {v.description}
</Text> </Text>
</Stack> </Stack>
@@ -111,16 +99,15 @@ function MotoDesa() {
</SimpleGrid> </SimpleGrid>
</Paper> </Paper>
{/* Motto Description */}
<Text <Text
ta="center" ta="center"
fw={700} fw={700}
fz={{ base: "md", md: "xl" }} fz={{ base: 15, md: 20 }}
lh={{ base: 1.6, md: 1.8 }}
c="blue.8" c="blue.8"
mt="md" mt="md"
style={{ style={{ maxWidth: 720 }}
maxWidth: 720,
lineHeight: 1.6,
}}
> >
&quot;Berkomitmen menghadirkan pelayanan terbaik dengan semangat{" "} &quot;Berkomitmen menghadirkan pelayanan terbaik dengan semangat{" "}
<Text span fw={800} c="cyan.6"> <Text span fw={800} c="cyan.6">

View File

@@ -2,44 +2,45 @@
'use client' 'use client'
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Divider, Image, Paper, SimpleGrid, Skeleton, Stack, Text } from '@mantine/core'; import { Box, Divider, Image, Paper, SimpleGrid, Skeleton, Stack, Text, Title } from '@mantine/core';
import { IconBriefcase, IconTargetArrow, IconUser, IconUsers } from '@tabler/icons-react'; import { IconBriefcase, IconTargetArrow, IconUser, IconUsers } from '@tabler/icons-react';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function ProfilPerbekel() { function ProfilPerbekel() {
const state = useProxy(stateProfileDesa.profilPerbekel) const state = useProxy(stateProfileDesa.profilPerbekel);
useEffect(() => { useEffect(() => {
state.findUnique.load("edit") state.findUnique.load("edit");
}, []) }, []);
const { data, loading } = state.findUnique const { data, loading } = state.findUnique;
if (loading || !data) { if (loading || !data) {
return ( return (
<Box py={20} px="md"> <Box py={20} px="md">
<Skeleton h={500} radius="lg" /> <Skeleton h={500} radius="lg" />
</Box> </Box>
) );
} }
return ( return (
<Box px="md"> <Box px="md">
{/* ===== PAGE TITLE ===== */}
<Stack align="center" gap={0} mb={40}> <Stack align="center" gap={0} mb={40}>
<Text <Title
order={1}
c={colors['blue-button']} c={colors['blue-button']}
ta="center" ta="center"
fw="bold"
fz={{ base: "2rem", md: "2.8rem" }}
style={{ letterSpacing: "0.5px" }} style={{ letterSpacing: "0.5px" }}
> >
Profil Perbekel Profil Perbekel
</Text> </Title>
<Divider w={120} size="sm" color={colors['blue-button']} mt={10} /> <Divider w={120} size="sm" color={colors['blue-button']} mt={10} />
</Stack> </Stack>
<SimpleGrid cols={{ base: 1, md: 2 }} spacing="xl" pb={50}> <SimpleGrid cols={{ base: 1, md: 2 }} spacing="xl" pb={50}>
{/* ========== FOTO PERBEKEL ========== */}
<Box> <Box>
<Paper <Paper
bg={colors['white-trans-1']} bg={colors['white-trans-1']}
@@ -60,6 +61,8 @@ function ProfilPerbekel() {
}} }}
loading="lazy" loading="lazy"
/> />
{/* ===== NAMA DAN JABATAN ===== */}
<Paper <Paper
bg={colors['blue-button']} bg={colors['blue-button']}
px="lg" px="lg"
@@ -67,22 +70,23 @@ function ProfilPerbekel() {
className="glass3" className="glass3"
py={{ base: 20, md: 50 }} py={{ base: 20, md: 50 }}
> >
<Text c={colors['white-1']} fz={{ base: "lg", md: "h3" }}> <Title order={3} c={colors['white-1']}>
Perbekel Desa Darmasaba Perbekel Desa Darmasaba
</Text> </Title>
<Text
<Title
order={2}
c={colors['white-1']} c={colors['white-1']}
fw="bolder"
fz={{ base: "xl", md: "h2" }}
mt={8} mt={8}
> >
{"I.B. Surya Prabhawa Manuaba, S.H.,M.H.,NL.P."} {"I.B. Surya Prabhawa Manuaba, S.H.,M.H.,NL.P."}
</Text> </Title>
</Paper> </Paper>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>
{/* ========== BIODATA & PENGALAMAN ========== */}
<Paper <Paper
p="xl" p="xl"
bg={colors['white-trans-1']} bg={colors['white-trans-1']}
@@ -92,34 +96,39 @@ function ProfilPerbekel() {
withBorder withBorder
> >
<Stack gap="xl"> <Stack gap="xl">
{/* ===== BIODATA ===== */}
<Box> <Box>
<Stack gap={6}> <Stack gap={6}>
<Stack align="center" gap={6}> <Stack align="center" gap={6}>
<IconUser size={22} /> <IconUser size={22} />
<Text fz={{ base: "1.2rem", md: "1.5rem" }} fw="bold">Biodata</Text> <Title order={3}>Biodata</Title>
</Stack> </Stack>
<Text <Text
fz={{ base: "1rem", md: "1.2rem" }} fz={{ base: "sm", md: "md" }}
ta="justify" ta="justify"
lh={1.6} lh={1.7}
dangerouslySetInnerHTML={{ __html: data.biodata }} dangerouslySetInnerHTML={{ __html: data.biodata }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }} style={{ wordBreak: "break-word" }}
/> />
</Stack> </Stack>
</Box> </Box>
{/* ===== PENGALAMAN ===== */}
<Box> <Box>
<Stack gap={6}> <Stack gap={6}>
<Stack align="center" gap={6}> <Stack align="center" gap={6}>
<IconBriefcase size={22} /> <IconBriefcase size={22} />
<Text fz={{ base: "1.2rem", md: "1.5rem" }} fw="bold">Pengalaman</Text> <Title order={3}>Pengalaman</Title>
</Stack> </Stack>
<Text <Text
fz={{ base: "1rem", md: "1.2rem" }} fz={{ base: "sm", md: "md" }}
ta="left" ta="left"
lh={1.6} lh={1.7}
dangerouslySetInnerHTML={{ __html: data.pengalaman }} dangerouslySetInnerHTML={{ __html: data.pengalaman }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }} style={{ wordBreak: "break-word" }}
/> />
</Stack> </Stack>
</Box> </Box>
@@ -127,6 +136,7 @@ function ProfilPerbekel() {
</Paper> </Paper>
</SimpleGrid> </SimpleGrid>
{/* ========== ORGANISASI & PROGRAM UNGGULAN ========== */}
<Paper <Paper
p="xl" p="xl"
bg={colors['white-trans-1']} bg={colors['white-trans-1']}
@@ -136,35 +146,41 @@ function ProfilPerbekel() {
withBorder withBorder
> >
<Stack gap="xl"> <Stack gap="xl">
{/* ===== PENGALAMAN ORGANISASI ===== */}
<Box> <Box>
<Stack align="center" gap={6} > <Stack align="center" gap={6}>
<IconUsers size={22} /> <IconUsers size={22} />
<Text fz={{ base: "1.2rem", md: "1.5rem" }} fw="bold">Pengalaman Organisasi</Text> <Title order={3}>Pengalaman Organisasi</Title>
</Stack> </Stack>
<Text <Text
fz={{ base: "1rem", md: "1.2rem" }} fz={{ base: "sm", md: "md" }}
ta="justify" ta="justify"
lh={1.6} lh={1.7}
dangerouslySetInnerHTML={{ __html: data.pengalamanOrganisasi }} dangerouslySetInnerHTML={{ __html: data.pengalamanOrganisasi }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }} style={{ wordBreak: "break-word" }}
/> />
</Box> </Box>
{/* ===== PROGRAM UNGGULAN ===== */}
<Box> <Box>
<Stack align="center" gap={6} mb={6}> <Stack align="center" gap={6} mb={6}>
<IconTargetArrow size={22} /> <IconTargetArrow size={22} />
<Text fz={{ base: "1.2rem", md: "1.5rem" }} fw="bold">Program Kerja Unggulan</Text> <Title order={3}>Program Kerja Unggulan</Title>
</Stack> </Stack>
<Box px={10}> <Box px={10}>
<Text <Text
fz={{ base: "1rem", md: "1.2rem" }} fz={{ base: "sm", md: "md" }}
ta="justify" ta="justify"
lh={1.6} lh={1.7}
dangerouslySetInnerHTML={{ __html: data.programUnggulan }} dangerouslySetInnerHTML={{ __html: data.programUnggulan }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }} style={{ wordBreak: "break-word" }}
/> />
</Box> </Box>
</Box> </Box>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

@@ -2,7 +2,7 @@
'use client' 'use client'
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Center, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { Box, Center, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
@@ -26,29 +26,32 @@ function SejarahDesa() {
return ( return (
<Box> <Box>
<Stack align="center" gap="xl"> <Stack align="center" gap="xl">
{/* HEADER ICON + TITLE */}
<Stack align="center" gap="sm"> <Stack align="center" gap="sm">
<Center> <Center>
<Image <Image
src="/darmasaba-icon.png" src="/darmasaba-icon.png"
alt="Ikon Desa Darmasaba" alt="Ikon Desa Darmasaba"
w={{ base: 180, md: 260 }} w={{ base: 160, md: 240 }}
radius="md" radius="md"
style={{ filter: 'drop-shadow(0 4px 12px rgba(0,0,0,0.15))' }} style={{ filter: 'drop-shadow(0 4px 12px rgba(0,0,0,0.15))' }}
loading="lazy" loading="lazy"
/> />
</Center> </Center>
<Center> <Center>
<Text <Title
order={1}
c={colors['blue-button']} c={colors['blue-button']}
ta="center" ta="center"
fw={700}
fz={{ base: '2rem', md: '2.8rem' }}
style={{ letterSpacing: '-0.5px' }} style={{ letterSpacing: '-0.5px' }}
> >
Sejarah Desa Sejarah Desa
</Text> </Title>
</Center> </Center>
</Stack> </Stack>
{/* CONTENT */}
<Paper <Paper
p="xl" p="xl"
radius="lg" radius="lg"
@@ -61,10 +64,14 @@ function SejarahDesa() {
> >
<Stack gap="md"> <Stack gap="md">
<Text <Text
fz={{ base: 'md', md: 'lg' }} fz={{ base: 'sm', md: 'md' }}
lh={1.8} lh={1.75}
ta="justify" ta="justify"
style={{ color: '#2a2a2a', wordBreak: "break-word", whiteSpace: "normal" }} c="dark.7"
style={{
wordBreak: 'break-word',
whiteSpace: 'normal',
}}
dangerouslySetInnerHTML={{ __html: data.deskripsi }} dangerouslySetInnerHTML={{ __html: data.deskripsi }}
/> />
</Stack> </Stack>

View File

@@ -28,8 +28,10 @@ function SemuaPerbekel() {
<Center py="xl"> <Center py="xl">
<Stack align="center" gap="sm"> <Stack align="center" gap="sm">
<IconUser size={48} stroke={1.5} /> <IconUser size={48} stroke={1.5} />
<Title fw="bold" order={2}>Belum ada data Perbekel</Title> <Title order={2} ta="center">Belum ada data Perbekel</Title>
<Text c="dimmed" fz="sm" ta="center">Data mantan Perbekel akan muncul di sini ketika sudah tersedia</Text> <Text c="dimmed" fz={{ base: 'xs', md: 'sm' }} lh={{ base: 1.4, md: 1.6 }} ta="center">
Data mantan Perbekel akan muncul di sini ketika sudah tersedia
</Text>
</Stack> </Stack>
</Center> </Center>
); );
@@ -38,17 +40,20 @@ function SemuaPerbekel() {
return ( return (
<Box> <Box>
<Stack align="center" gap="lg"> <Stack align="center" gap="lg">
<Box> <Title
<Text order={1}
ta="center" ta="center"
fw={900} style={{
fz={{ base: "2rem", md: "2.5rem" }} background: 'linear-gradient(45deg, blue, cyan)',
variant="gradient" WebkitBackgroundClip: 'text',
gradient={{ from: "blue", to: "cyan", deg: 45 }} WebkitTextFillColor: 'transparent',
> }}
Perbekel Dari Masa ke Masa fz={{ base: 28, md: 36 }}
</Text> lh={{ base: 1.2, md: 1.3 }}
</Box> fw={900}
>
Perbekel Dari Masa ke Masa
</Title>
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="xl" w="100%"> <SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="xl" w="100%">
{data.map((v: any, k: number) => ( {data.map((v: any, k: number) => (
@@ -59,9 +64,7 @@ function SemuaPerbekel() {
withBorder withBorder
p="lg" p="lg"
bg="white" bg="white"
style={{ style={{ transition: "all 250ms ease" }}
transition: "all 250ms ease",
}}
className="hover:shadow-xl hover:scale-[1.02]" className="hover:shadow-xl hover:scale-[1.02]"
> >
<Stack gap="md" align="center"> <Stack gap="md" align="center">
@@ -77,17 +80,17 @@ function SemuaPerbekel() {
</Box> </Box>
<Stack gap={4} align="center"> <Stack gap={4} align="center">
<Text fw={700} fz="lg" ta="center"> <Title order={3} fz={{ base: 18, md: 20 }} ta="center" fw={700}>
{v.nama} {v.nama}
</Text> </Title>
<Text c="dimmed" fz="sm" ta="center"> <Text c="dimmed" fz={{ base: 12, md: 14 }} lh={{ base: 1.4, md: 1.6 }} ta="center">
{v.daerah} {v.daerah}
</Text> </Text>
<Text c="blue" fw={600} fz="sm" ta="center"> <Text c="blue" fw={600} fz={{ base: 12, md: 14 }} lh={{ base: 1.4, md: 1.6 }} ta="center">
{v.periode} {v.periode}
</Text> </Text>
</Stack> </Stack>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -2,7 +2,7 @@
'use client' 'use client'
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { Box, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
@@ -34,60 +34,57 @@ function VisiMisiDesa() {
loading="lazy" loading="lazy"
/> />
{/* VISI */}
<Paper <Paper
p="xl" p="xl"
radius="lg" radius="lg"
shadow="md" shadow="md"
withBorder withBorder
w="100%" w="100%"
style={{ style={{ background: 'linear-gradient(145deg, #ffffff, #f5f7fa)' }}
background: 'linear-gradient(145deg, #ffffff, #f5f7fa)',
}}
> >
<Text <Title
order={1}
c={colors['blue-button']} c={colors['blue-button']}
ta="center" ta="center"
fw={700}
fz={{ base: '2rem', md: '2.5rem' }}
mb="md" mb="md"
> >
Visi Desa Visi Desa
</Text> </Title>
<Text <Text
fz={{ base: '1.125rem', md: '1.375rem' }} fz={{ base: 'sm', md: 'md' }} // body text responsive
lh={1.7}
ta="center" ta="center"
fw={500}
lh={1.6}
dangerouslySetInnerHTML={{ __html: data.visi }} dangerouslySetInnerHTML={{ __html: data.visi }}
style={{wordBreak: "break-word", whiteSpace: "normal"}} style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
/> />
</Paper> </Paper>
{/* MISI */}
<Paper <Paper
p="xl" p="xl"
radius="lg" radius="lg"
shadow="md" shadow="md"
withBorder withBorder
w="100%" w="100%"
style={{ style={{ background: 'linear-gradient(145deg, #ffffff, #f5f7fa)' }}
background: 'linear-gradient(145deg, #ffffff, #f5f7fa)',
}}
> >
<Text <Title
order={1}
c={colors['blue-button']} c={colors['blue-button']}
ta="center" ta="center"
fw={700}
fz={{ base: '2rem', md: '2.5rem' }}
mb="md" mb="md"
> >
Misi Desa Misi Desa
</Text> </Title>
<Text <Text
fz={{ base: '1.125rem', md: '1.375rem' }} fz={{ base: 'sm', md: 'md' }} // body text responsive
fw={500} lh={1.7}
lh={1.6} ta="left"
dangerouslySetInnerHTML={{ __html: data.misi }} dangerouslySetInnerHTML={{ __html: data.misi }}
style={{wordBreak: "break-word", whiteSpace: "normal"}} style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
/> />
</Paper> </Paper>
</Stack> </Stack>

View File

@@ -1,7 +1,7 @@
'use client' 'use client'
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa'; import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Grid, GridCol, Paper, SimpleGrid, Stack, Table, Text, Title } from '@mantine/core'; import { Box, Flex, Group, Paper, SimpleGrid, Stack, Table, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto'; import BackButton from '../../desa/layanan/_com/BackButto';
@@ -30,196 +30,265 @@ function Page() {
// Hasil akhir // Hasil akhir
const sisaAnggaran = totalPendapatan - totalBelanja - totalPembiayaan; const sisaAnggaran = totalPendapatan - totalBelanja - totalPembiayaan;
const formatCurrency = (value: number) => {
return new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(value);
};
return ( return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="lg"> <Stack pos="relative" bg={colors.Bg} py="xl" gap="lg">
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
<BackButton /> <BackButton />
</Box> </Box>
<Text ta="center" fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw="bold">
{/* Page Title */}
<Title
ta="center"
c={colors["blue-button"]}
fw="bold"
order={1}
fz={{ base: 28, md: 36 }}
>
Pendapatan Asli Desa Pendapatan Asli Desa
</Text> </Title>
<Box px={{ base: "md", md: 100 }}> <Box px={{ base: "md", md: 100 }}>
<Stack gap="lg" justify="center"> <Stack gap="lg" justify="center">
<Paper bg={colors['white-1']} p="xl"> <Paper bg={colors['white-1']} p={{ base: 'md', md: 'xl' }}>
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="md"> <SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg">
{/* Pendapatan Card */} {/* Pendapatan Card */}
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}> <Box
<Stack gap={"xs"}> p="md"
<Title order={3}>Pendapatan</Title> style={{
{latestApb?.pendapatan?.map((item) => ( border: '1px solid #e9ecef',
<Box key={item.id}> borderRadius: '8px',
<Grid> height: '100%'
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}> }}
<Text fz="md" fw={500}>{item.name}</Text> >
</GridCol> <Stack gap="md">
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}> <Title order={3} fz={{ base: 18, md: 20 }} c={colors['blue-button']}>
Pendapatan
</Title>
<Stack gap="sm">
{latestApb?.pendapatan?.map((item) => (
<Box key={item.id}>
<Flex gap={1}>
<Text <Text
fz="md" fz={{ base: 13, md: 14 }}
fw={500} fw={500}
style={{ lh={1.4}
wordBreak: 'break-word', c="black"
whiteSpace: 'normal', style={{ wordBreak: 'break-word' }}
textAlign: 'right',
}}
> >
{new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(item.value)} {item.name} {formatCurrency(item.value)}
</Text> </Text>
</GridCol> </Flex>
</Grid> </Box>
</Box> ))}
))} </Stack>
<Grid>
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}> <Box
<Text fz="lg" fw={600} mb="xs">Total Pendapatan</Text> pt="sm"
</GridCol> mt="auto"
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}> style={{
<Text style={{ borderTop: `2px solid ${colors['blue-button']}`
wordBreak: 'break-word', }}
whiteSpace: 'normal' >
}} fz="xl" fw={700} c={colors['blue-button']}> <Flex direction="column" gap={4}>
{new Intl.NumberFormat('id-ID', { <Text fz={{ base: 14, md: 16 }} fw={600} lh={1.4}>
style: 'currency', Total Pendapatan
currency: 'IDR',
minimumFractionDigits: 0
}).format(totalPendapatan)}
</Text> </Text>
</GridCol> <Text
</Grid> fz={{ base: 18, md: 22 }}
fw={700}
c={colors['blue-button']}
lh={1.4}
>
{formatCurrency(totalPendapatan)}
</Text>
</Flex>
</Box>
</Stack> </Stack>
</Box> </Box>
{/* Belanja Card */} {/* Belanja Card */}
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}> <Box
<Stack gap={"xs"}> p="md"
<Title order={3}>Belanja</Title> style={{
{latestApb?.belanja?.map((item) => ( border: '1px solid #e9ecef',
<Box key={item.id}> borderRadius: '8px',
<Grid> height: '100%'
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}> }}
<Text fz="md" fw={500}>{item.name}</Text> >
</GridCol> <Stack gap="md">
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}> <Title order={3} fz={{ base: 18, md: 20 }} c="orange">
Belanja
</Title>
<Stack gap="sm">
{latestApb?.belanja?.map((item) => (
<Box key={item.id}>
<Group gap={1}>
<Text <Text
fz="md" fz={{ base: 13, md: 14 }}
fw={500} fw={500}
style={{ lh={1.4}
wordBreak: 'break-word', c="black"
whiteSpace: 'normal', style={{ wordBreak: 'break-word' }}
textAlign: 'right',
}}
> >
{new Intl.NumberFormat('id-ID', { {item.name} {formatCurrency(item.value)}
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(item.value)}
</Text> </Text>
</GridCol> </Group>
</Grid> </Box>
</Box> ))}
))} </Stack>
<Grid>
<GridCol span={{ base: 12, md: 6 }}> <Box
<Text fz="lg" fw={600} mb="xs">Total Belanja</Text> pt="sm"
</GridCol> mt="auto"
<GridCol span={{ base: 12, md: 6 }}> style={{
<Text fz="xl" fw={700} c="orange"> borderTop: '2px solid orange'
{new Intl.NumberFormat('id-ID', { }}
style: 'currency', >
currency: 'IDR', <Flex direction="column" gap={4}>
minimumFractionDigits: 0 <Text fz={{ base: 14, md: 16 }} fw={600} lh={1.4}>
}).format(totalBelanja)} Total Belanja
</Text> </Text>
</GridCol> <Text
</Grid> fz={{ base: 18, md: 22 }}
fw={700}
c="orange"
lh={1.4}
>
{formatCurrency(totalBelanja)}
</Text>
</Flex>
</Box>
</Stack> </Stack>
</Box> </Box>
{/* Pembiayaan Card */} {/* Pembiayaan Card */}
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}> <Box
<Stack gap={"xs"}> p="md"
<Title order={3}>Pembiayaan</Title> style={{
{latestApb?.pembiayaan?.map((item) => ( border: '1px solid #e9ecef',
<Box key={item.id}> borderRadius: '8px',
<Grid> height: '100%'
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}> }}
<Text fz="md" fw={500}>{item.name}</Text> >
</GridCol> <Stack gap="md">
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}> <Title order={3} fz={{ base: 18, md: 20 }} c="green">
Pembiayaan
</Title>
<Stack gap="sm">
{latestApb?.pembiayaan?.map((item) => (
<Box key={item.id}>
<Group gap={1}>
<Text <Text
fz="md" fz={{ base: 13, md: 14 }}
fw={500} fw={500}
style={{ lh={1.4}
wordBreak: 'break-word', c="black"
whiteSpace: 'normal', style={{ wordBreak: 'break-word' }}
textAlign: 'right',
}}
> >
{new Intl.NumberFormat('id-ID', { {item.name} {formatCurrency(item.value)}
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(item.value)}
</Text> </Text>
</GridCol> </Group>
</Grid> </Box>
</Box> ))}
))} </Stack>
<Grid>
<GridCol span={{ base: 12, md: 6 }}> <Box
<Text fz="lg" fw={600} mb="xs">Total Pembiayaan</Text> pt="sm"
</GridCol> mt="auto"
<GridCol span={{ base: 12, md: 6 }}> style={{
<Text fz="xl" fw={700} c="green"> borderTop: '2px solid green'
{new Intl.NumberFormat('id-ID', { }}
style: 'currency', >
currency: 'IDR', <Flex direction="column" gap={4}>
minimumFractionDigits: 0 <Text fz={{ base: 14, md: 16 }} fw={600} lh={1.4}>
}).format(totalPembiayaan)} Total Pembiayaan
</Text> </Text>
</GridCol> <Text
</Grid> fz={{ base: 18, md: 22 }}
fw={700}
c="green"
lh={1.4}
>
{formatCurrency(totalPembiayaan)}
</Text>
</Flex>
</Box>
</Stack> </Stack>
</Box> </Box>
</SimpleGrid> </SimpleGrid>
</Paper> </Paper>
{/* 🔽 Tambahan Ringkasan Anggaran */} {/* Ringkasan Anggaran */}
<Paper bg={colors['white-1']} p="xl" shadow="sm" withBorder> <Paper bg={colors['white-1']} p={{ base: 'md', md: 'xl' }} shadow="sm" withBorder>
<Title order={3} mb="md">Ringkasan Anggaran</Title> <Title order={3} mb="md" fz={{ base: 18, md: 20 }}>
Ringkasan Anggaran
</Title>
<Table striped highlightOnHover withTableBorder> <Table striped highlightOnHover withTableBorder>
<Table.Thead> <Table.Thead>
<Table.Tr> <Table.Tr>
<Table.Th>Keterangan</Table.Th> <Table.Th>
<Table.Th ta={"right"}>Jumlah</Table.Th> <Text fz={{ base: 13, md: 14 }} fw={600}>Keterangan</Text>
</Table.Th>
<Table.Th ta="right">
<Text fz={{ base: 13, md: 14 }} fw={600}>Jumlah</Text>
</Table.Th>
</Table.Tr> </Table.Tr>
</Table.Thead> </Table.Thead>
<Table.Tbody> <Table.Tbody>
<Table.Tr> <Table.Tr>
<Table.Td>Total Pendapatan</Table.Td> <Table.Td>
<Table.Td align="right"> <Text fz={{ base: 13, md: 14 }} lh={1.4}>Total Pendapatan</Text>
{new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(totalPendapatan)} </Table.Td>
<Table.Td ta="right">
<Text fz={{ base: 13, md: 14 }} fw={600} lh={1.4}>
{formatCurrency(totalPendapatan)}
</Text>
</Table.Td> </Table.Td>
</Table.Tr> </Table.Tr>
<Table.Tr> <Table.Tr>
<Table.Td>Total Belanja</Table.Td> <Table.Td>
<Table.Td align="right" c="orange"> <Text fz={{ base: 13, md: 14 }} lh={1.4} c="orange">Total Belanja</Text>
{new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(totalBelanja)} </Table.Td>
<Table.Td ta="right">
<Text fz={{ base: 13, md: 14 }} fw={600} lh={1.4} c="orange">
{formatCurrency(totalBelanja)}
</Text>
</Table.Td> </Table.Td>
</Table.Tr> </Table.Tr>
<Table.Tr> <Table.Tr>
<Table.Td>Total Pembiayaan</Table.Td> <Table.Td>
<Table.Td align="right" c="green"> <Text fz={{ base: 13, md: 14 }} lh={1.4} c="green">Total Pembiayaan</Text>
{new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(totalPembiayaan)} </Table.Td>
<Table.Td ta="right">
<Text fz={{ base: 13, md: 14 }} fw={600} lh={1.4} c="green">
{formatCurrency(totalPembiayaan)}
</Text>
</Table.Td> </Table.Td>
</Table.Tr> </Table.Tr>
<Table.Tr> <Table.Tr style={{ backgroundColor: '#f8f9fa' }}>
<Table.Td><b>Sisa Anggaran</b></Table.Td> <Table.Td>
<Table.Td align="right" c={sisaAnggaran >= 0 ? "blue" : "red"}> <Text fz={{ base: 14, md: 15 }} fw={700} lh={1.4}>Sisa Anggaran</Text>
<b> </Table.Td>
{new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(sisaAnggaran)} <Table.Td ta="right">
</b> <Text
fz={{ base: 14, md: 15 }}
fw={700}
c={sisaAnggaran >= 0 ? colors['blue-button'] : "red"}
lh={1.4}
>
{formatCurrency(sisaAnggaran)}
</Text>
</Table.Td> </Table.Td>
</Table.Tr> </Table.Tr>
</Table.Tbody> </Table.Tbody>

View File

@@ -1,7 +1,7 @@
'use client' 'use client'
import pasarDesaState from '@/app/admin/(dashboard)/_state/ekonomi/pasar-desa/pasar-desa'; import pasarDesaState from '@/app/admin/(dashboard)/_state/ekonomi/pasar-desa/pasar-desa';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Center, Flex, Grid, GridCol, Image, Pagination, Paper, Select, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core'; import { Box, Center, Flex, Grid, GridCol, Image, Pagination, Paper, Select, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from '@mantine/core';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconBrandWhatsapp, IconMapPinFilled, IconSearch, IconStarFilled } from '@tabler/icons-react'; import { IconBrandWhatsapp, IconMapPinFilled, IconSearch, IconStarFilled } from '@tabler/icons-react';
import { motion } from 'motion/react'; import { motion } from 'motion/react';
@@ -14,7 +14,7 @@ function Page() {
const router = useRouter() const router = useRouter()
const state = useProxy(pasarDesaState.pasarDesa) const state = useProxy(pasarDesaState.pasarDesa)
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay const [debouncedSearch] = useDebouncedValue(search, 1000);
const [selectedCategory, setSelectedCategory] = useState<string | null>(null); const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
const { const {
data, data,
@@ -28,7 +28,6 @@ function Page() {
pasarDesaState.kategoriProduk.findManyAll.load() pasarDesaState.kategoriProduk.findManyAll.load()
}, []) }, [])
// Filter data based on selected category
const filteredData = selectedCategory const filteredData = selectedCategory
? data?.filter(item => ? data?.filter(item =>
item.KategoriToPasar?.some(kategori => kategori.kategoriId === selectedCategory) item.KategoriToPasar?.some(kategori => kategori.kategoriId === selectedCategory)
@@ -39,7 +38,6 @@ function Page() {
load(page, 4, debouncedSearch, selectedCategory || undefined) load(page, 4, debouncedSearch, selectedCategory || undefined)
}, [page, debouncedSearch, selectedCategory]) }, [page, debouncedSearch, selectedCategory])
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
@@ -49,21 +47,22 @@ function Page() {
} }
return ( return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}> <Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
<BackButton /> <BackButton />
</Box> </Box>
<Box> <Box>
<Grid align='center' px={{ base: 'md', md: 100 }}> <Grid align="center" px={{ base: 'md', md: 100 }}>
<GridCol span={{ base: 12, md: 9 }}> <GridCol span={{ base: 12, md: 9 }}>
<Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}> <Title order={1} c={colors["blue-button"]} fw="bold">
Pasar Desa Pasar Desa
</Text> </Title>
</GridCol> </GridCol>
<GridCol span={{ base: 12, md: 3 }}> <GridCol span={{ base: 12, md: 3 }}>
<TextInput <TextInput
radius={"lg"} radius="lg"
placeholder='Cari Produk' placeholder="Cari Produk"
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
leftSection={<IconSearch size={20} />} leftSection={<IconSearch size={20} />}
@@ -71,22 +70,15 @@ function Page() {
/> />
</GridCol> </GridCol>
</Grid> </Grid>
<Text px={{ base: 'md', md: 100 }} ta={"justify"} fz="md" >
Pasar Desa Online adalah media promosi untuk membantu warga memasarkan <Text px={{ base: 'md', md: 100 }} pt={20} ta="justify" fz={{ base: 'sm', md: 'md' }}>
</Text> Pasar Desa Online adalah media promosi untuk membantu warga memasarkan dan memperkenalkan produk mereka.
<Text px={{ base: 'md', md: 100 }} ta={"justify"} fz="md" >
dan memperkenalkan produk mereka.
</Text> </Text>
</Box> </Box>
<Box px={{ base: "md", md: 100 }}> <Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'}> <Stack gap="lg">
<SimpleGrid <SimpleGrid pb={30} cols={{ base: 1, md: 2 }}>
pb={30}
cols={{
base: 1,
md: 2
}}
>
<Box> <Box>
<Select <Select
placeholder="Pilih Kategori" placeholder="Pilih Kategori"
@@ -103,50 +95,58 @@ function Page() {
/> />
</Box> </Box>
</SimpleGrid> </SimpleGrid>
<SimpleGrid cols={{ base: 1, md: 4 }}> <SimpleGrid cols={{ base: 1, md: 4 }}>
{filteredData?.map((v, k) => { {filteredData?.map((v, k) => (
return ( <Stack key={k}>
<Stack key={k}> <motion.div
<motion.div onClick={() => router.push(`/darmasaba/ekonomi/pasar-desa/${v.id}`)}
onClick={() => router.push(`/darmasaba/ekonomi/pasar-desa/${v.id}`)} whileHover={{ scale: 1.05 }}
whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.8 }}
whileTap={{ scale: 0.8 }} >
> <Paper p="lg">
<Paper p={'lg'}> <Image
<Image radius="lg"
radius={'lg'} src={v.image?.link || '/placeholder-product.jpg'}
src={v.image?.link || '/placeholder-product.jpg'} alt={v.nama}
alt={v.nama} h={200}
h={200} w="100%"
w='100%' style={{ objectFit: 'cover' }}
style={{ objectFit: 'cover' }} loading="lazy"
loading="lazy" />
/> <Text py="sm" fw="bold" fz={{ base: 'md', md: 'lg' }}>
<Text py={10} fw={'bold'} fz={'lg'}>{v.nama}</Text> {v.nama}
<Text fz={'md'}>Rp {v.harga.toLocaleString('id-ID')}</Text> </Text>
<Flex py={10} gap={'md'}> <Text fz={{ base: 'sm', md: 'md' }}>
<IconStarFilled size={20} color='#EBCB09' /> Rp {v.harga.toLocaleString('id-ID')}
<Text fz={'sm'} ml={2}>{v.rating}</Text> </Text>
</Flex> <Flex py="sm" gap="md">
<Flex justify={'space-between'} align={'center'}> <IconStarFilled size={20} color="#EBCB09" />
<Box> <Text fz={{ base: 'xs', md: 'sm' }} ml={2}>
<Flex gap={'md'} align={'center'}> {v.rating}
<IconMapPinFilled size={20} color='red' /> </Text>
<Text fz={'sm'} ml={2}>{v.alamatUsaha}</Text> </Flex>
</Flex> <Flex justify="space-between" align="center">
</Box> <Box>
<IconBrandWhatsapp size={20} color={colors['blue-button']} /> <Flex gap="md" align="center">
</Flex> <IconMapPinFilled size={20} color="red" />
</Paper> <Text fz={{ base: 'xs', md: 'sm' }} ml={2}>
</motion.div> {v.alamatUsaha}
</Stack> </Text>
) </Flex>
})} </Box>
<IconBrandWhatsapp size={20} color={colors['blue-button']} />
</Flex>
</Paper>
</motion.div>
</Stack>
))}
</SimpleGrid> </SimpleGrid>
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => load(newPage)} // ini penting! onChange={(newPage) => load(newPage)}
total={totalPages} total={totalPages}
my="md" my="md"
/> />

View File

@@ -1,7 +1,7 @@
'use client' 'use client'
import programKemiskinanState from '@/app/admin/(dashboard)/_state/ekonomi/program-kemiskinan'; import programKemiskinanState from '@/app/admin/(dashboard)/_state/ekonomi/program-kemiskinan';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Center, Grid, GridCol, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core'; import { Box, Center, Grid, GridCol, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from '@mantine/core';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconSearch } from '@tabler/icons-react'; import { IconSearch } from '@tabler/icons-react';
import { useState } from 'react'; import { useState } from 'react';
@@ -32,10 +32,9 @@ interface ProgramKemiskinanData {
function Page() { function Page() {
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay const [debouncedSearch] = useDebouncedValue(search, 1000);
const state = useProxy(programKemiskinanState) const state = useProxy(programKemiskinanState)
// 🔧 Get valid statistics data with proper type checking
const statistikData = state.findMany.data const statistikData = state.findMany.data
.filter((item): item is ProgramKemiskinanData & { statistik: StatistikData } => { .filter((item): item is ProgramKemiskinanData & { statistik: StatistikData } => {
return !!item?.statistik && return !!item?.statistik &&
@@ -43,11 +42,11 @@ function Page() {
item.statistik.jumlah !== undefined; item.statistik.jumlah !== undefined;
}) })
.map(item => ({ .map(item => ({
tahun: Number(item.statistik.tahun) || 0, // Ensure tahun is a number tahun: Number(item.statistik.tahun) || 0,
jumlah: Number(item.statistik.jumlah) || 0, // Ensure jumlah is a number jumlah: Number(item.statistik.jumlah) || 0,
})) }))
.sort((a, b) => a.tahun - b.tahun) .sort((a, b) => a.tahun - b.tahun)
.filter(item => !isNaN(item.tahun) && !isNaN(item.jumlah)); // Remove any invalid entries .filter(item => !isNaN(item.tahun) && !isNaN(item.jumlah));
const { const {
data, data,
@@ -74,12 +73,18 @@ function Page() {
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
<BackButton /> <BackButton />
</Box> </Box>
<Box px={{ base: 'md', md: 100 }} > <Box px={{ base: 'md', md: 100 }}>
<Grid align='center'> <Grid align='center'>
<GridCol span={{ base: 12, md: 9 }}> <GridCol span={{ base: 12, md: 9 }}>
<Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}> <Title
order={1}
c={colors["blue-button"]}
fw={"bold"}
fz={{ base: '28px', md: '32px' }}
lh={{ base: '1.2', md: '1.25' }}
>
Program Kemiskinan Program Kemiskinan
</Text> </Title>
</GridCol> </GridCol>
<GridCol span={{ base: 12, md: 3 }}> <GridCol span={{ base: 12, md: 3 }}>
<TextInput <TextInput
@@ -92,7 +97,15 @@ function Page() {
/> />
</GridCol> </GridCol>
</Grid> </Grid>
<Text fz={'h4'}>Berbagai program bantuan untuk mengurangi kemiskinan dan meningkatkan kesejahteraan masyarakat</Text> <Text
fz={{ base: '14px', md: '16px' }}
lh={{ base: '1.5', md: '1.6' }}
c="black"
ta={{ base: 'left', md: 'left' }}
pt={20}
>
Berbagai program bantuan untuk mengurangi kemiskinan dan meningkatkan kesejahteraan masyarakat
</Text>
</Box> </Box>
<Box px={{ base: "md", md: 100 }}> <Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'} justify='center'> <Stack gap={'lg'} justify='center'>
@@ -106,8 +119,22 @@ function Page() {
{state.findMany.data.map(v => { {state.findMany.data.map(v => {
return ( return (
<Paper p={'xl'} key={v.id}> <Paper p={'xl'} key={v.id}>
<Text fz={'h3'} fw={'bold'} c={colors['blue-button']}>{v.nama}</Text> <Title
<Text fz={'lg'} c={'black'} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: v.deskripsi }}></Text> order={3}
fw={'bold'}
c={colors['blue-button']}
fz={{ base: '18px', md: '20px' }}
lh={{ base: '1.3', md: '1.35' }}
>
{v.nama}
</Title>
<Text
fz={{ base: '14px', md: '16px' }}
lh={{ base: '1.5', md: '1.6' }}
c={'black'}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
/>
</Paper> </Paper>
) )
})} })}
@@ -124,7 +151,16 @@ function Page() {
/> />
</Center> </Center>
<Paper p={'xl'}> <Paper p={'xl'}>
<Text fz={'h3'} fw={'bold'} c={colors['blue-button']} mb="md">Statistik Kemiskinan Masyarakat</Text> <Title
order={3}
fw={'bold'}
c={colors['blue-button']}
fz={{ base: '18px', md: '20px' }}
lh={{ base: '1.3', md: '1.35' }}
mb="md"
>
Statistik Kemiskinan Masyarakat
</Title>
<Box style={{ width: '100%', height: 'auto' }}> <Box style={{ width: '100%', height: 'auto' }}>
{statistikData.length > 0 ? ( {statistikData.length > 0 ? (
<Box w="100%" style={{ overflowX: 'auto' }}> <Box w="100%" style={{ overflowX: 'auto' }}>
@@ -162,7 +198,11 @@ function Page() {
</Box> </Box>
) : ( ) : (
<Box p="md" ta="center" bg="gray.0" style={{ borderRadius: '8px' }}> <Box p="md" ta="center" bg="gray.0" style={{ borderRadius: '8px' }}>
<Text c="dimmed"> <Text
fz={{ base: '12px', md: '14px' }}
c="dimmed"
lh={{ base: '1.4', md: '1.5' }}
>
{state.findMany.loading {state.findMany.loading
? 'Memuat data statistik...' ? 'Memuat data statistik...'
: 'Belum ada data statistik yang tersedia atau data tidak valid'} : 'Belum ada data statistik yang tersedia atau data tidak valid'}

View File

@@ -14,6 +14,9 @@ import {
Loader, Loader,
Paper, Paper,
Stack, Stack,
Tabs,
TabsList,
TabsTab,
Text, Text,
TextInput, TextInput,
Title, Title,
@@ -33,6 +36,8 @@ import { OrganizationChart } from 'primereact/organizationchart'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { useProxy } from 'valtio/utils' import { useProxy } from 'valtio/utils'
import BackButton from '../../desa/layanan/_com/BackButto' import BackButton from '../../desa/layanan/_com/BackButto'
import { useMediaQuery } from '@mantine/hooks'
import { useTransitionRouter } from 'next-view-transitions'
export default function Page() { export default function Page() {
return ( return (
@@ -51,11 +56,11 @@ export default function Page() {
order={1} order={1}
ta="center" ta="center"
c={colors['blue-button']} c={colors['blue-button']}
fz={{ base: 28, md: 36, lg: 44 }} fz={{ base: 28, md: 36 }}
> >
Struktur Organisasi & SK Pengurus BumDes Struktur Organisasi & SK Pengurus BumDes
</Title> </Title>
<Text ta="center" c="black" maw={800}> <Text ta="center" c="black" maw={800} fz={{ base: 14, md: 16 }} lh={1.6}>
Gambaran visual peran dan pengurus yang ditugaskan. Gunakan kontrol Gambaran visual peran dan pengurus yang ditugaskan. Gunakan kontrol
di bawah untuk mencari, memperbesar, atau melihat lebih jelas. di bawah untuk mencari, memperbesar, atau melihat lebih jelas.
</Text> </Text>
@@ -70,13 +75,14 @@ export default function Page() {
} }
function StrukturOrganisasiBumDes() { function StrukturOrganisasiBumDes() {
const router = useTransitionRouter()
const stateOrganisasi: any = useProxy(stateStrukturBumDes.pegawai) const stateOrganisasi: any = useProxy(stateStrukturBumDes.pegawai)
const chartContainerRef = useRef<HTMLDivElement>(null) const chartContainerRef = useRef<HTMLDivElement>(null)
const [scale, setScale] = useState(1) const [scale, setScale] = useState(1)
const [isFullscreen, setFullscreen] = useState(false) const [isFullscreen, setFullscreen] = useState(false)
const [searchQuery, setSearchQuery] = useState('') const [searchQuery, setSearchQuery] = useState('')
const debouncedSearch = useRef( const debouncedSearch = useRef(
debounce((value: string) => setSearchQuery(value), 400) debounce((value: string) => setSearchQuery(value), 1000)
).current ).current
useEffect(() => { useEffect(() => {
@@ -92,8 +98,10 @@ function StrukturOrganisasiBumDes() {
<Center py={48}> <Center py={48}>
<Stack align="center" gap="sm"> <Stack align="center" gap="sm">
<Loader size="lg" /> <Loader size="lg" />
<Text fw={600}>Memuat struktur organisasi</Text> <Text fw={600} fz={{ base: 15, md: 16 }} lh={1.4}>
<Text c="dimmed" size="sm"> Memuat struktur organisasi
</Text>
<Text c="dimmed" fz={{ base: 12, md: 14 }} lh={1.4}>
Mengambil data pengurus dan posisi. Mohon tunggu sebentar. Mengambil data pengurus dan posisi. Mohon tunggu sebentar.
</Text> </Text>
</Stack> </Stack>
@@ -119,10 +127,10 @@ function StrukturOrganisasiBumDes() {
<Center> <Center>
<IconUsers size={56} /> <IconUsers size={56} />
</Center> </Center>
<Title order={3} mt="md"> <Title order={3} mt="md" ta="center">
Data pengurus belum tersedia Data pengurus belum tersedia
</Title> </Title>
<Text c="dimmed" mt="xs"> <Text c="dimmed" mt="xs" fz={{ base: 12, md: 14 }} lh={1.4}>
Belum ada data pengurus yang tercatat untuk BumDes. Belum ada data pengurus yang tercatat untuk BumDes.
</Text> </Text>
<Group justify="center" mt="lg"> <Group justify="center" mt="lg">
@@ -218,155 +226,300 @@ function StrukturOrganisasiBumDes() {
return ( return (
<Stack align="center" mt="xl"> <Stack align="center" mt="xl">
{/* 🧭 Kontrol atas */} {/* 🧭 Kontrol atas */}
<Paper shadow="xs" p="md" radius="md" bg={colors['blue-button']}> <Paper
<Group gap="sm" wrap="wrap" justify="center"> shadow="xs"
<TextInput w={{
placeholder="Cari nama atau jabatan..." base: '100%', // Mobile: 100%
leftSection={<IconSearch size={16} />} sm: '40%', // Tablet: 95%
onChange={(e) => debouncedSearch(e.target.value)} md: '39%', // Desktop: 70%
lg: '38%', // Desktop L: 60%
xl: '37%', // 4K: 50%
'2xl': '36%', // Ultra-wide: 45%
}}
p="md"
radius="md"
style={{
background: colors['blue-button'], // ⬅️ penting
maxWidth: '100%', // ⬅️ penting
overflowX: 'auto' // ⬅️ untuk mencegah overflow
}}
>
<Stack gap="sm">
<Group justify='center'>
<TextInput
placeholder="Cari nama atau jabatan..."
leftSection={<IconSearch size={16} />}
onChange={(e) => debouncedSearch(e.target.value)}
styles={{
input: {
minWidth: 250,
},
}}
/>
</Group>
<Tabs
defaultValue="zoom-out"
variant="outline"
radius="md"
styles={{ styles={{
input: { panel: { display: 'none' },
minWidth: 250, tab: {
color: colors['blue-button'],
backgroundColor: colors['blue-button-2'],
border: 'none',
fontWeight: 600,
fontSize: '0.875rem',
padding: '6px 12px',
minHeight: 'auto',
flexShrink: 0,
}, },
}} }}
/> style={{ width: '100%' }} // 👈 penting
<Group gap="xs"> >
<Button <TabsList
variant="light"
bg={colors['blue-button-2']}
c={colors['blue-button']}
size="sm"
onClick={handleZoomOut}
leftSection={<IconZoomOut size={16} />}
>
Zoom Out
</Button>
<Box
bg={colors['blue-button-2']}
c={colors['blue-button']}
px={16}
py={8}
style={{ style={{
fontSize: 14, display: 'flex',
fontWeight: 700, overflowX: 'auto',
borderRadius: '8px', overflowY: 'hidden',
minWidth: 70, gap: '4px',
textAlign: 'center', paddingBottom: '4px',
flexWrap: 'nowrap',
WebkitOverflowScrolling: 'touch',
scrollbarWidth: 'thin',
msOverflowStyle: '-ms-autohiding-scrollbar',
maxWidth: '100%',
scrollBehavior: 'smooth', // 👈 smooth scroll
}} }}
> >
{Math.round(scale * 100)}% <TabsTab
</Box> value="zoom-out"
<Button onClick={handleZoomOut}
variant="light" leftSection={<IconZoomOut size={16} />}
bg={colors['blue-button-2']} style={{ flexShrink: 0 }}
c={colors['blue-button']} >
size="sm" <Text fz={{ base: 12, sm: 13 }} lh={1} ta="center">Zoom Out</Text>
onClick={handleZoomIn} </TabsTab>
leftSection={<IconZoomIn size={16} />}
> <Box
Zoom In bg={colors['blue-button-2']}
</Button> c={colors['blue-button']}
<Button px={12}
variant="light" py={6}
bg={colors['blue-button-2']} style={{
c={colors['blue-button']} fontWeight: 700,
size="sm" borderRadius: '6px',
onClick={resetZoom} minWidth: 60,
> textAlign: 'center',
Reset display: 'flex',
</Button> alignItems: 'center',
<Button justifyContent: 'center',
variant="light" flexShrink: 0,
bg={colors['blue-button-2']} whiteSpace: 'nowrap',
c={colors['blue-button']} }}
size="sm" >
onClick={toggleFullscreen} <Text fz={{ base: 12, sm: 13 }} lh={1} c={colors['blue-button']}>
leftSection={ {Math.round(scale * 100)}%
isFullscreen ? ( </Text>
<IconArrowsMinimize size={16} /> </Box>
) : (
<IconArrowsMaximize size={16} /> <TabsTab
) value="zoom-in"
} onClick={handleZoomIn}
> leftSection={<IconZoomIn size={16} />}
Fullscreen style={{ flexShrink: 0 }}
</Button> >
</Group> <Text fz={{ base: 12, sm: 13 }} lh={1} ta="center">Zoom In</Text>
</Group> </TabsTab>
<TabsTab
value="reset"
onClick={resetZoom}
style={{ flexShrink: 0 }}
>
<Text fz={{ base: 12, sm: 13 }} lh={1} ta="center">Reset</Text>
</TabsTab>
<TabsTab
value="fullscreen"
onClick={toggleFullscreen}
leftSection={
isFullscreen ? (
<IconArrowsMinimize size={16} />
) : (
<IconArrowsMaximize size={16} />
)
}
style={{ flexShrink: 0 }}
>
<Text fz={{ base: 12, sm: 13 }} lh={1} ta="center">
{isFullscreen ? 'Exit' : 'Fullscreen'}
</Text>
</TabsTab>
</TabsList>
</Tabs>
</Stack>
</Paper> </Paper>
{/* 📊 Chart Container */} {/* 🧩 Chart Container */}
<Center style={{ width: '100%' }}> <Center style={{ width: '100%' }}>
<Box <Box
ref={chartContainerRef} ref={chartContainerRef}
style={{ style={{
overflowX: 'auto', borderRadius: '12px',
overflowY: 'auto', overflowX: 'auto',
width: '100%', overflowY: 'auto',
padding: '32px 16px', width: '100%',
transition: 'transform 0.2s ease', maxWidth: '100%',
transform: `scale(${scale})`, padding: '32px 16px',
transformOrigin: 'center top', transition: 'transform 0.2s ease',
}} }}
> >
<OrganizationChart <Box style={{
value={chartData}
nodeTemplate={(node) => <NodeCard node={node} />}
className="p-organizationchart p-organizationchart-horizontal"
/>
</Box>
</Center>
</Stack>
)
}
function NodeCard({ node }: any) { transform: `scale(${scale})`,
const imageSrc = node?.data?.image || '/img/default.png' transformOrigin: 'center top',
const name = node?.data?.name || 'Tanpa Nama' display: 'inline-block', // 👈 agar tidak memenuhi lebar parent
const title = node?.data?.title || 'Tanpa Jabatan' minWidth: 'min-content', // 👈 penting agar chart tidak dipaksa muat di width 100%
const description = node?.data?.description || '' }}>
<OrganizationChart
return ( value={chartData}
<Transition mounted transition="pop" duration={300}> nodeTemplate={(node) => <NodeCard node={node} router={router} />}
{(styles) => ( className="p-organizationchart p-organizationchart-horizontal"
<Card />
shadow="md" </Box>
radius="xl" </Box>
withBorder </Center>
style={{
...styles,
width: 240,
padding: 20,
background:
'linear-gradient(135deg, rgba(28,110,164,0.15) 0%, rgba(255,255,255,0.95) 100%)',
borderColor: 'rgba(28, 110, 164, 0.3)',
transition: 'all 0.3s ease',
}}
>
<Stack align="center" gap={10}>
<Box
style={{
width: 90,
height: 90,
borderRadius: '50%',
overflow: 'hidden',
border: '3px solid rgba(28, 110, 164, 0.4)',
}}
>
<Image src={imageSrc} alt={name} fit="cover" loading="lazy" />
</Box>
<Text fw={700} size="sm" ta="center" c={colors['blue-button']}>
{name}
</Text>
<Text size="xs" c="dimmed" ta="center">
{title}
</Text>
<Text size="xs" c="dimmed" ta="center" lineClamp={3}>
{description || 'Belum ada deskripsi.'}
</Text>
</Stack> </Stack>
</Card> )
)} }
</Transition>
) function NodeCard({ node, router }: any) {
} const imageSrc = node?.data?.image || '/img/default.png'
const name = node?.data?.name || 'Tanpa Nama'
const title = node?.data?.title || 'Tanpa Jabatan'
const hasId = Boolean(node?.data?.id)
const isMobile = useMediaQuery("(max-width: 768px)");
return (
<Transition mounted transition="pop" duration={300}>
{(styles) => (
<Card
shadow="md"
radius="xl"
withBorder
style={{
...styles,
width: '100%',
maxWidth: isMobile ? 200 : 240, // lebih kecil di mobile
minHeight: isMobile ? 240 : 280,
padding: isMobile ? 16 : 20,
background: 'linear-gradient(135deg, rgba(28,110,164,0.15) 0%, rgba(255,255,255,0.95) 100%)',
borderColor: 'rgba(28, 110, 164, 0.3)',
borderWidth: 2,
transition: 'all 0.3s ease',
cursor: hasId ? 'pointer' : 'default',
}}
onMouseEnter={(e) => {
if (hasId) {
e.currentTarget.style.transform = 'translateY(-4px)'
e.currentTarget.style.boxShadow = '0 8px 24px rgba(28, 110, 164, 0.25)'
}
}}
onMouseLeave={(e) => {
if (hasId) {
e.currentTarget.style.transform = 'translateY(0)'
e.currentTarget.style.boxShadow = ''
}
}}
>
<Stack align="center" gap={12}>
{/* Photo */}
<Box
style={{
width: 96,
height: 96,
borderRadius: '50%',
overflow: 'hidden',
border: '3px solid rgba(28, 110, 164, 0.4)',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
background: 'white',
}}
>
<Image
src={imageSrc}
alt={name}
width={96}
height={96}
fit="cover"
loading="lazy"
style={{
objectFit: 'cover',
}}
/>
</Box>
{/* Name */}
<Text
fw={700}
ta="center"
c={colors['blue-button']}
lineClamp={2}
fz={{ base: 13, md: 15 }}
lh={1.2}
style={{
minHeight: 40,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
wordBreak: 'break-word',
}}
>
{name}
</Text>
{/* Title/Position */}
<Text
c="dimmed"
ta="center"
fw={500}
lineClamp={2}
fz={{ base: 12, md: 13 }}
lh={1.3}
style={{
minHeight: 32,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
wordBreak: 'break-word',
}}
>
{title}
</Text>
{/* Detail Button */}
{hasId && (
<Button
variant="gradient"
gradient={{ from: 'blue', to: 'cyan' }}
size="xs"
fullWidth
mt={8}
radius="md"
onClick={() =>
router.push(`/darmasaba/ppid/struktur-ppid/${node.data.id}`)
}
style={{
height: 32,
fontWeight: 600,
}}
>
<Text fz={{ base: 12, md: 13 }} lh={1} ta="center">Lihat Detail</Text>
</Button>
)}
</Stack>
</Card>
)}
</Transition>
)
}

View File

@@ -58,7 +58,7 @@ function Page() {
/> />
</GridCol> </GridCol>
</Grid> </Grid>
<Text px={{ base: 'md', md: 100 }} ta={"justify"} fz="md" mt={4} > <Text px={{ base: 'md', md: 100 }} pt={20} ta={"justify"} fz="md" mt={4} >
Pecalang dan Patwal (Patroli Pengawal) bertugas memastikan desa tetap aman, tertib, dan kondusif bagi seluruh warga. Pecalang dan Patwal (Patroli Pengawal) bertugas memastikan desa tetap aman, tertib, dan kondusif bagi seluruh warga.
</Text> </Text>
</Box> </Box>

View File

@@ -71,14 +71,14 @@ function Page() {
<Stack gap="md"> <Stack gap="md">
<Box> <Box>
<Text fw={600} fz="lg" >Judul</Text> <Text fw={600} fz="lg" >Judul</Text>
<Text fz="sm" c="dimmed">{data.judul || '-'}</Text> <Text fz="sm" c="black">{data.judul || '-'}</Text>
</Box> </Box>
<Divider /> <Divider />
<Box> <Box>
<Text fw={600} fz="lg" >Tanggal</Text> <Text fw={600} fz="lg" >Tanggal</Text>
<Text fz="sm" c="dimmed"> <Text fz="sm" c="black">
{data.tanggalWaktu {data.tanggalWaktu
? new Date(data.tanggalWaktu).toLocaleString('id-ID', { dateStyle: 'full', timeStyle: 'short' }) ? new Date(data.tanggalWaktu).toLocaleString('id-ID', { dateStyle: 'full', timeStyle: 'short' })
: '-'} : '-'}
@@ -89,7 +89,7 @@ function Page() {
<Box> <Box>
<Text fw={600} fz="lg" >Lokasi</Text> <Text fw={600} fz="lg" >Lokasi</Text>
<Text fz="sm" c="dimmed">{data.lokasi || '-'}</Text> <Text fz="sm" c="black">{data.lokasi || '-'}</Text>
</Box> </Box>
<Divider /> <Divider />
@@ -120,7 +120,7 @@ function Page() {
<Box> <Box>
<Text fw={600} fz="lg" >Kronologi</Text> <Text fw={600} fz="lg" >Kronologi</Text>
<Text fz="sm" c="dimmed" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: data.kronologi || '-' }} /> <Text fz="sm" c="black" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: data.kronologi || '-' }} />
</Box> </Box>
<Divider /> <Divider />
@@ -136,11 +136,11 @@ function Page() {
radius="md" radius="md"
shadow="xs" shadow="xs"
withBorder withBorder
bg="dark.5" bg={colors['blue-button-1']}
> >
<Text <Text
fz="sm" fz="sm"
c="dimmed" c="black"
dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }} dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }}
style={{wordBreak: "break-word", whiteSpace: "normal"}} style={{wordBreak: "break-word", whiteSpace: "normal"}}
/> />

View File

@@ -1,6 +1,6 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Center, Grid, GridCol, Image, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core'; import { Box, Center, Grid, GridCol, Image, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from '@mantine/core';
import BackButton from '../../desa/layanan/_com/BackButto'; import BackButton from '../../desa/layanan/_com/BackButto';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import tipsKeamananState from '@/app/admin/(dashboard)/_state/keamanan/tips-keamanan'; import tipsKeamananState from '@/app/admin/(dashboard)/_state/keamanan/tips-keamanan';
@@ -8,11 +8,10 @@ import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { useState } from 'react'; import { useState } from 'react';
import { IconSearch } from '@tabler/icons-react'; import { IconSearch } from '@tabler/icons-react';
function Page() { function Page() {
const state = useProxy(tipsKeamananState) const state = useProxy(tipsKeamananState);
const [search, setSearch] = useState('') const [search, setSearch] = useState('');
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay const [debouncedSearch] = useDebouncedValue(search, 1000);
const { const {
data, data,
page, page,
@@ -22,84 +21,114 @@ function Page() {
} = state.findMany; } = state.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 3, debouncedSearch) load(page, 3, debouncedSearch);
}, [page, debouncedSearch]) }, [page, debouncedSearch]);
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}> <Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
<Skeleton h={500} /> <Skeleton h={500} />
</Stack> </Stack>
) );
} }
return ( return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}> <Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
<BackButton /> <BackButton />
</Box> </Box>
<Box> <Box>
<Grid align='center' px={{ base: 'md', md: 100 }}> <Grid align="center" px={{ base: 'md', md: 100 }}>
<GridCol span={{ base: 12, md: 9 }}> <GridCol span={{ base: 12, md: 9 }}>
<Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}> <Title
order={1}
c={colors['blue-button']}
style={{ lineHeight: '1.2' }}
>
Tips Keamanan Tips Keamanan
</Text> </Title>
</GridCol> </GridCol>
<GridCol span={{ base: 12, md: 3 }}> <GridCol span={{ base: 12, md: 3 }}>
<TextInput <TextInput
radius={"lg"} radius="lg"
placeholder='Cari Tips' placeholder="Cari Tips"
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
leftSection={<IconSearch size={20} />} leftSection={<IconSearch size={20} />}
w={{ base: "50%", md: "100%" }} w={{ base: '50%', md: '100%' }}
/> />
</GridCol> </GridCol>
</Grid> </Grid>
<Text px={{ base: 'md', md: 100 }} ta={"justify"} fz="md" >
<Text
px={{ base: 'md', md: 100 }}
ta="justify"
fz={{ base: 'sm', md: 'md' }}
lh={{ base: '1.5', md: '1.6' }}
mt="sm"
>
Keamanan dan ketertiban lingkungan di Desa Darmasaba dijaga melalui peran aktif Pecalang dan Patwal (Patroli Pengawal). Keamanan dan ketertiban lingkungan di Desa Darmasaba dijaga melalui peran aktif Pecalang dan Patwal (Patroli Pengawal).
</Text> </Text>
<Text px={{ base: 'md', md: 100 }} ta={"justify"} fz="md" > <Text
px={{ base: 'md', md: 100 }}
ta="justify"
fz={{ base: 'sm', md: 'md' }}
lh={{ base: '1.5', md: '1.6' }}
mt="xs"
>
Mereka bertugas memastikan desa tetap aman, tertib, dan kondusif bagi seluruh warga. Mereka bertugas memastikan desa tetap aman, tertib, dan kondusif bagi seluruh warga.
</Text> </Text>
</Box> </Box>
<Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'}> <Box px={{ base: 'md', md: 100 }}>
<Stack gap="lg">
<SimpleGrid <SimpleGrid
pb={10} pb="10"
cols={{ cols={{ base: 1, md: 3 }}
base: 1, >
md: 3, {data.map((v, k) => (
}}> <Paper radius={10} key={k} bg={colors['white-trans-1']}>
{data.map((v, k) => { <Stack gap="xs">
return ( <Center p="10">
<Paper radius={10} key={k} bg={colors["white-trans-1"]}> <Image
<Stack gap={'xs'}> src={v.image?.link}
<Center p={10}> radius={10}
<Image src={v.image?.link} radius={10} loading="lazy" loading="lazy"
alt='' /> alt=""
</Center> />
<Box px={'xl'}> </Center>
<Box pb={20}> <Box px="xl">
<Text pb={10} c={colors["blue-button"]} fw={"bold"} fz={"h3"}> <Box pb="20">
{v.judul} <Title
</Text> order={3}
<Box> c={colors['blue-button']}
<Text pb={10} fz={"md"} style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: v.deskripsi }} /> style={{ lineHeight: '1.3' }}
</Box> >
{v.judul}
</Title>
<Box>
<Text
pb="10"
fz={{ base: 'xs', md: 'md' }}
lh={{ base: '1.5', md: '1.6' }}
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
/>
</Box> </Box>
</Box> </Box>
</Stack> </Box>
</Paper> </Stack>
) </Paper>
})} ))}
</SimpleGrid> </SimpleGrid>
</Stack> </Stack>
</Box> </Box>
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => load(newPage)} // ini penting! onChange={(newPage) => load(newPage)}
total={totalPages} total={totalPages}
my="md" my="md"
/> />

View File

@@ -1,7 +1,7 @@
'use client' 'use client'
import artikelKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/artikelKesehatan'; import artikelKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/artikelKesehatan';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Card, Divider, Group, Image, Loader, Paper, Stack, Text } from '@mantine/core'; import { Box, Button, Card, Divider, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconCalendar, IconChevronRight } from '@tabler/icons-react'; import { IconCalendar, IconChevronRight } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@@ -17,9 +17,8 @@ function ArtikelKesehatanPage() {
if (!state.findMany.data) { if (!state.findMany.data) {
return ( return (
<Box py="xl" ta="center"> <Box py="lg">
<Loader size="lg" color={colors['blue-button']} /> <Skeleton h={500} radius="lg" />
<Text mt="md" c="dimmed" fz="md">Memuat artikel kesehatan...</Text>
</Box> </Box>
) )
} }

View File

@@ -1,7 +1,7 @@
'use client' 'use client'
import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan'; import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Badge, Box, Button, Card, Divider, Group, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { Badge, Box, Button, Card, Divider, Group, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconChevronRight, IconClock, IconMapPin } from '@tabler/icons-react'; import { IconChevronRight, IconClock, IconMapPin } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@@ -17,12 +17,8 @@ function FasilitasKesehatanPage() {
if (!state.findMany.data) { if (!state.findMany.data) {
return ( return (
<Box py="xl" px="md"> <Box py="lg">
<Stack gap="md"> <Skeleton h={500} radius="lg" />
<Skeleton height={80} radius="lg" />
<Skeleton height={80} radius="lg" />
<Skeleton height={80} radius="lg" />
</Stack>
</Box> </Box>
); );
} }
@@ -31,14 +27,20 @@ function FasilitasKesehatanPage() {
<Box> <Box>
<Paper bg={colors['white-trans-1']} p="xl" radius="xl" shadow="md" h="100%"> <Paper bg={colors['white-trans-1']} p="xl" radius="xl" shadow="md" h="100%">
<Stack gap="lg"> <Stack gap="lg">
<Text ta="center" fw={700} fz="32px" c={colors['blue-button']}> <Title
order={1}
ta="center"
fw={700}
c={colors['blue-button']}
style={{ lineHeight: '1.2' }}
>
Fasilitas Kesehatan Fasilitas Kesehatan
</Text> </Title>
<Divider size="sm" color={colors['blue-button']} /> <Divider size="sm" color={colors['blue-button']} />
<Stack gap="lg"> <Stack gap="lg">
{state.findMany.data.length === 0 ? ( {state.findMany.data.length === 0 ? (
<Box py="xl" ta="center"> <Box py="xl" ta="center">
<Text fz="lg" c="dimmed"> <Text size="lg" c="dimmed" lh="1.5">
Belum ada fasilitas kesehatan yang tersedia Belum ada fasilitas kesehatan yang tersedia
</Text> </Text>
</Box> </Box>
@@ -65,22 +67,20 @@ function FasilitasKesehatanPage() {
> >
<Stack gap="sm"> <Stack gap="sm">
<Group justify="space-between" align="center"> <Group justify="space-between" align="center">
<Text fw={700} fz="lg" c={colors['blue-button']}> <Title order={3} fw={700} c={colors['blue-button']} lh="1.3" />
{item.name} <Badge color="blue" radius="sm" variant="light" size="xs">
</Text>
<Badge color="blue" radius="sm" variant="light" fz="xs">
Aktif Aktif
</Badge> </Badge>
</Group> </Group>
<Group gap="xs"> <Group gap="xs">
<IconMapPin size={18} stroke={1.5} /> <IconMapPin size={18} stroke={1.5} />
<Text fz="sm"> <Text size="sm" lh="1.5">
{item.informasiumum.alamat} {item.informasiumum.alamat}
</Text> </Text>
</Group> </Group>
<Group gap="xs"> <Group gap="xs">
<IconClock size={18} stroke={1.5} /> <IconClock size={18} stroke={1.5} />
<Text fz="sm"> <Text size="sm" lh="1.5">
{item.informasiumum.jamOperasional} {item.informasiumum.jamOperasional}
</Text> </Text>
</Group> </Group>

View File

@@ -6,9 +6,7 @@ import { Box, Center, ColorSwatch, Flex, Paper, SimpleGrid, Skeleton, Stack, Tex
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import BackButton from '../../desa/layanan/_com/BackButto'; import BackButton from '../../desa/layanan/_com/BackButto';
// import { useRouter } from 'next/navigation';
import { useMediaQuery, useShallowEffect } from '@mantine/hooks'; import { useMediaQuery, useShallowEffect } from '@mantine/hooks';
import persentasekelahiran from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran'; import persentasekelahiran from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
@@ -17,7 +15,6 @@ import GrafikPenyakit from './grafik-penyakit/page';
import JadwalKegiatan from './jadwal-kegiatan-page/page'; import JadwalKegiatan from './jadwal-kegiatan-page/page';
import ArtikelKesehatanPage from './artikel-kesehatan-page/page'; import ArtikelKesehatanPage from './artikel-kesehatan-page/page';
function Page() { function Page() {
type DataTahunan = { type DataTahunan = {
tahun: string; tahun: string;
@@ -31,7 +28,6 @@ function Page() {
}>; }>;
}; };
// Count occurrences per year
const countByYear = (data: any[], dateField: string) => { const countByYear = (data: any[], dateField: string) => {
const counts: Record<string, number> = {}; const counts: Record<string, number> = {};
data?.forEach(item => { data?.forEach(item => {
@@ -43,28 +39,23 @@ function Page() {
const statePersentase = useProxy(persentasekelahiran); const statePersentase = useProxy(persentasekelahiran);
const [chartData, setChartData] = useState<DataTahunan[]>([]); const [chartData, setChartData] = useState<DataTahunan[]>([]);
const isTablet = useMediaQuery('(max-width: 1024px)');
const isMobile = useMediaQuery('(max-width: 768px)'); const isMobile = useMediaQuery('(max-width: 768px)');
useShallowEffect(() => { useShallowEffect(() => {
statePersentase.kelahiran.findMany.load(1, 1000); // Load all kelahiran data statePersentase.kelahiran.findMany.load(1, 1000);
statePersentase.kematian.findMany.load(1, 1000); // Load all kematian data statePersentase.kematian.findMany.load(1, 1000);
}, []); }, []);
useEffect(() => { useEffect(() => {
if (statePersentase.kelahiran.findMany.data && statePersentase.kematian.findMany.data) { if (statePersentase.kelahiran.findMany.data && statePersentase.kematian.findMany.data) {
// Count kelahiran and kematian by year
const kelahiranByYear = countByYear(statePersentase.kelahiran.findMany.data, 'tanggal'); const kelahiranByYear = countByYear(statePersentase.kelahiran.findMany.data, 'tanggal');
const kematianByYear = countByYear(statePersentase.kematian.findMany.data, 'tanggal'); const kematianByYear = countByYear(statePersentase.kematian.findMany.data, 'tanggal');
// Get all unique years
const allYears = new Set([ const allYears = new Set([
...Object.keys(kelahiranByYear), ...Object.keys(kelahiranByYear),
...Object.keys(kematianByYear) ...Object.keys(kematianByYear)
]); ]);
// Create data structure for the chart
const dataByYear = Array.from(allYears).reduce<Record<string, DataTahunan>>((acc, year) => { const dataByYear = Array.from(allYears).reduce<Record<string, DataTahunan>>((acc, year) => {
acc[year] = { acc[year] = {
tahun: year, tahun: year,
@@ -93,32 +84,44 @@ function Page() {
</Stack> </Stack>
); );
} }
return ( return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}> <Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
<BackButton /> <BackButton />
</Box> </Box>
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
{/* Page Title */}
<Title
order={1}
ta="center"
c={colors['blue-button']}
fw="bold"
lh={1.2}
>
Data Kesehatan Masyarakat Puskesmas Darmasaba Data Kesehatan Masyarakat Puskesmas Darmasaba
</Text> </Title>
<Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'}> <Box px={{ base: 'md', md: 100 }}>
<Stack gap="lg">
{/* Bar Chart Kematian Kelahiran */} {/* Bar Chart Kematian Kelahiran */}
<Box> <Box>
<Paper p={"xl"} bg={colors['white-trans-1']}> <Paper p="xl" bg={colors['white-trans-1']}>
<Box pb={30}> <Box pb={30}>
<Title order={2} mb="md">Data Kematian dan Kelahiran</Title> <Title order={2} mb="md" ta="center">
Data Kematian dan Kelahiran
</Title>
{chartData.length === 0 ? ( {chartData.length === 0 ? (
<Text c="dimmed" ta="center" py="xl"> <Text c="dimmed" ta="center" py="xl" size="md">
Belum ada data yang tersedia untuk ditampilkan Belum ada data yang tersedia untuk ditampilkan
</Text> </Text>
) : ( ) : (
<> <>
{/* Main Chart */}
<Center> <Center>
<Box h={400}> <Box h={400}>
<Box style={{ <Box style={{
width: isMobile ? '90vw' : isTablet ? '700px' : '800px', width: isMobile ? '90vw' : '800px',
maxWidth: '100%', maxWidth: '100%',
margin: '0 auto' margin: '0 auto'
}}> }}>
@@ -137,16 +140,21 @@ function Page() {
</Center> </Center>
</> </>
)} )}
<Flex pb={30} justify={'center'} gap={'xl'} align={'center'}>
<Flex pb={30} justify="center" gap="xl" align="center" wrap="wrap">
<Box> <Box>
<Flex gap={{ base: 0, md: 5 }} align={'center'}> <Flex gap={{ base: 'xs', md: 'sm' }} align="center">
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Angka Kematian</Text> <Text fw="bold" fz={{ base: 'sm', md: 'md' }}>
<ColorSwatch color="#EF3E3E" size={30} /> Angka Kematian
</Text>
<ColorSwatch color="#EF3E3E" size={30} />
</Flex> </Flex>
</Box> </Box>
<Box> <Box>
<Flex gap={{ base: 0, md: 5 }} align={'center'}> <Flex gap={{ base: 'xs', md: 'sm' }} align="center">
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Angka Kelahiran</Text> <Text fw="bold" fz={{ base: 'sm', md: 'md' }}>
Angka Kelahiran
</Text>
<ColorSwatch color="#3290CA" size={30} /> <ColorSwatch color="#3290CA" size={30} />
</Flex> </Flex>
</Box> </Box>
@@ -154,20 +162,13 @@ function Page() {
</Box> </Box>
</Paper> </Paper>
</Box> </Box>
<GrafikPenyakit /> <GrafikPenyakit />
{/* Artikel Kesehatan */}
<Box> <Box>
<SimpleGrid <SimpleGrid cols={{ base: 1, md: 3 }}>
cols={{
base: 1,
md: 3,
}}
>
{/* Fasilitas Kesehatan */}
<FasilitasKesehatan /> <FasilitasKesehatan />
{/* Jadwal Kegiatan */}
<JadwalKegiatan /> <JadwalKegiatan />
{/* Artikel Kesehatan */}
<ArtikelKesehatanPage /> <ArtikelKesehatanPage />
</SimpleGrid> </SimpleGrid>
</Box> </Box>

View File

@@ -1,7 +1,7 @@
'use client' 'use client'
import infoWabahPenyakit from '@/app/admin/(dashboard)/_state/kesehatan/info-wabah-penyakit/infoWabahPenyakit'; import infoWabahPenyakit from '@/app/admin/(dashboard)/_state/kesehatan/info-wabah-penyakit/infoWabahPenyakit';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { Box, Button, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
@@ -51,10 +51,15 @@ function DetailInfoWabahPenyakitUser() {
shadow="sm" shadow="sm"
> >
<Stack gap="lg"> <Stack gap="lg">
{/* Judul */} {/* Judul — H1 */}
<Text fz="xl" fw="bold" c={colors['blue-button']} ta="center"> <Title
order={1}
fw="bold"
c={colors['blue-button']}
ta="center"
>
{data.name || 'Kontak Darurat'} {data.name || 'Kontak Darurat'}
</Text> </Title>
{/* Gambar */} {/* Gambar */}
{data.image?.link && ( {data.image?.link && (
@@ -70,10 +75,16 @@ function DetailInfoWabahPenyakitUser() {
{/* Deskripsi */} {/* Deskripsi */}
<Box> <Box>
<Text fz="lg" fw="bold">Deskripsi</Text> {/* Section Title — H2 */}
<Title order={2} fw="bold" fz={{ base: 'md', md: 'lg' }} lh="1.4">
Deskripsi
</Title>
<Box pl={20}> <Box pl={20}>
<Text <Text
fz="md" fz={{ base: 'sm', md: 'md' }}
lh="1.6"
c="dimmed"
ta={"justify"}
dangerouslySetInnerHTML={{ __html: data.deskripsiLengkap || '-' }} dangerouslySetInnerHTML={{ __html: data.deskripsiLengkap || '-' }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }} style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/> />

View File

@@ -1,7 +1,18 @@
'use client'; 'use client';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Button, Center, Flex, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import {
Button,
Center,
Flex,
Group,
Image,
Paper,
Skeleton,
Stack,
Text,
Title,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconCalendar, IconInfoCircle, IconPhone } from '@tabler/icons-react'; import { IconArrowBack, IconCalendar, IconInfoCircle, IconPhone } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
@@ -53,15 +64,14 @@ export default function DetailPosyanduUser() {
mx="auto" mx="auto"
> >
<Stack gap="md"> <Stack gap="md">
{/* Header */} {/* Header — Dijadikan Title */}
<Text <Title
ta="center" ta="center"
fz={{ base: '1.8rem', md: '2.2rem' }} order={1}
fw={700}
c={colors['blue-button']} c={colors['blue-button']}
> >
{data.name || 'Posyandu Desa'} {data.name || 'Posyandu Desa'}
</Text> </Title>
{/* Gambar */} {/* Gambar */}
{data.image?.link ? ( {data.image?.link ? (
@@ -78,7 +88,7 @@ export default function DetailPosyanduUser() {
</Center> </Center>
) : ( ) : (
<Center> <Center>
<Text fz="sm" c="dimmed"> <Text fz={{ base: 'xs', md: 'sm' }} c="dimmed">
Tidak ada gambar Tidak ada gambar
</Text> </Text>
</Center> </Center>
@@ -88,7 +98,11 @@ export default function DetailPosyanduUser() {
<Stack gap="sm" mt="md"> <Stack gap="sm" mt="md">
<Flex align="center" gap="xs"> <Flex align="center" gap="xs">
<IconPhone size={18} stroke={1.5} /> <IconPhone size={18} stroke={1.5} />
<Text fz="sm" c="dimmed"> <Text
fz={{ base: 'xs', md: 'sm' }}
c="dimmed"
lh={{ base: 1.5, md: 1.6 }}
>
{data.nomor || 'Nomor tidak tersedia'} {data.nomor || 'Nomor tidak tersedia'}
</Text> </Text>
</Flex> </Flex>
@@ -96,8 +110,9 @@ export default function DetailPosyanduUser() {
<Flex align="center" gap="xs"> <Flex align="center" gap="xs">
<IconCalendar size={18} stroke={1.5} /> <IconCalendar size={18} stroke={1.5} />
<Text <Text
fz="sm" fz={{ base: 'xs', md: 'sm' }}
c="dimmed" c="dimmed"
lh={{ base: 1.5, md: 1.6 }}
dangerouslySetInnerHTML={{ __html: data.jadwalPelayanan || '-' }} dangerouslySetInnerHTML={{ __html: data.jadwalPelayanan || '-' }}
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }} style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
/> />
@@ -106,9 +121,9 @@ export default function DetailPosyanduUser() {
<Flex align="center" gap="xs"> <Flex align="center" gap="xs">
<IconInfoCircle size={18} stroke={1.5} /> <IconInfoCircle size={18} stroke={1.5} />
<Text <Text
fz="sm" fz={{ base: 'xs', md: 'sm' }}
c="dimmed" c="dimmed"
lh={1.6} lh={{ base: 1.5, md: 1.6 }}
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }} dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }} style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
/> />

View File

@@ -1,7 +1,7 @@
'use client' 'use client'
import posyandustate from "@/app/admin/(dashboard)/_state/kesehatan/posyandu/posyandu"; import posyandustate from "@/app/admin/(dashboard)/_state/kesehatan/posyandu/posyandu";
import colors from "@/con/colors"; import colors from "@/con/colors";
import { Badge, Box, Button, Center, Flex, Group, Image, List, ListItem, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from "@mantine/core"; import { Badge, Box, Button, Center, Flex, Group, Image, List, ListItem, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from "@mantine/core";
import { useDebouncedValue, useShallowEffect } from "@mantine/hooks"; import { useDebouncedValue, useShallowEffect } from "@mantine/hooks";
import { IconCalendar, IconInfoCircle, IconPhone, IconSearch } from "@tabler/icons-react"; import { IconCalendar, IconInfoCircle, IconPhone, IconSearch } from "@tabler/icons-react";
import { useState } from "react"; import { useState } from "react";
@@ -12,8 +12,8 @@ import { useTransitionRouter } from "next-view-transitions";
export default function Page() { export default function Page() {
const state = useProxy(posyandustate); const state = useProxy(posyandustate);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay const [debouncedSearch] = useDebouncedValue(search, 1000);
const router = useTransitionRouter() const router = useTransitionRouter();
const { data, page, totalPages, loading, load } = state.findMany; const { data, page, totalPages, loading, load } = state.findMany;
@@ -35,14 +35,13 @@ export default function Page() {
<Box px={{ base: "md", md: 100 }}> <Box px={{ base: "md", md: 100 }}>
<BackButton /> <BackButton />
<Flex mt="md" justify="space-between" align="center" wrap="wrap" gap="md"> <Flex mt="md" justify="space-between" align="center" wrap="wrap" gap="md">
<Text <Title
order={1}
ta="left" ta="left"
fz={{ base: "1.8rem", md: "2.5rem" }}
c={colors["blue-button"]} c={colors["blue-button"]}
fw="bold"
> >
Posyandu Desa Darmasaba Posyandu Desa Darmasaba
</Text> </Title>
<TextInput <TextInput
placeholder="Cari posyandu berdasarkan nama..." placeholder="Cari posyandu berdasarkan nama..."
aria-label="Pencarian Posyandu" aria-label="Pencarian Posyandu"
@@ -55,6 +54,7 @@ export default function Page() {
/> />
</Flex> </Flex>
</Box> </Box>
<Title c={"dimmed"} order={3} ta={"center"}>Belum ada posyandu yang terdaftar</Title>
</Stack> </Stack>
); );
} }
@@ -64,14 +64,13 @@ export default function Page() {
<Box px={{ base: "md", md: 100 }}> <Box px={{ base: "md", md: 100 }}>
<BackButton /> <BackButton />
<Flex mt="md" justify="space-between" align="center" wrap="wrap" gap="md"> <Flex mt="md" justify="space-between" align="center" wrap="wrap" gap="md">
<Text <Title
order={1}
ta="left" ta="left"
fz={{ base: "1.8rem", md: "2.5rem" }}
c={colors["blue-button"]} c={colors["blue-button"]}
fw="bold"
> >
Posyandu Desa Darmasaba Posyandu Desa Darmasaba
</Text> </Title>
<TextInput <TextInput
placeholder="Cari posyandu berdasarkan nama..." placeholder="Cari posyandu berdasarkan nama..."
aria-label="Pencarian Posyandu" aria-label="Pencarian Posyandu"
@@ -116,9 +115,9 @@ export default function Page() {
> >
<Stack gap="sm"> <Stack gap="sm">
<Group justify="space-between" align="center"> <Group justify="space-between" align="center">
<Text c={colors["blue-button"]} fw="bold" fz="lg" lineClamp={1}> <Title order={3} c={colors["blue-button"]} fw="bold" lineClamp={1}>
{v.name} {v.name}
</Text> </Title>
<Badge color="blue" variant="light" size="sm" radius="sm"> <Badge color="blue" variant="light" size="sm" radius="sm">
Aktif Aktif
</Badge> </Badge>
@@ -137,7 +136,7 @@ export default function Page() {
<Flex align="flex-start" gap="xs"> <Flex align="flex-start" gap="xs">
<IconPhone size={18} stroke={1.5} style={{ marginTop: 3 }} /> <IconPhone size={18} stroke={1.5} style={{ marginTop: 3 }} />
<Box> <Box>
<Text fz="sm" c="dimmed" lh={1.4}> <Text fz={{ base: "xs", md: "sm" }} c="dimmed" lh={1.4}>
{v.nomor || "Tidak tersedia"} {v.nomor || "Tidak tersedia"}
</Text> </Text>
</Box> </Box>
@@ -146,7 +145,7 @@ export default function Page() {
<Flex align="flex-start" gap="xs"> <Flex align="flex-start" gap="xs">
<IconCalendar size={18} stroke={1.5} style={{ marginTop: 3 }} /> <IconCalendar size={18} stroke={1.5} style={{ marginTop: 3 }} />
<Box> <Box>
<Text fz="sm" c="dimmed" lh={1.4}> <Text fz={{ base: "xs", md: "sm" }} c="dimmed" lh={1.4}>
<strong>Jadwal:</strong>{" "} <strong>Jadwal:</strong>{" "}
<span <span
style={{ wordBreak: "break-word", whiteSpace: "normal" }} style={{ wordBreak: "break-word", whiteSpace: "normal" }}
@@ -159,7 +158,7 @@ export default function Page() {
<Flex align="flex-start" gap="xs"> <Flex align="flex-start" gap="xs">
<IconInfoCircle size={18} stroke={1.5} style={{ marginTop: 3 }} /> <IconInfoCircle size={18} stroke={1.5} style={{ marginTop: 3 }} />
<Text <Text
fz="sm" fz={{ base: "xs", md: "sm" }}
lh={1.5} lh={1.5}
c="dimmed" c="dimmed"
dangerouslySetInnerHTML={{ __html: v.deskripsi }} dangerouslySetInnerHTML={{ __html: v.deskripsi }}
@@ -168,7 +167,9 @@ export default function Page() {
truncate="end" truncate="end"
/> />
</Flex> </Flex>
<Button radius="lg" size="md" variant="outline" onClick={() => router.push(`/darmasaba/kesehatan/posyandu/${v.id}`)}>Detail</Button> <Button radius="lg" size="md" variant="outline" onClick={() => router.push(`/darmasaba/kesehatan/posyandu/${v.id}`)}>
Detail
</Button>
</Stack> </Stack>
</Paper> </Paper>
))} ))}
@@ -191,11 +192,11 @@ export default function Page() {
<Stack gap="sm"> <Stack gap="sm">
<Flex align="center" gap="xs"> <Flex align="center" gap="xs">
<IconInfoCircle size={22} color={colors["blue-button"]} /> <IconInfoCircle size={22} color={colors["blue-button"]} />
<Text fz="lg" fw="bold" c={colors["blue-button"]}> <Title order={2} c={colors["blue-button"]}>
Layanan Utama Posyandu Layanan Utama Posyandu
</Text> </Title>
</Flex> </Flex>
<List spacing="xs" size="sm" center> <List spacing="xs" fz={{ base: "xs", md: "sm" }} center>
<ListItem>Penimbangan bayi dan balita</ListItem> <ListItem>Penimbangan bayi dan balita</ListItem>
<ListItem>Pemantauan status gizi</ListItem> <ListItem>Pemantauan status gizi</ListItem>
<ListItem>Imunisasi dasar lengkap</ListItem> <ListItem>Imunisasi dasar lengkap</ListItem>

View File

@@ -91,7 +91,7 @@ function StrukturOrganisasiPPID() {
const debouncedSearch = useRef( const debouncedSearch = useRef(
debounce((value: string) => { debounce((value: string) => {
setSearchQuery(value) setSearchQuery(value)
}, 400) }, 1000)
).current ).current
useEffect(() => { useEffect(() => {

View File

@@ -2,7 +2,7 @@
'use client'; 'use client';
import penghargaanState from "@/app/admin/(dashboard)/_state/desa/penghargaan"; import penghargaanState from "@/app/admin/(dashboard)/_state/desa/penghargaan";
import colors from "@/con/colors"; import colors from "@/con/colors";
import { Box, Button, Container, Group, Paper, Skeleton, Stack, Text } from "@mantine/core"; import { Box, Button, Container, Group, Paper, Skeleton, Stack, Text, Title } from "@mantine/core";
import { useMediaQuery } from "@mantine/hooks"; import { useMediaQuery } from "@mantine/hooks";
import { IconArrowRight, IconAward } from "@tabler/icons-react"; import { IconArrowRight, IconAward } from "@tabler/icons-react";
import { useTransitionRouter } from "next-view-transitions"; import { useTransitionRouter } from "next-view-transitions";
@@ -20,11 +20,21 @@ export default function Page() {
<Stack align="center" gap="sm"> <Stack align="center" gap="sm">
<Group gap="xs"> <Group gap="xs">
<IconAward size={40} color={colors["blue-button"]} /> <IconAward size={40} color={colors["blue-button"]} />
<Text fz={{ base: "2rem", md: "3.2rem" }} fw={800} variant="gradient" gradient={{ from: "#1C6EA4", to: "#69BFF8" }}> <Title
order={1}
fw={800}
c={colors["blue-button"]}
ta="center"
>
Penghargaan Desa Penghargaan Desa
</Text> </Title>
</Group> </Group>
<Text fz="lg" c="dimmed" ta="center"> <Text
fz={{ base: "sm", md: "md" }}
lh={{ base: "1.5", md: "1.6" }}
c="black"
ta="center"
>
Desa Darmasaba berhasil meraih beragam penghargaan bergengsi yang mencerminkan dedikasi dan kerja keras masyarakat dalam membangun desa yang maju dan berkelanjutan. Desa Darmasaba berhasil meraih beragam penghargaan bergengsi yang mencerminkan dedikasi dan kerja keras masyarakat dalam membangun desa yang maju dan berkelanjutan.
</Text> </Text>
</Stack> </Stack>
@@ -61,10 +71,8 @@ function Slider() {
const data = state.findMany.data || []; const data = state.findMany.data || [];
const loading = state.findMany.loading; const loading = state.findMany.loading;
// Triple data untuk infinite loop (desktop only)
const slidesData = mobile ? data : [...data, ...data, ...data]; const slidesData = mobile ? data : [...data, ...data, ...data];
// Auto-scroll animation untuk desktop
useEffect(() => { useEffect(() => {
if (loading || !containerRef.current || data.length === 0 || mobile) return; if (loading || !containerRef.current || data.length === 0 || mobile) return;
@@ -72,7 +80,6 @@ function Slider() {
const slideWidth = container.scrollWidth / slidesData.length; const slideWidth = container.scrollWidth / slidesData.length;
const originalLength = data.length; const originalLength = data.length;
// Start dari middle set
scrollPosRef.current = slideWidth * originalLength; scrollPosRef.current = slideWidth * originalLength;
container.scrollLeft = scrollPosRef.current; container.scrollLeft = scrollPosRef.current;
@@ -88,7 +95,6 @@ function Slider() {
const speed = isHoveredRef.current ? SPEED_HOVER : SPEED_NORMAL; const speed = isHoveredRef.current ? SPEED_HOVER : SPEED_NORMAL;
scrollPosRef.current += speed; scrollPosRef.current += speed;
// Reset untuk infinite loop
if (scrollPosRef.current >= slideWidth * (originalLength * 2)) { if (scrollPosRef.current >= slideWidth * (originalLength * 2)) {
scrollPosRef.current -= slideWidth * originalLength; scrollPosRef.current -= slideWidth * originalLength;
} }
@@ -100,7 +106,6 @@ function Slider() {
} else { } else {
scrollPosRef.current = container.scrollLeft; scrollPosRef.current = container.scrollLeft;
// Momentum untuk drag release
if (!isDraggingRef.current && Math.abs(velocityRef.current) > 0.1) { if (!isDraggingRef.current && Math.abs(velocityRef.current) > 0.1) {
scrollPosRef.current += velocityRef.current; scrollPosRef.current += velocityRef.current;
velocityRef.current *= VELOCITY_DECAY; velocityRef.current *= VELOCITY_DECAY;
@@ -185,7 +190,7 @@ function Slider() {
return ( return (
<Stack align="center" py="xl"> <Stack align="center" py="xl">
<IconAward size={56} color={colors["blue-button"]} /> <IconAward size={56} color={colors["blue-button"]} />
<Text fz="lg" fw={600} c="dimmed"> <Text fz={{ base: "sm", md: "md" }} fw={600} c="dimmed" ta="center">
Belum ada penghargaan yang ditambahkan Belum ada penghargaan yang ditambahkan
</Text> </Text>
</Stack> </Stack>
@@ -213,7 +218,6 @@ function Slider() {
msOverflowStyle: "none", msOverflowStyle: "none",
}} }}
> >
{/* Blur edges - hanya untuk desktop */}
{!mobile && ( {!mobile && (
<> <>
<Box <Box
@@ -291,8 +295,8 @@ function Slider() {
style={{ borderRadius: 16 }} style={{ borderRadius: 16 }}
/> />
<Stack justify="flex-end" h="100%" gap="sm" p="lg" pos="relative"> <Stack justify="flex-end" h="100%" gap="sm" p="lg" pos="relative">
<Text <Title
fz={{ base: "lg", sm: "xl", md: "1.5rem" }} order={3}
fw={700} fw={700}
ta="center" ta="center"
c="white" c="white"
@@ -300,7 +304,7 @@ function Slider() {
style={{ textShadow: "0 2px 8px rgba(0,0,0,0.8)" }} style={{ textShadow: "0 2px 8px rgba(0,0,0,0.8)" }}
> >
{item.name} {item.name}
</Text> </Title>
<Group justify="center"> <Group justify="center">
<Button <Button
onClick={() => router.push(`/darmasaba/penghargaan/${item.id}`)} onClick={() => router.push(`/darmasaba/penghargaan/${item.id}`)}

View File

@@ -74,7 +74,7 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
<Tooltip label="Profil Saya" position="bottom" withArrow> <Tooltip label="Profil Saya" position="bottom" withArrow>
<ActionIcon <ActionIcon
onClick={() => { onClick={() => {
next.push("/admin/landing-page/profile/program-inovasi") next.push("/admin/landing-page/profil/program-inovasi")
}} }}
color={colors["blue-button"]} color={colors["blue-button"]}
radius="xl" radius="xl"

View File

@@ -96,7 +96,7 @@ function Potensi() {
</Stack> </Stack>
) : ( ) : (
/* CARD LIST */ /* CARD LIST */
<SimpleGrid cols={{ base: 1, sm: 2 }}> <SimpleGrid cols={{ base: 1, sm: 2 }} px={{base: 'md', md: 80}}>
{_.take(data, 4).map((v, k) => ( {_.take(data, 4).map((v, k) => (
<motion.div <motion.div
key={k} key={k}

View File

@@ -6,20 +6,20 @@ const getDetailUrl = (item: { type?: string; id: string | number;[key: string]:
sdgsdesa: () => '/darmasaba/sdgs-desa', sdgsdesa: () => '/darmasaba/sdgs-desa',
apbdes: () => '/darmasaba/apbdes', apbdes: () => '/darmasaba/apbdes',
prestasidesa: () => '/darmasaba/prestasi-desa', prestasidesa: () => '/darmasaba/prestasi-desa',
pejabatdesa: () => '/darmasaba/ppid/profile-ppid', pejabatdesa: () => '/darmasaba/ppid/profil-ppid',
strukturppid: () => '/darmasaba/ppid/struktur-ppid', strukturppid: () => '/darmasaba/ppid/struktur-ppid',
visimisippid: () => '/darmasaba/ppid/visi-misi', visimisippid: () => '/darmasaba/ppid/visi-misi',
dasarhukumppid: () => '/darmasaba/ppid/dasar-hukum', dasarhukumppid: () => '/darmasaba/ppid/dasar-hukum',
profileppid: () => '/darmasaba/ppid/profile', profileppid: () => '/darmasaba/ppid/profil',
daftarinformasipublik: () => '/darmasaba/ppid/daftar-informasi-publik', daftarinformasipublik: () => '/darmasaba/ppid/daftar-informasi-publik',
perbekeldarmasaba: () => '/darmasaba/desa/profile', perbekeldarmasaba: () => '/darmasaba/desa/profil',
berita: (id, kategori) => `/darmasaba/desa/berita/${kategori}/${id}`, berita: (id, kategori) => `/darmasaba/desa/berita/${kategori}/${id}`,
pengumuman: (id, kategori) => `/darmasaba/desa/pengumuman/${kategori}/${id}`, pengumuman: (id, kategori) => `/darmasaba/desa/pengumuman/${kategori}/${id}`,
sejarahdesa: () => '/darmasaba/desa/profile', sejarahdesa: () => '/darmasaba/desa/profil',
visimisidesa: () => '/darmasaba/desa/profile', visimisidesa: () => '/darmasaba/desa/profil',
lambangdesa: () => '/darmasaba/desa/profile', lambangdesa: () => '/darmasaba/desa/profil',
maskotdesa: () => '/darmasaba/desa/profile', maskotdesa: () => '/darmasaba/desa/profil',
profilperbekel: () => '/darmasaba/desa/profile', profilperbekel: () => '/darmasaba/desa/profil',
potensi: () => '/darmasaba/desa/potensi-desa', potensi: () => '/darmasaba/desa/potensi-desa',
galleryFoto: () => '/darmasaba/desa/gallery/foto', galleryFoto: () => '/darmasaba/desa/gallery/foto',
galleryVideo: () => '/darmasaba/desa/gallery/video', galleryVideo: () => '/darmasaba/desa/gallery/video',