Merge pull request 'nico/3-nov-25' (#5) from nico/3-nov-25 into staging

Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/5
This commit is contained in:
2025-11-03 10:29:31 +08:00
52 changed files with 1139 additions and 686 deletions

View File

@@ -67,16 +67,26 @@ function Page() {
<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>
<Image {/* ✅ Bagian gambar dibuat konsisten tanpa CSS manual */}
src={state.findUnique.data?.image?.link || ''} <Box
alt={state.findUnique.data?.name || 'Potensi Desa'}
radius="lg"
fit="cover"
w="100%" w="100%"
h={{ base: 220, md: 400 }} h={{ base: 220, md: 400 }}
fallbackSrc="https://placehold.co/800x400?text=Gambar+tidak+tersedia" style={{
loading="lazy" overflow: 'hidden',
/> borderRadius: 'var(--mantine-radius-lg)',
}}
>
<Image
src={state.findUnique.data?.image?.link || ''}
alt={state.findUnique.data?.name || 'Potensi Desa'}
fit="cover"
w="100%"
h="100%"
fallbackSrc="https://placehold.co/800x400?text=Gambar+tidak+tersedia"
loading="lazy"
radius="lg"
/>
</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={1.8} dangerouslySetInnerHTML={{ __html: state.findUnique.data?.deskripsi || 'Belum ada deskripsi untuk potensi desa ini.' }} />
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -26,7 +26,7 @@ function Page() {
</Text> </Text>
</Stack> </Stack>
</Container> </Container>
<Box px={{ base: "md", md: 100 }}> <Stack px={{ base: "md", md: 100 }} gap={"xl"}>
<ProfileDesa /> <ProfileDesa />
<SejarahDesa /> <SejarahDesa />
<VisimisiDesa /> <VisimisiDesa />
@@ -35,7 +35,7 @@ function Page() {
<ProfilPerbekel /> <ProfilPerbekel />
<MotoDesa /> <MotoDesa />
<SemuaPerbekel /> <SemuaPerbekel />
</Box> </Stack>
</Stack> </Stack>
{/* Tombol Scroll ke Atas */} {/* Tombol Scroll ke Atas */}
<ScrollToTopButton /> <ScrollToTopButton />

View File

@@ -24,7 +24,7 @@ function LambangDesa() {
} }
return ( return (
<Box pb={90}> <Box>
<Stack align="center" gap="lg"> <Stack align="center" gap="lg">
<Box pb="lg"> <Box pb="lg">
<Center> <Center>
@@ -59,7 +59,7 @@ function LambangDesa() {
}} }}
> >
<Text <Text
fz={{ base: 'md', md: 'lg' }} fz={{ base: '1.125rem', md: '1.375rem' }}
lh={1.8} lh={1.8}
c="dark" c="dark"
ta="justify" ta="justify"

View File

@@ -28,7 +28,7 @@ function MaskotDesa() {
} }
return ( return (
<Box pb={80}> <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 src="/pudak-icon.png" alt="Ikon Desa" w={{ base: 160, md: 240 }} loading="lazy"/>

View File

@@ -36,7 +36,7 @@ const letters = ["S", "I", "G", "A", "P"];
function MotoDesa() { function MotoDesa() {
return ( return (
<Box pb={80} px={{ base: "md", md: "xl" }}> <Box px={{ base: "md", md: "xl" }}>
<Stack align="center" gap="lg"> <Stack align="center" gap="lg">
<Box> <Box>
<Text <Text

View File

@@ -25,7 +25,7 @@ function ProfilPerbekel() {
} }
return ( return (
<Box pb={80} px="md"> <Box px="md">
<Stack align="center" gap={0} mb={40}> <Stack align="center" gap={0} mb={40}>
<Text <Text
c={colors['blue-button']} c={colors['blue-button']}
@@ -116,7 +116,7 @@ function ProfilPerbekel() {
</Stack> </Stack>
<Text <Text
fz={{ base: "1rem", md: "1.2rem" }} fz={{ base: "1rem", md: "1.2rem" }}
ta="justify" ta="left"
lh={1.6} lh={1.6}
dangerouslySetInnerHTML={{ __html: data.pengalaman }} dangerouslySetInnerHTML={{ __html: data.pengalaman }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }} style={{ wordBreak: "break-word", whiteSpace: "normal" }}

View File

@@ -3,7 +3,7 @@ import { Box, Center, Paper } from '@mantine/core';
function ProfileDesa() { function ProfileDesa() {
return ( return (
<Box pb={90}> <Box>
<Center> <Center>
<Paper p={"xl"} bg={colors['white-trans-1']} w={{ base: "100%", md: "100%" }}> <Paper p={"xl"} bg={colors['white-trans-1']} w={{ base: "100%", md: "100%" }}>
<Center> <Center>

View File

@@ -24,7 +24,7 @@ function SejarahDesa() {
} }
return ( return (
<Box py="xl"> <Box>
<Stack align="center" gap="xl"> <Stack align="center" gap="xl">
<Stack align="center" gap="sm"> <Stack align="center" gap="sm">
<Center> <Center>

View File

@@ -36,7 +36,7 @@ function SemuaPerbekel() {
} }
return ( return (
<Box pb={80}> <Box>
<Stack align="center" gap="lg"> <Stack align="center" gap="lg">
<Box> <Box>
<Text <Text

View File

@@ -24,7 +24,7 @@ function VisiMisiDesa() {
} }
return ( return (
<Box py="xl"> <Box>
<Stack align="center" gap="xl"> <Stack align="center" gap="xl">
<Image <Image
src="/darmasaba-icon.png" src="/darmasaba-icon.png"

View File

@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
'use client' 'use client'
import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi' import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi'
import colors from '@/con/colors' import colors from '@/con/colors'
import { import {
@@ -9,20 +9,28 @@ import {
Button, Button,
Card, Card,
Center, Center,
Container,
Group, Group,
Image, Image,
Loader, Loader,
Paper, Paper,
Stack, Stack,
Text, Text,
TextInput,
Title, Title,
Tooltip, Transition
Transition,
} from '@mantine/core' } from '@mantine/core'
import { IconRefresh, IconSearch, IconUsers } from '@tabler/icons-react' import {
IconArrowsMaximize,
IconArrowsMinimize,
IconRefresh,
IconSearch,
IconUsers,
IconZoomIn,
IconZoomOut,
} from '@tabler/icons-react'
import { debounce } from 'lodash'
import { OrganizationChart } from 'primereact/organizationchart' import { OrganizationChart } from 'primereact/organizationchart'
import { useEffect } 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'
@@ -36,35 +44,40 @@ export default function Page() {
paddingBottom: 48, paddingBottom: 48,
}} }}
> >
<Container size="xl" py="xl"> <Box px={{ base: 'md', md: 100 }} py="xl">
<Box px={{ base: 'md', md: 100 }}> <BackButton />
<BackButton />
</Box>
<Stack align="center" gap="xl" mt="xl"> <Stack align="center" gap="xl" mt="xl">
<Title <Title
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, lg: 44 }}
> >
Struktur Organisasi Dan SK Pengurus BumDes Struktur Organisasi & SK Pengurus BumDes
</Title> </Title>
<Text ta="center" c="black" maw={800}> <Text ta="center" c="black" maw={800}>
Gambaran visual peran dan pegawai yang ditugaskan. Arahkan kursor Gambaran visual peran dan pengurus yang ditugaskan. Gunakan kontrol
untuk melihat detail atau klik node untuk fokus tampilan. di bawah untuk mencari, memperbesar, atau melihat lebih jelas.
</Text> </Text>
</Stack> </Stack>
<Box mt="lg"> <Box mt="lg">
<StrukturOrganisasiBumDes /> <StrukturOrganisasiBumDes />
</Box> </Box>
</Container> </Box>
</Box> </Box>
) )
} }
function StrukturOrganisasiBumDes() { function StrukturOrganisasiBumDes() {
const stateOrganisasi: any = useProxy(stateStrukturBumDes.pegawai) const stateOrganisasi: any = useProxy(stateStrukturBumDes.pegawai)
const chartContainerRef = useRef<HTMLDivElement>(null)
const [scale, setScale] = useState(1)
const [isFullscreen, setFullscreen] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
const debouncedSearch = useRef(
debounce((value: string) => setSearchQuery(value), 400)
).current
useEffect(() => { useEffect(() => {
void stateOrganisasi.findMany.load() void stateOrganisasi.findMany.load()
@@ -81,17 +94,15 @@ function StrukturOrganisasiBumDes() {
<Loader size="lg" /> <Loader size="lg" />
<Text fw={600}>Memuat struktur organisasi</Text> <Text fw={600}>Memuat struktur organisasi</Text>
<Text c="dimmed" size="sm"> <Text c="dimmed" size="sm">
Mengambil data pegawai dan posisi. Mohon tunggu sebentar. Mengambil data pengurus dan posisi. Mohon tunggu sebentar.
</Text> </Text>
</Stack> </Stack>
</Center> </Center>
) )
} }
if ( const data = stateOrganisasi.findMany.data || []
!stateOrganisasi.findMany.data || if (data.length === 0) {
stateOrganisasi.findMany.data.length === 0
) {
return ( return (
<Center py={40}> <Center py={40}>
<Stack align="center" gap="md"> <Stack align="center" gap="md">
@@ -109,11 +120,10 @@ function StrukturOrganisasiBumDes() {
<IconUsers size={56} /> <IconUsers size={56} />
</Center> </Center>
<Title order={3} mt="md"> <Title order={3} mt="md">
Data pegawai belum tersedia Data pengurus belum tersedia
</Title> </Title>
<Text c="dimmed" mt="xs"> <Text c="dimmed" mt="xs">
Belum ada data pegawai yang tercatat untuk BumDes. Silakan coba Belum ada data pengurus yang tercatat untuk BumDes.
muat ulang atau periksa sumber data.
</Text> </Text>
<Group justify="center" mt="lg"> <Group justify="center" mt="lg">
<Button <Button
@@ -124,15 +134,6 @@ function StrukturOrganisasiBumDes() {
> >
Muat Ulang Muat Ulang
</Button> </Button>
<Button
leftSection={<IconSearch size={16} />}
variant="subtle"
onClick={() =>
stateOrganisasi.findMany.load({ query: { q: '' } })
}
>
Cari Pegawai
</Button>
</Group> </Group>
</Paper> </Paper>
</Stack> </Stack>
@@ -140,161 +141,232 @@ function StrukturOrganisasiBumDes() {
) )
} }
// 📊 susun struktur organisasi
const posisiMap = new Map<string, any>() const posisiMap = new Map<string, any>()
const aktifPegawai = data.filter((p: any) => p.isActive)
const aktifPegawai = stateOrganisasi.findMany.data.filter((p: any) => p.isActive);
for (const pegawai of aktifPegawai) { for (const pegawai of aktifPegawai) {
const posisiId = pegawai.posisi.id; const posisiId = pegawai.posisi.id
if (!posisiMap.has(posisiId)) { if (!posisiMap.has(posisiId)) {
posisiMap.set(posisiId, { posisiMap.set(posisiId, {
...pegawai.posisi, ...pegawai.posisi,
pegawaiList: [], pegawaiList: [],
children: [], children: [],
}); })
} }
posisiMap.get(posisiId)!.pegawaiList.push(pegawai); posisiMap.get(posisiId)!.pegawaiList.push(pegawai)
} }
// First, create a map of all unique positions const root: any[] = []
const allPositions = new Map(); posisiMap.forEach((posisi) => {
aktifPegawai.forEach((pegawai: any) => { if (posisi.parentId) {
if (!allPositions.has(pegawai.posisi.id)) { const parent = posisiMap.get(posisi.parentId)
allPositions.set(pegawai.posisi.id, { if (parent) parent.children.push(posisi)
...pegawai.posisi, else root.push(posisi)
pegawaiList: [], } else root.push(posisi)
children: [] })
});
}
});
// Then assign employees to their positions const toOrgChartFormat = (node: any): any => {
aktifPegawai.forEach((pegawai: any) => { const pegawai = node.pegawaiList?.[0]
const posisi = allPositions.get(pegawai.posisi.id);
if (posisi) {
posisi.pegawaiList.push(pegawai);
}
});
// Now build the hierarchy
const root = [];
for (const [_, posisi] of allPositions) {
if (posisi.parentId) {
const parent = allPositions.get(posisi.parentId);
if (parent) {
parent.children.push(posisi);
} else {
// Only add to root if it's a top-level position
if (!posisi.parentId) {
root.push(posisi);
}
}
} else {
root.push(posisi);
}
}
function toOrgChartFormat(node: any): any {
return { return {
expanded: true, expanded: true,
type: 'person',
styleClass: 'p-person',
data: { data: {
name: node.pegawaiList?.[0]?.namaLengkap || 'Belum ditugaskan', id: pegawai?.id,
title: node.nama || 'Tanpa jabatan', name: pegawai?.namaLengkap || 'Belum Ditugaskan',
image: node.pegawaiList?.[0]?.image?.link || '/img/default.png', title: node.nama || 'Tanpa Jabatan',
image: pegawai?.image?.link || '/img/default.png',
description: node.deskripsi || '', description: node.deskripsi || '',
positionId: node.id || null,
}, },
children: node.children?.map(toOrgChartFormat) || [], children: node.children?.map(toOrgChartFormat) || [],
} }
} }
const chartData = root.map(toOrgChartFormat) let chartData = root.map(toOrgChartFormat)
// 🔍 filter by search
if (searchQuery) {
const filterNodes = (nodes: any[]): any[] =>
nodes
.map((n) => ({
...n,
children: filterNodes(n.children || []),
}))
.filter(
(n) =>
n.data.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
n.data.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
n.children.length > 0
)
chartData = filterNodes(chartData)
}
// 🔍 fullscreen dan zoom control
const toggleFullscreen = () => {
if (!document.fullscreenElement) {
chartContainerRef.current?.requestFullscreen()
setFullscreen(true)
} else {
document.exitFullscreen()
setFullscreen(false)
}
}
const handleZoomIn = () => setScale((s) => Math.min(s + 0.1, 2))
const handleZoomOut = () => setScale((s) => Math.max(s - 0.1, 0.5))
const resetZoom = () => setScale(1)
return ( return (
<Box py={16} > <Stack align="center" mt="xl">
<Paper {/* 🧭 Kontrol atas */}
radius="md" <Paper shadow="xs" p="md" radius="md" bg={colors['blue-button']}>
p="md" <Group gap="sm" wrap="wrap" justify="center">
style={{ <TextInput
background: 'rgba(28,110,164,0.2)', placeholder="Cari nama atau jabatan..."
border: `1px solid rgba(255,255,255,0.1)`, leftSection={<IconSearch size={16} />}
overflowX: 'auto', onChange={(e) => debouncedSearch(e.target.value)}
}} styles={{
> input: {
<OrganizationChart minWidth: 250,
value={chartData} },
nodeTemplate={nodeTemplate} }}
/> />
<Group gap="xs">
<Button
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={{
fontSize: 14,
fontWeight: 700,
borderRadius: '8px',
minWidth: 70,
textAlign: 'center',
}}
>
{Math.round(scale * 100)}%
</Box>
<Button
variant="light"
bg={colors['blue-button-2']}
c={colors['blue-button']}
size="sm"
onClick={handleZoomIn}
leftSection={<IconZoomIn size={16} />}
>
Zoom In
</Button>
<Button
variant="light"
bg={colors['blue-button-2']}
c={colors['blue-button']}
size="sm"
onClick={resetZoom}
>
Reset
</Button>
<Button
variant="light"
bg={colors['blue-button-2']}
c={colors['blue-button']}
size="sm"
onClick={toggleFullscreen}
leftSection={
isFullscreen ? (
<IconArrowsMinimize size={16} />
) : (
<IconArrowsMaximize size={16} />
)
}
>
Fullscreen
</Button>
</Group>
</Group>
</Paper> </Paper>
</Box>
{/* 📊 Chart Container */}
<Center style={{ width: '100%' }}>
<Box
ref={chartContainerRef}
style={{
overflowX: 'auto',
overflowY: 'auto',
width: '100%',
padding: '32px 16px',
transition: 'transform 0.2s ease',
transform: `scale(${scale})`,
transformOrigin: 'center top',
}}
>
<OrganizationChart
value={chartData}
nodeTemplate={(node) => <NodeCard node={node} />}
className="p-organizationchart p-organizationchart-horizontal"
/>
</Box>
</Center>
</Stack>
) )
} }
function nodeTemplate(node: any) { function NodeCard({ node }: any) {
const imageSrc = node?.data?.image || '/img/default.png' const imageSrc = node?.data?.image || '/img/default.png'
const name = node?.data?.name || 'Tanpa Nama' const name = node?.data?.name || 'Tanpa Nama'
const title = node?.data?.title || 'Tanpa Jabatan' const title = node?.data?.title || 'Tanpa Jabatan'
const description = node?.data?.description || '' const description = node?.data?.description || ''
return ( return (
<Transition mounted transition="pop" duration={240}> <Transition mounted transition="pop" duration={300}>
{(styles) => ( {(styles) => (
<Card <Card
radius="lg" shadow="md"
radius="xl"
withBorder withBorder
style={{ style={{
...styles, ...styles,
width: 260, width: 240,
padding: 16, padding: 20,
background: 'rgba(28,110,164,0.3)', background:
borderColor: 'rgba(255,255,255,0.15)', 'linear-gradient(135deg, rgba(28,110,164,0.15) 0%, rgba(255,255,255,0.95) 100%)',
display: 'flex', borderColor: 'rgba(28, 110, 164, 0.3)',
flexDirection: 'column', transition: 'all 0.3s ease',
alignItems: 'center',
textAlign: 'center',
}} }}
> >
<Image <Stack align="center" gap={10}>
src={imageSrc} <Box
alt={name} style={{
radius="md" width: 90,
width={120} height: 90,
height={120} borderRadius: '50%',
fit="cover" overflow: 'hidden',
style={{ border: '3px solid rgba(28, 110, 164, 0.4)',
objectFit: 'cover',
border: '2px solid rgba(255,255,255,0.2)',
marginBottom: 12,
}}
loading='lazy'
/>
<Text fw={700}>{name}</Text>
<Text size="sm" c="dimmed" mt={4}>
{title}
</Text>
<Text size="xs" c="dimmed" mt={8} lineClamp={3}>
{description || 'Belum ada deskripsi.'}
</Text>
<Tooltip label="Kembali ke struktur organisasi" withArrow position="bottom">
<Button
variant="light"
size="xs"
mt="md"
onClick={() => {
const id = node?.data?.positionId
if (id && (window as any).scrollTo) {
;(window as any).scrollTo({ top: 0, behavior: 'smooth' })
}
}} }}
> >
Kembali <Image src={imageSrc} alt={name} fit="cover" loading="lazy" />
</Button> </Box>
</Tooltip> <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>
</Card> </Card>
)} )}
</Transition> </Transition>
) )
} }

View File

@@ -57,7 +57,8 @@ function Page() {
/> />
</GridCol> </GridCol>
</Grid> </Grid>
<Text fz={'h4'}>Desa Darmasaba berkomitmen mengembangkan teknologi tepat guna yang sesuai dengan kebutuhan masyarakat, mendukung pembangunan berkelanjutan, dan meningkatkan kualitas hidup warga.</Text> <Text fz={'md'}>Desa Darmasaba berkomitmen mengembangkan teknologi tepat guna yang sesuai dengan kebutuhan masyarakat,</Text>
<Text fz={'md'}>mendukung pembangunan berkelanjutan, dan meningkatkan kualitas hidup warga.</Text>
</Box> </Box>
<Box px={{ base: "md", md: 100 }}> <Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'} p={'lg'}> <Stack gap={'lg'} p={'lg'}>

View File

@@ -0,0 +1,89 @@
'use client'
import { Box, Center, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'
import { useShallowEffect } from '@mantine/hooks'
import { useParams } from 'next/navigation'
import { useProxy } from 'valtio/utils'
import keamananLingkunganState from '@/app/admin/(dashboard)/_state/keamanan/keamanan-lingkungan'
import colors from '@/con/colors'
import BackButton from '../../../desa/layanan/_com/BackButto'
function DetailKeamananLingkunganUser() {
const keamananState = useProxy(keamananLingkunganState)
const params = useParams()
// Ambil data berdasarkan ID dari URL
useShallowEffect(() => {
keamananState.findUnique.load(params?.id as string)
}, [])
// Loading state
if (!keamananState.findUnique.data) {
return (
<Stack py={40}>
<Skeleton height={500} radius="md" />
</Stack>
)
}
const data = keamananState.findUnique.data
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="lg">
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
{/* Wrapper Detail */}
<Paper
withBorder
w={{ base: '100%', md: '80%' }}
mx="auto"
bg={colors['white-1']}
p="xl"
radius="lg"
shadow="md"
>
<Stack gap="lg">
{/* Judul */}
<Text
ta="center"
fz={{ base: 'xl', md: '2xl' }}
fw={700}
c={colors['blue-button']}
>
{data?.name || 'Tanpa Judul'}
</Text>
{/* Gambar */}
<Center>
<Image
w={{ base: 250, sm: 400, md: 550 }}
src={data?.image?.link}
alt={data?.name || 'gambar keamanan lingkungan'}
radius="md"
loading="lazy"
fit="cover"
/>
</Center>
{/* Deskripsi */}
<Box>
<Text fz="lg" fw="bold" mb={5}>
Deskripsi
</Text>
<Text
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{ __html: data?.deskripsi || '-' }}
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
/>
</Box>
</Stack>
</Paper>
</Box>
</Stack>
)
}
export default DetailKeamananLingkunganUser

View File

@@ -1,17 +1,18 @@
'use client' 'use client'
import keamananLingkunganState from '@/app/admin/(dashboard)/_state/keamanan/keamanan-lingkungan'; import keamananLingkunganState from '@/app/admin/(dashboard)/_state/keamanan/keamanan-lingkungan';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Center, Grid, GridCol, Image, Pagination, Paper, SimpleGrid, Skeleton, Spoiler, Stack, Text, TextInput } from '@mantine/core'; import { Box, Button, Center, Grid, GridCol, Image, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } 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';
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 { useRouter } from 'next/navigation';
function Page() { function Page() {
const state = useProxy(keamananLingkunganState) const state = useProxy(keamananLingkunganState)
const [expandedMap, setExpandedMap] = useState<Record<number, boolean>>({}); const router = useRouter()
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const { const {
@@ -26,13 +27,6 @@ function Page() {
load(page, 3, debouncedSearch) load(page, 3, debouncedSearch)
}, [page, debouncedSearch]) }, [page, debouncedSearch])
const toggleExpanded = (index: number, value: boolean) => {
setExpandedMap((prev) => ({
...prev,
[index]: value,
}));
};
if (loading || !data) { if (loading || !data) {
return ( return (
<Box py={10}> <Box py={10}>
@@ -65,60 +59,100 @@ function Page() {
</GridCol> </GridCol>
</Grid> </Grid>
<Text px={{ base: 'md', md: 100 }} ta={"justify"} fz="md" mt={4} > <Text px={{ base: 'md', md: 100 }} 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>
<Box px={{ base: "md", md: 100 }}> <Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'}> <Stack gap={'lg'}>
<SimpleGrid <SimpleGrid
pb={10} cols={{ base: 1, sm: 2, md: 3 }} spacing="xl" mt="lg">
cols={{ {data.map((v, k) => (
base: 1, <Paper
md: 3, key={k}
}}> radius="xl"
{data.map((v, k) => { shadow="md"
return ( withBorder
<Paper radius={10} key={k} bg={colors["white-trans-1"]}> p="lg"
<Stack gap={'xs'}> bg={colors['white-trans-1']}
<Center px={10} py={20}> style={{
<Image loading="lazy" src={v.image?.link} alt='' /> transition: 'all 200ms ease',
</Center> cursor: 'pointer',
<Box px={'lg'}> display: 'flex',
<Box pb={20}> flexDirection: 'column',
<Text pb={10} c={colors["blue-button"]} fw={"bold"} fz={"h3"}> justifyContent: 'space-between',
{v.name} height: '100%',
</Text> }}
<Spoiler >
showLabel={ <Stack align="center" gap="sm" style={{ flexGrow: 1 }}>
<Text fw="bold" fz="sm" c={colors['blue-button']}> <Box
Show more style={{
</Text> width: '100%',
} aspectRatio: '16/9',
hideLabel={ borderRadius: '12px',
<Text fw="bold" fz="sm" c={colors['blue-button']}> overflow: 'hidden',
Hide details position: 'relative',
</Text> }}
} >
expanded={expandedMap[k] || false} <Image
onExpandedChange={(val) => toggleExpanded(k, val)} src={v.image?.link}
> alt={v.name}
<Text pb={10} fz={"h4"} ta={'justify'} style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: v.deskripsi }} /> fit="cover"
</Spoiler> loading="lazy"
</Box> style={{
</Box> width: '100%',
</Stack> height: '100%',
</Paper> transition: 'transform 0.4s ease',
) }}
})} onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.05)')}
onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')}
/>
</Box>
<Text ta="center" fw={700} fz="lg" c={colors['blue-button']}>
{v.name}
</Text>
<Text
fz="sm"
ta="justify"
lineClamp={3}
lh={1.6}
style={{
minHeight: '4.8em',
}}
>
<span
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
/>
</Text>
</Stack>
<Center mt="md">
<Button
variant="light"
onClick={() => {
router.push(`/darmasaba/keamanan/keamanan-lingkungan-pecalang-patwal/${v.id}`)
}}
>
Detail
</Button>
</Center>
</Paper>
))}
</SimpleGrid> </SimpleGrid>
</Stack> </Stack>
</Box> </Box>
<Center>
<Center mt="xl">
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => load(newPage)} // ini penting! onChange={(newPage) => load(newPage, 3, search)}
total={totalPages} total={totalPages}
my="md" size="lg"
radius="xl"
styles={{
control: {
border: `1px solid ${colors['blue-button']}`,
},
}}
/> />
</Center> </Center>
</Stack> </Stack>

View File

@@ -45,7 +45,7 @@ function Page() {
<Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}> <Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
Kontak Darurat Kontak Darurat
</Text> </Text>
<Text fz={{ base: "h4", md: "h3" }} > <Text fz="md" >
Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung. Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung.
</Text> </Text>
</Box> </Box>

View File

@@ -1,27 +1,26 @@
'use client' 'use client'
import { import {
Box, Box,
Button,
Card, Card,
Center, Center,
Group, Group,
Pagination, Pagination,
Paper,
Skeleton, Skeleton,
Stack, Stack,
Text, Text,
Title, Title
Tooltip,
Button,
Paper,
} from '@mantine/core'; } from '@mantine/core';
import { IconSearch, IconArrowRight } from '@tabler/icons-react'; import { IconArrowRight, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import HeaderSearch from '@/app/admin/(dashboard)/_com/header';
import { useProxy } from 'valtio/utils';
import pencegahanKriminalitasState from '@/app/admin/(dashboard)/_state/keamanan/pencegahan-kriminalitas'; import pencegahanKriminalitasState from '@/app/admin/(dashboard)/_state/keamanan/pencegahan-kriminalitas';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { useState } from 'react';
import HeaderSearch from '@/app/admin/(dashboard)/_com/header';
import { IconArrowLeft } from '@tabler/icons-react'; import { IconArrowLeft } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function PencegahanKriminalitas() { function PencegahanKriminalitas() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
@@ -96,7 +95,6 @@ function ListPencegahanKriminalitas({ search }: { search: string }) {
style={{ wordBreak: "break-word", whiteSpace: "normal" }} style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/> />
<Group justify="flex-end" mt="sm"> <Group justify="flex-end" mt="sm">
<Tooltip label="Lihat detail program" withArrow>
<Button <Button
size="sm" size="sm"
variant="gradient" variant="gradient"
@@ -106,7 +104,6 @@ function ListPencegahanKriminalitas({ search }: { search: string }) {
> >
Lihat Detail Lihat Detail
</Button> </Button>
</Tooltip>
</Group> </Group>
</Stack> </Stack>
</Card> </Card>

View File

@@ -9,21 +9,16 @@ import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto'; import BackButton from '../../desa/layanan/_com/BackButto';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
function Page() { function Page() {
const state = useProxy(polsekTerdekatState.findFirst); const state = useProxy(polsekTerdekatState.findFirst);
const router = useRouter() const router = useRouter();
const { const { data, loading, load } = state;
data,
loading,
load,
} = state;
useEffect(() => { useEffect(() => {
load(); load();
}, []); }, []);
// kalau masih loading // Loading state
if (loading) { if (loading) {
return ( return (
<Stack py={10}> <Stack py={10}>
@@ -32,104 +27,175 @@ function Page() {
); );
} }
// kalau data kosong // Data kosong
if (!data) { if (!data) {
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 pb={10} px={{ base: 20, md: 100 }}> <Box pb={10} px={{ base: 20, md: 100 }}>
<Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}> <Text fz={{ base: 'h1', md: '2.5rem' }} c={colors['blue-button']} fw="bold">
Kantor Polisi Terdekat Kantor Polisi Terdekat
</Text> </Text>
<Text pb={15} fz={'md'} > <Text pb={15} fz="md">
Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung
</Text> </Text>
</Box> </Box>
<Center py="xl"> <Center py="xl">
<Text fz="lg" fw="bold" c="red"> <Text fz="lg" fw="bold" c="red">
Data Polsek tidak ada Data Polsek tidak ada
</Text> </Text>
</Center> </Center>
</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 pb={10} px={{ base: 20, md: 100 }}> <Box pb={10} px={{ base: 20, md: 100 }}>
<Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}> <Text fz={{ base: 'h1', md: '2.5rem' }} c={colors['blue-button']} fw="bold">
Kantor Polisi Terdekat Kantor Polisi Terdekat
</Text> </Text>
<Text pb={15} fz={'h4'} > <Text pb={15} fz="h4">
Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung
</Text> </Text>
</Box> </Box>
<Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'}> <Box px={{ base: 'md', md: 100 }}>
<Paper radius={10} bg={colors["white-trans-1"]} p={'xl'}> <Stack gap="lg">
<Stack gap={'xs'}> <Paper radius={10} bg={colors['white-trans-1']} p="xl">
<SimpleGrid <Stack gap="xs">
cols={{ <SimpleGrid cols={{ base: 1, md: 2 }}>
base: 1,
md: 2,
}}
>
{/* Content Sebelah Kiri */}
{loading ? ( {loading ? (
<Center><Skeleton h={400} /></Center> <Center>
) : data ? ( <Skeleton h={400} />
</Center>
) : (
<> <>
{/* === KIRI === */}
<Box> <Box>
<Text c={colors["blue-button"]} fw={'bold'} fz={'h2'}>{data.nama}</Text> <Text c={colors['blue-button']} fw="bold" fz="h2">
<Text c={colors["blue-button"]} fz={'sm'}>{data.jarakKeDesa}</Text> {data.nama}
<Flex py={10} gap={9} align={'center'}> </Text>
<IconPin size={25} color={colors["blue-button"]} /> <Text c={colors['blue-button']} fz="sm">
<Text c={colors["blue-button"]} fz={'lg'}>{data.alamat}</Text> {data.jarakKeDesa}
</Flex> </Text>
<Flex gap={9} align={'center'}>
<IconPhone size={25} color={colors["blue-button"]} /> {/* Alamat */}
<Text c={colors["blue-button"]} fz={'lg'}>{data.nomorTelepon}</Text> <Flex
</Flex> py={10}
<Flex py={10} gap={9} align={'center'}> gap={9}
<IconClock size={25} color={colors["blue-button"]} /> align="flex-start"
<Text c={colors["blue-button"]} fz={'lg'}>{data.jamOperasional}</Text> wrap="wrap"
</Flex> style={{ wordBreak: 'break-word' }}
<Box> >
<Text c={colors["blue-button"]} fw={'bold'} fz={'h2'}>Layanan Yang Tersedia :</Text> <Box w={25} mt={3}>
<SimpleGrid <IconPin size={22} />
py={10} </Box>
cols={{ <Text
base: 1, fz="lg"
md: 2, style={{
flex: 1,
wordBreak: 'break-word',
lineHeight: 1.4,
}} }}
> >
<Box> {data.alamat}
<Text c={colors["blue-button"]} fz={'lg'}>{data.layananPolsek.nama}</Text> </Text>
</Box> </Flex>
{/* Telepon */}
<Flex
gap={9}
align="flex-start"
wrap="wrap"
style={{ wordBreak: 'break-word' }}
>
<Box w={25} mt={3}>
<IconPhone size={22} />
</Box>
<Text fz="lg">
{data.nomorTelepon}
</Text>
</Flex>
{/* Jam Operasional */}
<Flex
py={10}
gap={9}
align="flex-start"
wrap="wrap"
style={{ wordBreak: 'break-word' }}
>
<Box w={25} mt={3}>
<IconClock size={22} />
</Box>
<Text fz="lg">
{data.jamOperasional}
</Text>
</Flex>
{/* Layanan */}
<Box>
<Text c={colors['blue-button']} fw="bold" fz="h2">
Layanan Yang Tersedia :
</Text>
<SimpleGrid py={10} cols={{ base: 1, md: 2 }}>
<Text fz="lg">
{data.layananPolsek.nama}
</Text>
</SimpleGrid> </SimpleGrid>
</Box> </Box>
<Box> <Box>
<Button bg={colors["blue-button"]} onClick={() => router.push(`/darmasaba/keamanan/polsek-terdekat/semua-polsek`)} rightSection={<IconArrowDown size={20} />}>Lihat Kantor Polisi Lainnya</Button> <Button
bg={colors['blue-button']}
onClick={() =>
router.push(`/darmasaba/keamanan/polsek-terdekat/semua-polsek`)
}
rightSection={<IconArrowDown size={20} />}
>
Lihat Kantor Polisi Lainnya
</Button>
</Box> </Box>
</Box> </Box>
<Box pos={'relative'}>
{/* === KANAN === */}
<Box pos="relative">
<Box style={{ position: 'absolute', top: 0, right: 0 }}> <Box style={{ position: 'absolute', top: 0, right: 0 }}>
<Badge size='lg' c={'#287407'} bg={'#A8EDC4'}>Buka</Badge> <Badge size="lg" c="#287407" bg="#A8EDC4">
Buka
</Badge>
</Box> </Box>
<Box pt={40}> <Box pt={40}>
<iframe style={{ border: 2, width: "100%" }} src={data.embedMapUrl} width="550" height="300" ></iframe> <iframe
style={{ border: 2, width: '100%' }}
src={data.embedMapUrl}
width="550"
height="300"
></iframe>
</Box> </Box>
<Box pt={20}> <Box pt={20}>
<Button onClick={() => router.push(data.linkPetunjukArah)} fullWidth bg={colors["blue-button"]} radius={10} leftSection={<IconNavigation size={20} />}>Petunjuk Arah</Button> <Button
onClick={() => router.push(data.linkPetunjukArah)}
fullWidth
bg={colors['blue-button']}
radius={10}
leftSection={<IconNavigation size={20} />}
>
Petunjuk Arah
</Button>
</Box> </Box>
</Box> </Box>
</> </>
) : null} )}
</SimpleGrid> </SimpleGrid>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -56,8 +56,11 @@ function Page() {
/> />
</GridCol> </GridCol>
</Grid> </Grid>
<Text px={{ base: 'md', md: 100 }} ta={"justify"} fz={{ base: "h4", md: "h3" }} > <Text px={{ base: 'md', md: 100 }} ta={"justify"} fz="md" >
Keamanan dan ketertiban lingkungan di Desa Darmasaba dijaga melalui peran aktif Pecalang dan Patwal (Patroli Pengawal). Mereka bertugas memastikan desa tetap aman, tertib, dan kondusif bagi seluruh warga. Keamanan dan ketertiban lingkungan di Desa Darmasaba dijaga melalui peran aktif Pecalang dan Patwal (Patroli Pengawal).
</Text>
<Text px={{ base: 'md', md: 100 }} ta={"justify"} fz="md" >
Mereka bertugas memastikan desa tetap aman, tertib, dan kondusif bagi seluruh warga.
</Text> </Text>
</Box> </Box>
<Box px={{ base: "md", md: 100 }}> <Box px={{ base: "md", md: 100 }}>
@@ -82,7 +85,7 @@ function Page() {
{v.judul} {v.judul}
</Text> </Text>
<Box> <Box>
<Text pb={10} fz={"md"} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: v.deskripsi }} /> <Text pb={10} fz={"md"} style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: v.deskripsi }} />
</Box> </Box>
</Box> </Box>
</Box> </Box>

View File

@@ -88,59 +88,83 @@ function Page() {
) : ( ) : (
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="xl" mt="lg"> <SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="xl" mt="lg">
{data.map((v, k) => ( {data.map((v, k) => (
<Paper <Paper
key={k} key={k}
radius="xl" radius="xl"
shadow="md" shadow="md"
withBorder withBorder
p="lg" p="lg"
bg={colors['white-trans-1']} bg={colors['white-trans-1']}
style={{ style={{
transition: 'all 200ms ease', transition: 'all 200ms ease',
cursor: 'pointer', cursor: 'pointer',
}} display: 'flex',
> flexDirection: 'column',
<Stack align="center" gap="sm"> justifyContent: 'space-between', // ✅ biar button selalu di bawah
<Box height: '100%', // ✅ bikin tinggi seragam
style={{ }}
width: '100%', >
aspectRatio: '16/9', <Stack align="center" gap="sm" style={{ flexGrow: 1 }}>
borderRadius: '12px', <Box
overflow: 'hidden', style={{
position: 'relative', width: '100%',
}} aspectRatio: '16/9',
> borderRadius: '12px',
<Image overflow: 'hidden',
src={v.image.link} position: 'relative',
alt={v.name} }}
fit="cover" >
loading="lazy" <Image
style={{ src={v.image.link}
width: '100%', alt={v.name}
height: '100%', fit="cover"
transition: 'transform 0.4s ease', loading="lazy"
}} style={{
onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.05)')} width: '100%',
onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')} height: '100%',
/> transition: 'transform 0.4s ease',
</Box> }}
onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.05)')}
<Text ta="center" fw={700} fz="lg" c={colors['blue-button']}> onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')}
{v.name} />
</Text> </Box>
<Text fz="sm" ta="center" lineClamp={3}>
<span style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: v.deskripsi }} /> <Text ta="center" fw={700} fz="lg" c={colors['blue-button']}>
</Text> {v.name}
<Button </Text>
variant="light"
leftSection={<IconBrandWhatsapp size={18} />} <Text
component="a" fz="sm"
href={`https://wa.me/${v.whatsapp.replace(/\D/g, '')}`} ta="center"
target="_blank" lineClamp={3}
aria-label="Hubungi WhatsApp" lh={1.6}
>WhatsApp</Button> style={{
</Stack> minHeight: '4.8em', // tinggi tetap 3 baris
</Paper> }}
>
<span
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
/>
</Text>
</Stack>
{/* ✅ Tombol selalu di bagian bawah card */}
<Center mt="md">
<Button
variant="light"
leftSection={<IconBrandWhatsapp size={18} />}
component="a"
href={`https://wa.me/${v.whatsapp.replace(/\D/g, '')}`}
target="_blank"
aria-label="Hubungi WhatsApp"
>
WhatsApp
</Button>
</Center>
</Paper>
))} ))}
</SimpleGrid> </SimpleGrid>
)} )}

View File

@@ -1,15 +1,14 @@
'use client'; 'use client';
import penangananDarurat from '@/app/admin/(dashboard)/_state/kesehatan/penanganan-darurat/penangananDarurat'; import penangananDarurat from '@/app/admin/(dashboard)/_state/kesehatan/penanganan-darurat/penangananDarurat';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { Box, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack } from '@tabler/icons-react'; import { useParams } from 'next/navigation';
import { useParams, useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import BackButton from '../../../desa/layanan/_com/BackButto';
function DetailPenangananDaruratUser() { function DetailPenangananDaruratUser() {
const state = useProxy(penangananDarurat); const state = useProxy(penangananDarurat);
const router = useRouter();
const params = useParams(); const params = useParams();
useShallowEffect(() => { useShallowEffect(() => {
@@ -32,14 +31,7 @@ function DetailPenangananDaruratUser() {
<Box py={20}> <Box py={20}>
{/* Tombol Back */} {/* Tombol Back */}
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
<Button <BackButton/>
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={22} color={colors['blue-button']} />}
mb={20}
>
Kembali
</Button>
</Box> </Box>
{/* Wrapper Detail */} {/* Wrapper Detail */}

View File

@@ -1,7 +1,7 @@
// Create a new component: components/EdukasiCard.tsx // Create a new component: components/EdukasiCard.tsx
'use client'; 'use client';
import { Box, Paper, Stack, Text, Tooltip } from '@mantine/core'; import { Box, Paper, Stack, Text } from '@mantine/core';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
interface EdukasiCardProps { interface EdukasiCardProps {
@@ -31,7 +31,6 @@ export function EdukasiCard({ icon, title, description, color = '#1e88e5' }: Edu
<Box> <Box>
<Stack align="center" gap="xs" mb="md"> <Stack align="center" gap="xs" mb="md">
<Box style={{ color }}>{icon}</Box> <Box style={{ color }}>{icon}</Box>
<Tooltip label={title} maw={250} multiline withArrow position="top">
<Text <Text
fz={{ base: 'h5', md: 'h4' }} fz={{ base: 'h5', md: 'h4' }}
fw={700} fw={700}
@@ -47,7 +46,6 @@ export function EdukasiCard({ icon, title, description, color = '#1e88e5' }: Edu
}} }}
dangerouslySetInnerHTML={{ __html: title }} dangerouslySetInnerHTML={{ __html: title }}
/> />
</Tooltip>
</Stack> </Stack>
<Text <Text
size="sm" size="sm"

View File

@@ -62,7 +62,6 @@ export default function EdukasiLingkunganPage() {
</Text> </Text>
<Text <Text
fz={{ base: 'md', md: 'lg' }} fz={{ base: 'md', md: 'lg' }}
c="dimmed"
maw={800} maw={800}
mx="auto" mx="auto"
> >
@@ -78,21 +77,21 @@ export default function EdukasiLingkunganPage() {
verticalSpacing={{ base: 'md', md: 'xl' }} verticalSpacing={{ base: 'md', md: 'xl' }}
> >
<EdukasiCard <EdukasiCard
icon={<IconLeaf size={32} />} icon={<IconLeaf size={45} />}
title={tujuan.data?.judul || ''} title={tujuan.data?.judul || ''}
description={tujuan.data?.deskripsi || ''} description={tujuan.data?.deskripsi || ''}
color={colors['blue-button']} color={colors['blue-button']}
/> />
<EdukasiCard <EdukasiCard
icon={<IconRecycle size={32} />} icon={<IconRecycle size={45} />}
title={materi.data?.judul || ''} title={materi.data?.judul || ''}
description={materi.data?.deskripsi || ''} description={materi.data?.deskripsi || ''}
color={colors['blue-button']} color={colors['blue-button']}
/> />
<EdukasiCard <EdukasiCard
icon={<IconPlant2 size={32} />} icon={<IconPlant2 size={45} />}
title={contoh.data?.judul || ''} title={contoh.data?.judul || ''}
description={contoh.data?.deskripsi || ''} description={contoh.data?.deskripsi || ''}
color={colors['blue-button']} color={colors['blue-button']}

View File

@@ -44,13 +44,12 @@ function Page() {
<Box style={{ display: 'flex', height: '100%' }}> <Box style={{ display: 'flex', height: '100%' }}>
<Paper <Paper
p="lg" p="lg"
bg="linear-gradient(145deg, #DFE3E8FF 0%, #EFF1F4FF 100%)"
style={{ style={{
borderRadius: 16, borderRadius: 16,
boxShadow: '0 0 20px rgba(28, 110, 164, 0.5)',
width: '100%', width: '100%',
display: 'flex', display: 'flex',
flexDirection: 'column' flexDirection: 'column',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)'
}} }}
> >
<Stack gap="md" px={20} style={{ height: '100%' }}> <Stack gap="md" px={20} style={{ height: '100%' }}>
@@ -74,13 +73,12 @@ function Page() {
<Box style={{ display: 'flex', height: '100%' }}> <Box style={{ display: 'flex', height: '100%' }}>
<Paper <Paper
p="lg" p="lg"
bg="linear-gradient(145deg, #DFE3E8FF 0%, #EFF1F4FF 100%)"
style={{ style={{
borderRadius: 16, borderRadius: 16,
boxShadow: '0 0 20px rgba(28, 110, 164, 0.5)',
width: '100%', width: '100%',
display: 'flex', display: 'flex',
flexDirection: 'column' flexDirection: 'column',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)'
}} }}
> >
<Stack gap="md" px={20} style={{ height: '100%' }}> <Stack gap="md" px={20} style={{ height: '100%' }}>
@@ -105,13 +103,12 @@ function Page() {
<Box> <Box>
<Paper <Paper
p="lg" p="lg"
bg="linear-gradient(145deg, #DFE3E8FF 0%, #EFF1F4FF 100%)"
style={{ style={{
borderRadius: 16, borderRadius: 16,
boxShadow: '0 0 20px rgba(28, 110, 164, 0.5)',
width: '100%', width: '100%',
display: 'flex', display: 'flex',
flexDirection: 'column' flexDirection: 'column',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)'
}} }}
> >
<Stack gap="md" px={20} style={{ height: '100%' }}> <Stack gap="md" px={20} style={{ height: '100%' }}>

View File

@@ -91,7 +91,7 @@ function Page() {
<Box style={{ alignContent: 'center', alignItems: 'center' }}> <Box style={{ alignContent: 'center', alignItems: 'center' }}>
{iconMap[v.icon] ? React.createElement(iconMap[v.icon]) : null} {iconMap[v.icon] ? React.createElement(iconMap[v.icon]) : null}
</Box> </Box>
<Text fw={'bold'} fz={{ base: "lg", md: "xl" }} c={'black'}>{v.name}</Text> <Text fz={{ base: "lg", md: "xl" }} c={'black'}>{v.name}</Text>
</Flex> </Flex>
</Paper> </Paper>
</Box> </Box>

View File

@@ -57,7 +57,7 @@ function Page() {
<Title order={1} fw={700} ta="center" c={colors['blue-button']}> <Title order={1} fw={700} ta="center" c={colors['blue-button']}>
Statistik Data Pendidikan Statistik Data Pendidikan
</Title> </Title>
<Text c="dimmed" size="sm" ta="center"> <Text fz="md" ta="center">
Visualisasi jumlah pendidikan berdasarkan kategori yang tersedia Visualisasi jumlah pendidikan berdasarkan kategori yang tersedia
</Text> </Text>
</Stack> </Stack>

View File

@@ -55,7 +55,7 @@ function Page({ params }: PageProps) {
<Group justify="space-between" align="center" mb="md"> <Group justify="space-between" align="center" mb="md">
<Group gap="sm"> <Group gap="sm">
<IconChalkboard size={28} stroke={1.5} color={colors['blue-button']} /> <IconChalkboard size={28} stroke={1.5} color={colors['blue-button']} />
<Title order={2} fz="xl">Daftar Lembaga Pendidikan</Title> <Title order={2} fz="xl" c={colors['blue-button']}>Daftar Lembaga Pendidikan</Title>
</Group> </Group>
<TextInput <TextInput
placeholder='pencarian' placeholder='pencarian'

View File

@@ -55,7 +55,7 @@ function Page({ params }: PageProps) {
<Group justify="space-between" align="center" mb="md"> <Group justify="space-between" align="center" mb="md">
<Group gap="sm"> <Group gap="sm">
<IconMicroscope size={28} stroke={1.5} color={colors['blue-button']} /> <IconMicroscope size={28} stroke={1.5} color={colors['blue-button']} />
<Title order={2} fz="xl">Daftar Pengajar</Title> <Title order={2} fz="xl" c={colors['blue-button']}>Daftar Pengajar</Title>
</Group> </Group>
<TextInput <TextInput
placeholder='pencarian' placeholder='pencarian'

View File

@@ -55,7 +55,7 @@ function Page({ params }: PageProps) {
<Group justify="space-between" align="center" mb="md"> <Group justify="space-between" align="center" mb="md">
<Group gap="sm"> <Group gap="sm">
<IconSchool size={28} stroke={1.5} color={colors['blue-button']} /> <IconSchool size={28} stroke={1.5} color={colors['blue-button']} />
<Title order={2} fz="xl">Daftar Siswa</Title> <Title order={2} fz="xl" c={colors['blue-button']}>Daftar Siswa</Title>
</Group> </Group>
<TextInput <TextInput
placeholder='pencarian' placeholder='pencarian'

View File

@@ -96,23 +96,21 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
// pastikan path benar // pastikan path benar
import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud';
import { import {
ActionIcon,
Box, Box,
Button, Button,
Container, Container,
Group, Group,
Loader,
Paper, Paper,
Stack, Stack,
Text, Text
VisuallyHidden,
Loader,
} from '@mantine/core'; } from '@mantine/core';
import { IconArrowLeft } from '@tabler/icons-react';
import { useRouter, useSearchParams } from 'next/navigation'; import { useRouter, useSearchParams } from 'next/navigation';
import { useSnapshot } from 'valtio';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud'; import { useSnapshot } from 'valtio';
import BackButton from '../../../desa/layanan/_com/BackButto';
type LayoutSekolahProps = { type LayoutSekolahProps = {
title?: string; title?: string;
@@ -153,10 +151,7 @@ export default function LayoutSekolah({
<Container size="xl" py={{ base: 'md', md: 'xl' }}> <Container size="xl" py={{ base: 'md', md: 'xl' }}>
<Stack gap="lg"> <Stack gap="lg">
{/* Back Button */} {/* Back Button */}
<ActionIcon onClick={() => window.history.back()} variant="light" radius="md" size="lg"> <BackButton/>
<IconArrowLeft size={20} />
<VisuallyHidden>Kembali</VisuallyHidden>
</ActionIcon>
{/* Search & Filter */} {/* Search & Filter */}
<Paper radius="lg" p="xl" withBorder> <Paper radius="lg" p="xl" withBorder>
@@ -185,8 +180,8 @@ export default function LayoutSekolah({
radius="xl" radius="xl"
size="sm" size="sm"
variant={aktif ? 'filled' : 'light'} variant={aktif ? 'filled' : 'light'}
bg={colors['blue-button']} bg={aktif? colors['blue-button'] : '#BDCADE'}
c={aktif ? colors['white-1'] : 'gray'} c={aktif ? colors['white-1'] : colors['blue-button']}
> >
{k} {k}
</Button> </Button>

View File

@@ -47,7 +47,7 @@ function Page() {
<Group justify="space-between" align="center" mb="md"> <Group justify="space-between" align="center" mb="md">
<Group gap="sm"> <Group gap="sm">
<IconChalkboard size={28} stroke={1.5} color={colors['blue-button']} /> <IconChalkboard size={28} stroke={1.5} color={colors['blue-button']} />
<Title order={2} fz="xl">Daftar Lembaga Pendidikan</Title> <Title order={2} fz="xl" c={colors['blue-button']}>Daftar Lembaga Pendidikan</Title>
</Group> </Group>
<TextInput <TextInput
placeholder='pencarian' placeholder='pencarian'

View File

@@ -46,7 +46,7 @@ function Page() {
<Group justify="space-between" align="center" mb="md"> <Group justify="space-between" align="center" mb="md">
<Group gap="sm"> <Group gap="sm">
<IconMicroscope size={28} stroke={1.5} color={colors['blue-button']} /> <IconMicroscope size={28} stroke={1.5} color={colors['blue-button']} />
<Title order={2} fz="xl">Daftar Pengajar</Title> <Title order={2} fz="xl" c={colors['blue-button']}>Daftar Pengajar</Title>
</Group> </Group>
<TextInput <TextInput
placeholder='pencarian' placeholder='pencarian'

View File

@@ -47,7 +47,7 @@ function Page() {
<Group justify="space-between" align="center" mb="md"> <Group justify="space-between" align="center" mb="md">
<Group gap="sm"> <Group gap="sm">
<IconSchool size={28} stroke={1.5} color={colors['blue-button']} /> <IconSchool size={28} stroke={1.5} color={colors['blue-button']} />
<Title order={2} fz="xl">Daftar Siswa</Title> <Title order={2} fz="xl" c={colors['blue-button']}>Daftar Siswa</Title>
</Group> </Group>
<TextInput <TextInput
placeholder='pencarian' placeholder='pencarian'

View File

@@ -37,13 +37,15 @@ function Page() {
<Box px={{ base: 'md', md: 100 }} pb={50}> <Box px={{ base: 'md', md: 100 }} pb={50}>
<Box mb="xl"> <Box mb="xl">
<Title ta="center" order={1} fw="bold" c={colors['blue-button']} mb="sm"> <Title ta="center" order={1} fw="bold" c={colors['blue-button']}>
Program Pendidikan Anak Program Pendidikan Anak
</Title> </Title>
<Box my={"sm"}>
<Divider size="sm" color={colors['blue-button']} mx="auto" maw={550} />
</Box>
<Text ta="center" fz="lg" c="black" mb="lg" maw={800} mx="auto"> <Text ta="center" fz="lg" c="black" mb="lg" maw={800} mx="auto">
Desa Darmasaba berkomitmen mencetak generasi muda yang cerdas, berkarakter, dan siap bersaing melalui program pendidikan yang inklusif dan berkelanjutan. Desa Darmasaba berkomitmen mencetak generasi muda yang cerdas, berkarakter, dan siap bersaing melalui program pendidikan yang inklusif dan berkelanjutan.
</Text> </Text>
<Divider size="sm" color={colors['blue-button']} mx="auto" maw={120} />
</Box> </Box>
<SimpleGrid <SimpleGrid
@@ -66,7 +68,7 @@ function Page() {
</Title> </Title>
</Group> </Group>
<Tooltip label="Detail tujuan program pendidikan anak" position="top-start" withArrow> <Tooltip label="Detail tujuan program pendidikan anak" position="top-start" withArrow>
<Text fz="lg" lh={1.6} c="dark" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: stateTujuan.findById.data?.deskripsi }} /> <Text fz="lg" lh={1.6} c="dark" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: stateTujuan.findById.data?.deskripsi }} />
</Tooltip> </Tooltip>
</Stack> </Stack>
</Paper> </Paper>
@@ -87,7 +89,7 @@ function Page() {
</Title> </Title>
</Group> </Group>
<Tooltip label="Detail program unggulan yang sedang berjalan" position="top-start" withArrow> <Tooltip label="Detail program unggulan yang sedang berjalan" position="top-start" withArrow>
<Text fz="lg" lh={1.6} c="dark" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: stateUnggulan.findById.data?.deskripsi }} /> <Text fz="lg" lh={1.6} c="dark" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: stateUnggulan.findById.data?.deskripsi }} />
</Tooltip> </Tooltip>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -7,6 +7,7 @@ import {
Box, Box,
Button, Button,
Center, Center,
Group,
Image, Image,
Pagination, Pagination,
Paper, Paper,
@@ -68,7 +69,7 @@ function Page() {
<Image src="/darmasaba-icon.png" w={{ base: 70, md: 100 }} alt="Logo Desa Darmasaba" loading="lazy" /> <Image src="/darmasaba-icon.png" w={{ base: 70, md: 100 }} alt="Logo Desa Darmasaba" loading="lazy" />
</Center> </Center>
<Text ta="center" fz={{ base: "1.8rem", md: "2.5rem" }} c={colors["blue-button"]} fw="bold" lh={1.4}> <Text ta="center" fz={{ base: "1.8rem", md: "2.5rem" }} c={colors["blue-button"]} fw="bold" lh={1.4}>
Daftar Informasi Publik Desa Darmasaba Daftar Informasi Publik
</Text> </Text>
<Box px={{ base: "md", md: 100 }}> <Box px={{ base: "md", md: 100 }}>
<Stack gap="lg"> <Stack gap="lg">
@@ -178,12 +179,18 @@ function Page() {
<Paper p="lg" radius="xl" shadow="xs" withBorder> <Paper p="lg" radius="xl" shadow="xs" withBorder>
<Stack gap="xs"> <Stack gap="xs">
<Text fz="lg" fw="bold" c={colors["blue-button"]}>Kontak PPID</Text> <Text fz="lg" fw="bold" c={colors["blue-button"]}>Kontak PPID</Text>
<Text fz="sm" c="dimmed" lh={1.6}> <Group>
<IconMail size={16} style={{ marginRight: 6 }} /> Email: <Text span fw="500">ppid@desadarmasaba.id</Text> <IconMail color='gray' size={16} style={{ marginRight: 6 }} />
</Text> <Text c={"dimmed"} fz="sm" lh={1.6}>
<Text fz="sm" c="dimmed" lh={1.6}> Email: <Text c={"dimmed"} span fw="500">ppid@desadarmasaba.id</Text>
<IconBrandWhatsapp size={16} style={{ marginRight: 6 }} /> WhatsApp: <Text span fw="500">081-xxx-xxx-xxx</Text> </Text>
</Text> </Group>
<Group>
<IconBrandWhatsapp color='gray' size={16} style={{ marginRight: 6 }} />
<Text c={"dimmed"} fz="sm" lh={1.6}>
WhatsApp: <Text c={"dimmed"} span fw="500">081-xxx-xxx-xxx</Text>
</Text>
</Group>
</Stack> </Stack>
</Paper> </Paper>
</Stack> </Stack>

View File

@@ -239,7 +239,7 @@ const state = useProxy(indeksKepuasanState.responden);
{/* Chart Rating */} {/* Chart Rating */}
<Paper bg={colors['white-1']} p="md" radius="md"> <Paper bg={colors['white-1']} p="md" radius="md">
<Stack> <Stack>
<Title order={4}>Pilihan</Title> <Title order={4}>Ulasan</Title>
{donutDataRating.every(item => item.value === 0) ? ( {donutDataRating.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md"> <Text c="dimmed" ta="center" my="md">
Belum ada data untuk ditampilkan dalam grafik Belum ada data untuk ditampilkan dalam grafik
@@ -505,7 +505,7 @@ const state = useProxy(indeksKepuasanState.responden);
{/* Chart Rating */} {/* Chart Rating */}
<Paper bg={colors['white-1']} p="md" radius="md"> <Paper bg={colors['white-1']} p="md" radius="md">
<Stack> <Stack>
<Title order={4}>Pilihan</Title> <Title order={4}>Ulasan</Title>
{donutDataRating.every(item => item.value === 0) ? ( {donutDataRating.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md"> <Text c="dimmed" ta="center" my="md">
Belum ada data untuk ditampilkan dalam grafik Belum ada data untuk ditampilkan dalam grafik

View File

@@ -39,9 +39,9 @@ function DetailPegawaiUser() {
const data = statePegawai.findUnique.data; const data = statePegawai.findUnique.data;
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="xl"> <Box px={{ base: 'md', md: 100 }} py="xl">
{/* Back button */} {/* Back button */}
<Group mb="lg"> <Group mb="lg" px={{ base: 'md', md: 100 }}>
<Box <Box
onClick={() => router.back()} onClick={() => router.back()}
style={{ style={{

View File

@@ -1,7 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
'use client' 'use client'
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID' import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID'
import ScrollToTopButton from '@/app/darmasaba/_com/scrollToTopButton' import ScrollToTopButton from '@/app/darmasaba/_com/scrollToTopButton'
import colors from '@/con/colors' import colors from '@/con/colors'
@@ -10,7 +9,6 @@ import {
Button, Button,
Card, Card,
Center, Center,
Container,
Group, Group,
Image, Image,
Loader, Loader,
@@ -36,6 +34,7 @@ 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 './struktur.css'
export default function Page() { export default function Page() {
return ( return (
@@ -47,10 +46,9 @@ export default function Page() {
paddingBottom: 48, paddingBottom: 48,
}} }}
> >
<Container size="xl" py="xl"> <Box px={{ base: 'md', md: 100 }} py={"xl"}>
<Box px={{ base: 'md', md: 100 }}> <BackButton />
<BackButton />
</Box>
<Stack align="center" gap="xl" mt="xl"> <Stack align="center" gap="xl" mt="xl">
<Title <Title
order={1} order={1}
@@ -65,12 +63,12 @@ export default function Page() {
untuk melihat detail atau klik node untuk fokus tampilan. untuk melihat detail atau klik node untuk fokus tampilan.
</Text> </Text>
</Stack> </Stack>
<Box mt="lg"> <Box mt="lg">
<StrukturOrganisasiPPID /> <StrukturOrganisasiPPID />
</Box> </Box>
</Container> </Box>
{/* Tombol Scroll ke Atas */}
<ScrollToTopButton /> <ScrollToTopButton />
</Box> </Box>
) )
@@ -84,7 +82,7 @@ function StrukturOrganisasiPPID() {
const [isFullscreen, setFullscreen] = useState(false) const [isFullscreen, setFullscreen] = useState(false)
const [searchQuery, setSearchQuery] = useState('') const [searchQuery, setSearchQuery] = useState('')
// debounce untuk pencarian // debounce pencarian
const debouncedSearch = useRef( const debouncedSearch = useRef(
debounce((value: string) => { debounce((value: string) => {
setSearchQuery(value) setSearchQuery(value)
@@ -112,7 +110,8 @@ function StrukturOrganisasiPPID() {
) )
} }
if (!stateOrganisasi.findMany.data || stateOrganisasi.findMany.data.length === 0) { const data = stateOrganisasi.findMany.data || []
if (data.length === 0) {
return ( return (
<Center py={40}> <Center py={40}>
<Stack align="center" gap="md"> <Stack align="center" gap="md">
@@ -151,9 +150,9 @@ function StrukturOrganisasiPPID() {
) )
} }
// Buat struktur organisasi // 🧩 buat struktur organisasi
const posisiMap = new Map<string, any>() const posisiMap = new Map<string, any>()
const aktifPegawai = stateOrganisasi.findMany.data.filter((p: any) => p.isActive) const aktifPegawai = data.filter((p: any) => p.isActive)
for (const pegawai of aktifPegawai) { for (const pegawai of aktifPegawai) {
const posisiId = pegawai.posisi.id const posisiId = pegawai.posisi.id
@@ -176,19 +175,15 @@ function StrukturOrganisasiPPID() {
} else root.push(posisi) } else root.push(posisi)
}) })
function toOrgChartFormat(node: any): any { const toOrgChartFormat = (node: any): any => {
const pegawai = node.pegawaiList?.[0] const pegawai = node.pegawaiList?.[0]
return { return {
expanded: true, expanded: true,
type: 'person',
styleClass: 'p-person',
data: { data: {
id: pegawai?.id || null, id: pegawai?.id,
name: pegawai?.namaLengkap || 'Belum ditugaskan', name: pegawai?.namaLengkap || 'Belum Ditugaskan',
title: node.nama || 'Tanpa jabatan', title: node.nama || 'Tanpa Jabatan',
image: pegawai?.image?.link || '/img/default.png', image: pegawai?.image?.link || '/img/default.png',
description: node.deskripsi || '',
positionId: node.id || null,
}, },
children: node.children?.map(toOrgChartFormat) || [], children: node.children?.map(toOrgChartFormat) || [],
} }
@@ -213,7 +208,7 @@ function StrukturOrganisasiPPID() {
chartData = filterNodes(chartData) chartData = filterNodes(chartData)
} }
// 🧭 fungsi fullscreen // 🎬 fullscreen & zoom control
const toggleFullscreen = () => { const toggleFullscreen = () => {
if (!document.fullscreenElement) { if (!document.fullscreenElement) {
chartContainerRef.current?.requestFullscreen() chartContainerRef.current?.requestFullscreen()
@@ -224,138 +219,249 @@ function StrukturOrganisasiPPID() {
} }
} }
// 🧭 fungsi zoom const handleZoomIn = () => setScale((s) => Math.min(s + 0.1, 2))
const handleZoomIn = () => setScale((prev) => Math.min(prev + 0.1, 2)) const handleZoomOut = () => setScale((s) => Math.max(s - 0.1, 0.5))
const handleZoomOut = () => setScale((prev) => Math.max(prev - 0.1, 0.5))
const resetZoom = () => setScale(1) const resetZoom = () => setScale(1)
return ( return (
<Stack align="center" mt="xl"> <Stack align="center" mt="xl">
{/* 🔍 Search + Zoom + Fullscreen controls */} {/* 🔍 Controls */}
<Group mb="md" justify="center" gap="sm" align="center"> <Paper
<TextInput shadow="xs"
placeholder="Cari nama atau jabatan..." p="md"
leftSection={<IconSearch size={16} />} radius="md"
onChange={(e) => debouncedSearch(e.target.value)}
/>
<Button variant="light" size="sm" onClick={handleZoomOut}>
<IconZoomOut size={16} />
</Button>
{/* 🔍 Tambahkan indikator zoom di sini */}
{/* Floating Zoom Indicator */}
<Box
bg="#C3D0E8"
c="blue"
px={9}
py={8}
style={{
fontSize: 14,
fontWeight: 600,
borderRadius: '5px',
}}
>
{Math.round(scale * 100)}%
</Box>
<Button variant="light" size="sm" onClick={handleZoomIn}>
<IconZoomIn size={16} />
</Button>
<Button variant="light" size="sm" onClick={resetZoom}>
Reset
</Button>
<Button
variant="light"
size="sm"
onClick={toggleFullscreen}
leftSection={
isFullscreen ? <IconArrowsMinimize size={16} /> : <IconArrowsMaximize size={16} />
}
>
{isFullscreen ? 'Keluar' : 'Fullscreen'}
</Button>
</Group>
{/* Chart Container */}
<Box
ref={chartContainerRef}
style={{ style={{
overflow: 'auto', background: colors['blue-button']
transform: `scale(${scale})`,
transformOrigin: 'center top',
transition: 'transform 0.25s ease',
}} }}
> >
<OrganizationChart <Group gap="sm" wrap="wrap" justify="center">
value={chartData} <TextInput
nodeTemplate={(node) => nodeTemplate(node, router)} placeholder="Cari nama atau jabatan..."
/> leftSection={<IconSearch size={16} />}
</Box> onChange={(e) => debouncedSearch(e.target.value)}
styles={{
input: {
minWidth: 250,
},
}}
/>
<Group gap="xs">
<Button
variant="light"
bg={colors['blue-button-2']}
size="sm"
onClick={handleZoomOut}
leftSection={<IconZoomOut size={16} />}
c={colors['blue-button']}
>
Zoom Out
</Button>
<Box
bg={colors['blue-button-2']}
c={colors['blue-button']}
px={16}
py={8}
style={{
fontSize: 14,
fontWeight: 700,
borderRadius: '8px',
minWidth: 70,
textAlign: 'center',
}}
>
{Math.round(scale * 100)}%
</Box>
<Button
bg={colors['blue-button-2']}
c={colors['blue-button']}
variant="light"
size="sm"
onClick={handleZoomIn}
leftSection={<IconZoomIn size={16} />}
>
Zoom In
</Button>
<Button
bg={colors['blue-button-2']}
c={colors['blue-button']}
variant="light"
size="sm"
onClick={resetZoom}
>
Reset
</Button>
<Button
bg={colors['blue-button-2']}
c={colors['blue-button']}
size="sm"
onClick={toggleFullscreen}
leftSection={
isFullscreen ? (
<IconArrowsMinimize size={16} />
) : (
<IconArrowsMaximize size={16} />
)
}
>
Fullscreen
</Button>
</Group>
</Group>
</Paper>
{/* 🧩 Chart Container */}
<Center style={{ width: '100%' }}>
<Box
ref={chartContainerRef}
style={{
overflowX: 'auto',
overflowY: 'auto',
width: '100%',
maxWidth: '100%',
padding: '32px 16px',
transition: 'transform 0.2s ease',
transform: `scale(${scale})`,
transformOrigin: 'center top',
}}
>
<OrganizationChart
value={chartData}
nodeTemplate={(node) => <NodeCard node={node} router={router} />}
className="p-organizationchart p-organizationchart-horizontal"
/>
</Box>
</Center>
</Stack> </Stack>
) )
} }
function nodeTemplate(node: any, router: ReturnType<typeof useTransitionRouter>) { function NodeCard({ node, router }: any) {
const imageSrc = node?.data?.image || '/img/default.png' const imageSrc = node?.data?.image || '/img/default.png'
const name = node?.data?.name || 'Tanpa Nama' const name = node?.data?.name || 'Tanpa Nama'
const title = node?.data?.title || 'Tanpa Jabatan' const title = node?.data?.title || 'Tanpa Jabatan'
const description = node?.data?.description || '' const hasId = Boolean(node?.data?.id)
return ( return (
<Transition mounted transition="pop" duration={240}> <Transition mounted transition="pop" duration={300}>
{(styles) => ( {(styles) => (
<Card <Card
radius="lg" shadow="md"
radius="xl"
withBorder withBorder
style={{ style={{
...styles, ...styles,
width: 260, width: 240,
padding: 16, minHeight: 280,
background: 'rgba(28,110,164,0.3)', padding: 20,
borderColor: 'rgba(255,255,255,0.15)', background: 'linear-gradient(135deg, rgba(28,110,164,0.15) 0%, rgba(255,255,255,0.95) 100%)',
display: 'flex', borderColor: 'rgba(28, 110, 164, 0.3)',
flexDirection: 'column', borderWidth: 2,
alignItems: 'center', transition: 'all 0.3s ease',
textAlign: 'center', 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 = ''
}
}} }}
> >
<Image <Stack align="center" gap={12}>
src={imageSrc} {/* Photo */}
alt={name} <Box
radius="md" style={{
width={60} width: 96,
height={60} height: 96,
fit="cover" borderRadius: '50%',
style={{ overflow: 'hidden',
objectFit: 'cover', border: '3px solid rgba(28, 110, 164, 0.4)',
border: '2px solid rgba(255,255,255,0.2)', boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
marginBottom: 12, background: 'white',
}}
loading="lazy"
/>
<Text fw={700}>{name}</Text>
<Text size="sm" c="dimmed" mt={4}>
{title}
</Text>
<Text size="xs" c="dimmed" mt={8} lineClamp={3}>
{description || 'Belum ada deskripsi.'}
</Text>
<Button
variant="light"
size="xs"
mt="md"
onClick={() => {
const id = node?.data?.id
router.push(`/darmasaba/ppid/struktur-ppid/${id}`)
}} }}
> >
Lihat Detail <Image
</Button> src={imageSrc}
alt={name}
width={96}
height={96}
fit="cover"
loading="lazy"
style={{
objectFit: 'cover',
}}
/>
</Box>
{/* Name */}
<Text
fw={700}
size="sm"
ta="center"
c={colors['blue-button']}
lineClamp={2}
style={{
minHeight: 40,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
wordBreak: 'break-word',
lineHeight: 1.3,
}}
>
{name}
</Text>
{/* Title/Position */}
<Text
size="xs"
c="dimmed"
ta="center"
fw={500}
lineClamp={2}
style={{
minHeight: 32,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
wordBreak: 'break-word',
lineHeight: 1.2,
}}
>
{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,
}}
>
Lihat Detail
</Button>
)}
</Stack>
</Card> </Card>
)} )}
</Transition> </Transition>

View File

@@ -0,0 +1,68 @@
/* ============================================
STRUKTUR ORGANISASI PPID - STYLING
============================================ */
/* Tabel chart selalu center */
.p-organizationchart-table {
margin: 0 auto !important;
}
/* Jarak vertikal antar level - lebih lega */
.p-organizationchart-line-down {
height: 32px !important;
}
/* Padding di dalam node - lebih rapi */
.p-organizationchart-node-content {
padding: 0 !important;
background: transparent !important;
border: none !important;
}
/* Garis connector antar node - lebih tebal dan jelas */
.p-organizationchart-line-down,
.p-organizationchart-line-left,
.p-organizationchart-line-right,
.p-organizationchart-line-top {
border-color: rgba(28, 110, 164, 0.4) !important;
border-width: 2px !important;
}
/* Garis horizontal */
.p-organizationchart-line-left,
.p-organizationchart-line-right {
border-top-width: 2px !important;
}
/* Jarak horizontal antar node - lebih proporsional */
.p-organizationchart-table > tbody > tr > td {
padding: 0 24px !important;
vertical-align: top !important;
}
/* Node container spacing */
.p-organizationchart-node {
padding: 8px !important;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.p-organizationchart-table > tbody > tr > td {
padding: 0 12px !important;
}
.p-organizationchart-line-down {
height: 24px !important;
}
}
/* Smooth transitions untuk zoom */
.p-organizationchart {
transition: transform 0.2s ease;
}
/* Fullscreen mode adjustments */
.p-organizationchart-table:fullscreen {
background: rgba(230, 240, 255, 0.98);
padding: 40px;
}

View File

@@ -63,7 +63,7 @@ function Page() {
<SimpleGrid px={{ base: 'md', md: 100 }} cols={{ base: 1, sm: 2, md: 3 }} spacing="xl"> <SimpleGrid px={{ base: 'md', md: 100 }} cols={{ base: 1, sm: 2, md: 3 }} spacing="xl">
{data.map((v: any, k: number) => ( {data.map((v: any, k: number) => (
<BackgroundImage key={k} src={v.image?.link || ''} h={360} radius="xl" pos="relative"> <BackgroundImage key={k} src={v.image?.link || ''} h={360} radius="xl" pos="relative">
<Box pos="absolute" inset={0} bg="rgba(0,0,0,0.45)" style={{ borderRadius: 16 }} /> <Box pos="absolute" inset={0} bg="rgba(0,0,0,0.45)" style={{ borderRadius: 27 }} />
<Stack justify="space-between" h="100%" p="lg" pos="relative"> <Stack justify="space-between" h="100%" p="lg" pos="relative">
<Box> <Box>
<Text fz="lg" fw={600} c="white" ta="center"> <Text fz="lg" fw={600} c="white" ta="center">

View File

@@ -1,10 +1,10 @@
'use client' 'use client'
import { Box, Center, Container, Image, LoadingOverlay, Paper, SimpleGrid, Stack, Text, Title, Tooltip } from '@mantine/core';
import { Prisma } from '@prisma/client';
import { useEffect, useState } from 'react';
import { IconMoodSad } from '@tabler/icons-react';
import BackButton from '../../(pages)/desa/layanan/_com/BackButto';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Center, Container, Image, LoadingOverlay, Paper, SimpleGrid, Stack, Text, Title } from '@mantine/core';
import { Prisma } from '@prisma/client';
import { IconMoodSad } from '@tabler/icons-react';
import { useEffect, useState } from 'react';
import BackButton from '../../(pages)/desa/layanan/_com/BackButto';
function Page() { function Page() {
const [sdgsDesa, setSdgsDesa] = useState<Prisma.SdgsDesaGetPayload<{ include: { image: true } }>[]>([]); const [sdgsDesa, setSdgsDesa] = useState<Prisma.SdgsDesaGetPayload<{ include: { image: true } }>[]>([]);
@@ -114,11 +114,9 @@ function Page() {
/> />
</Box> </Box>
<Stack gap="xs" align="center" style={{ width: '100%' }}> <Stack gap="xs" align="center" style={{ width: '100%' }}>
<Tooltip label={item.name} position="top" withArrow>
<Title order={4} ta="center" c="dark" fw={600} lineClamp={2} style={{ minHeight: '3rem' }}> <Title order={4} ta="center" c="dark" fw={600} lineClamp={2} style={{ minHeight: '3rem' }}>
{item.name} {item.name}
</Title> </Title>
</Tooltip>
<Text <Text
ta="center" ta="center"
fw={700} fw={700}

View File

@@ -116,7 +116,7 @@ function NavbarMobile({ listNavbar }: { listNavbar: MenuItem[] }) {
radius="md" radius="md"
p="sm" p="sm"
withBorder withBorder
bg={active ? "blue.0" : "gray.0"} bg={active ? colors["blue-button-2"] : "gray.0"}
onClick={() => { onClick={() => {
if (item.href) { if (item.href) {
router.push(item.href); router.push(item.href);
@@ -126,21 +126,21 @@ function NavbarMobile({ listNavbar }: { listNavbar: MenuItem[] }) {
style={{ style={{
cursor: item.href ? "pointer" : "default", cursor: item.href ? "pointer" : "default",
transition: "background 0.15s ease", transition: "background 0.15s ease",
borderLeft: active ? "4px solid #1e66f5" : "4px solid transparent", borderLeft: active ? `4px solid ${colors['blue-button']}` : "4px solid transparent",
}} }}
> >
<Group justify="space-between" align="center" wrap="nowrap"> <Group justify="space-between" align="center" wrap="nowrap">
<Text <Text
fw={active ? 700 : 600} fw={active ? 700 : 600}
fz="md" fz="md"
c={active ? "blue.7" : "dark.9"} c={active ? colors['blue-button'] : "dark.9"}
> >
{item.name} {item.name}
</Text> </Text>
{item.href && ( {item.href && (
<IconSquareArrowRight <IconSquareArrowRight
size={18} size={18}
color={active ? "#1e66f5" : "inherit"} color={active ? colors['blue-button'] : "inherit"}
/> />
)} )}
</Group> </Group>
@@ -167,21 +167,21 @@ function NavbarMobile({ listNavbar }: { listNavbar: MenuItem[] }) {
cursor: child.href ? "pointer" : "default", cursor: child.href ? "pointer" : "default",
opacity: child.href ? 1 : 0.8, opacity: child.href ? 1 : 0.8,
borderRadius: "0.5rem", borderRadius: "0.5rem",
backgroundColor: childActive ? "#e7f0ff" : "transparent", backgroundColor: childActive ? colors["blue-button-2"] : "transparent",
borderLeft: childActive ? "3px solid #1e66f5" : "3px solid transparent", borderLeft: childActive ? `3px solid ${colors['blue-button']}` : "3px solid transparent",
transition: "background 0.15s ease", transition: "background 0.15s ease",
}} }}
> >
<Text <Text
fz="sm" fz="sm"
fw={childActive ? 600 : 400} fw={childActive ? 600 : 400}
c={childActive ? "blue.7" : "dark.8"} c={childActive ? colors['blue-button'] : "dark.8"}
> >
{child.name} {child.name}
</Text> </Text>
<IconSquareArrowRight <IconSquareArrowRight
size={14} size={14}
color={childActive ? "#1e66f5" : "inherit"} color={childActive ? colors['blue-button'] : "inherit"}
/> />
</Group> </Group>
); );

View File

@@ -112,7 +112,7 @@ function MenuItemCom({ item, isActive = false }: { item: MenuItem, isActive?: bo
<MenuTarget> <MenuTarget>
<Button <Button
variant="subtle" variant="subtle"
color={isActive ? 'blue' : 'gray'} color={isActive ? colors['blue-button'] : 'gray'}
onClick={() => { onClick={() => {
if (item.href) { if (item.href) {
router.push(item.href); router.push(item.href);

View File

@@ -49,12 +49,12 @@ export function NavbarSubMenu({ item }: { item: MenuItem[] | null }) {
rightSection={<IconArrowRight size={18} />} rightSection={<IconArrowRight size={18} />}
styles={(theme) => ({ styles={(theme) => ({
root: { root: {
background: link.href && pathname.startsWith(link.href) ? theme.colors.blue[0] : 'transparent', background: link.href && pathname.startsWith(link.href) ? colors['blue-button-2'] : 'transparent',
color: link.href && pathname.startsWith(link.href) ? theme.colors.blue[7] : colors['blue-button'], color: link.href && pathname.startsWith(link.href) ? colors['blue-button'] : 'gray',
fontWeight: link.href && pathname.startsWith(link.href) ? 600 : 500, fontWeight: link.href && pathname.startsWith(link.href) ? 600 : 500,
transition: "all 0.2s ease", transition: "all 0.2s ease",
"&:hover": { "&:hover": {
background: link.href && pathname.startsWith(link.href) ? theme.colors.blue[1] : theme.colors.gray[0], background: link.href && pathname.startsWith(link.href) ? colors['blue-button-2'] : theme.colors.gray[0],
} }
}, },
})} })}

View File

@@ -35,7 +35,7 @@ function Apbdes() {
return ( return (
<Stack p="sm" gap="xl" bg={colors.Bg}> <Stack p="sm" gap="xl" bg={colors.Bg}>
<Box> <Box mt={"xl"}>
<Stack gap="sm"> <Stack gap="sm">
<Text c={colors["blue-button"]} ta={"center"} fw={"bold"} fz={{ base: "1.8rem", md: "3.4rem" }}> <Text c={colors["blue-button"]} ta={"center"} fw={"bold"} fz={{ base: "1.8rem", md: "3.4rem" }}>
{textHeading.title} {textHeading.title}
@@ -72,12 +72,7 @@ function Apbdes() {
pos="relative" pos="relative"
style={{ overflow: 'hidden' }} style={{ overflow: 'hidden' }}
> >
<Box <Box pos="absolute" inset={0} bg="rgba(0,0,0,0.45)" style={{ borderRadius: 16 }} />
pos="absolute"
inset={0}
bg="rgba(0,0,0,0.55)"
style={{ backdropFilter: 'blur(4px)' }}
/>
<Stack justify="space-between" h="100%" p="xl" pos="relative"> <Stack justify="space-between" h="100%" p="xl" pos="relative">
<Text <Text
c="white" c="white"
@@ -117,7 +112,7 @@ function Apbdes() {
)} )}
</SimpleGrid> </SimpleGrid>
<Group justify="center" pb={10}> <Group justify="center" pb={"xl"}>
<Button <Button
component={Link} component={Link}
href="/darmasaba/apbdes" href="/darmasaba/apbdes"

View File

@@ -29,7 +29,7 @@ function DesaAntiKorupsi() {
const data = (state.desaAntikorupsi.findMany.data || []).slice(0, 6); const data = (state.desaAntikorupsi.findMany.data || []).slice(0, 6);
return ( return (
<Stack gap={"0"} bg={colors.Bg} p={"sm"}> <Stack gap={"0"} bg={colors.Bg} p={"sm"} my={"xs"}>
<Container w={{ base: "100%", md: "80%" }} p={"md"} > <Container w={{ base: "100%", md: "80%" }} p={"md"} >
<Center> <Center>
<Text fw={"bold"} c={colors["blue-button"]} fz={{ base: "1.8rem", md: "3.4rem" }}>Desa Anti Korupsi</Text> <Text fw={"bold"} c={colors["blue-button"]} fz={{ base: "1.8rem", md: "3.4rem" }}>Desa Anti Korupsi</Text>

View File

@@ -153,7 +153,7 @@ function Kepuasan() {
if (data.length === 0) { if (data.length === 0) {
return ( return (
<Stack p="sm"> <Stack p="sm" my={"xs"}>
<Container w={{ base: "100%", md: "80%" }} p={"sm"}> <Container w={{ base: "100%", md: "80%" }} p={"sm"}>
<Center> <Center>
<Text <Text
@@ -245,7 +245,7 @@ function Kepuasan() {
{/* Chart Rating */} {/* Chart Rating */}
<Paper bg={colors['white-1']} p="md" radius="md"> <Paper bg={colors['white-1']} p="md" radius="md">
<Stack> <Stack>
<Title order={4}>Pilihan</Title> <Title order={4}>Ulasan</Title>
{donutDataRating.every(item => item.value === 0) ? ( {donutDataRating.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md"> <Text c="dimmed" ta="center" my="md">
Belum ada data untuk ditampilkan dalam grafik Belum ada data untuk ditampilkan dalam grafik
@@ -421,7 +421,7 @@ function Kepuasan() {
); );
} }
return ( return (
<Stack p={"sm"}> <Stack p={"sm"} my={"xs"}>
<Container size="lg" px="sm"> <Container size="lg" px="sm">
<Center> <Center>
<Text <Text
@@ -517,7 +517,7 @@ function Kepuasan() {
{/* Chart Rating */} {/* Chart Rating */}
<Paper bg={colors['white-1']} p="md" radius="md"> <Paper bg={colors['white-1']} p="md" radius="md">
<Stack> <Stack>
<Title order={4}>Pilihan</Title> <Title order={4}>Ulasan</Title>
{donutDataRating.every(item => item.value === 0) ? ( {donutDataRating.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md"> <Text c="dimmed" ta="center" my="md">
Belum ada data untuk ditampilkan dalam grafik Belum ada data untuk ditampilkan dalam grafik

View File

@@ -87,7 +87,7 @@ function Slider() {
}}> }}>
<Box <Box
style={{ style={{
borderRadius: 16, borderRadius: 8,
zIndex: 0, zIndex: 0,
}} }}
pos={"absolute"} pos={"absolute"}

View File

@@ -3,11 +3,10 @@
import prestasiState from "@/app/admin/(dashboard)/_state/landing-page/prestasi-desa"; import prestasiState from "@/app/admin/(dashboard)/_state/landing-page/prestasi-desa";
import colors from "@/con/colors"; import colors from "@/con/colors";
import { BackgroundImage, Box, Button, Center, Container, Group, Loader, SimpleGrid, Stack, Text } from "@mantine/core"; import { BackgroundImage, Box, Button, Center, Container, Group, Loader, SimpleGrid, Stack, Text } from "@mantine/core";
import { useProxy } from "valtio/utils";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link"; import Link from "next/link";
import { IconTrophy } from "@tabler/icons-react"; import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { useProxy } from "valtio/utils";
function Prestasi() { function Prestasi() {
const state = useProxy(prestasiState.prestasiDesa); const state = useProxy(prestasiState.prestasiDesa);
@@ -33,12 +32,9 @@ function Prestasi() {
<Stack p="sm" bg="linear-gradient(180deg, #ffffff 0%, #f8fbff 100%)"> <Stack p="sm" bg="linear-gradient(180deg, #ffffff 0%, #f8fbff 100%)">
<Container w={{ base: "100%", md: "80%" }} p="xl"> <Container w={{ base: "100%", md: "80%" }} p="xl">
<Stack align="center" gap="sm"> <Stack align="center" gap="sm">
<Group gap="xs">
<IconTrophy size={36} color={colors["blue-button"]} />
<Text c={colors["blue-button"]} ta="center" fz={{ base: "2rem", md: "3.4rem" }} fw={700}> <Text c={colors["blue-button"]} ta="center" fz={{ base: "2rem", md: "3.4rem" }} fw={700}>
Prestasi Desa Prestasi Desa
</Text> </Text>
</Group>
<Text fz={{ base: "1rem", md: "1.3rem" }} ta="center" c="dimmed" maw={700}> <Text fz={{ base: "1rem", md: "1.3rem" }} ta="center" c="dimmed" maw={700}>
Kami bangga dengan pencapaian desa hingga saat ini. Semoga prestasi ini menjadi inspirasi untuk terus berkarya dan berinovasi demi kemajuan bersama. Kami bangga dengan pencapaian desa hingga saat ini. Semoga prestasi ini menjadi inspirasi untuk terus berkarya dan berinovasi demi kemajuan bersama.
</Text> </Text>
@@ -63,14 +59,13 @@ function Prestasi() {
) : data.length === 0 ? ( ) : data.length === 0 ? (
<Center mih={200}> <Center mih={200}>
<Stack align="center" gap="xs"> <Stack align="center" gap="xs">
<IconTrophy size={48} color="gray" />
<Text fz="1.2rem" fw={500} c="dimmed"> <Text fz="1.2rem" fw={500} c="dimmed">
Belum ada prestasi yang ditampilkan Belum ada prestasi yang ditampilkan
</Text> </Text>
</Stack> </Stack>
</Center> </Center>
) : ( ) : (
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="lg"> <SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="lg" mb={"xl"}>
{data.map((v, k) => ( {data.map((v, k) => (
<BackgroundImage <BackgroundImage
key={k} key={k}
@@ -82,7 +77,7 @@ function Prestasi() {
pos="absolute" pos="absolute"
inset={0} inset={0}
bg="linear-gradient(180deg, rgba(0,0,0,0.4) 0%, rgba(0,0,0,0.7) 100%)" bg="linear-gradient(180deg, rgba(0,0,0,0.4) 0%, rgba(0,0,0,0.7) 100%)"
style={{ borderRadius: "1rem" }} style={{ borderRadius: 27 }}
/> />
<Stack justify="space-between" h="100%" pos="relative" p="lg"> <Stack justify="space-between" h="100%" pos="relative" p="lg">
<Box> <Box>

View File

@@ -1,11 +1,12 @@
'use client' 'use client'
import { useEffect, useState } from "react" import colors from "@/con/colors"
import { Box, Button, Center, Container, Image, Paper, SimpleGrid, Stack, Text, Title, useMantineTheme, Tooltip } from "@mantine/core" import { Box, Button, Center, Container, Image, Paper, SimpleGrid, Stack, Text, Title, useMantineTheme } from "@mantine/core"
import { useMediaQuery } from "@mantine/hooks" import { useMediaQuery } from "@mantine/hooks"
import { Prisma } from "@prisma/client" import { Prisma } from "@prisma/client"
import Link from "next/link"
import { IconMoodSad } from "@tabler/icons-react" import { IconMoodSad } from "@tabler/icons-react"
import colors from "@/con/colors" import Link from "next/link"
import { useEffect, useState } from "react"
import { motion } from "framer-motion";
export default function SDGS() { export default function SDGS() {
const theme = useMantineTheme() const theme = useMantineTheme()
@@ -25,8 +26,8 @@ export default function SDGS() {
setSdgsDesa([]) setSdgsDesa([])
return return
} }
const top3Sdgs = [...data].sort((a, b) => parseInt(b.jumlah) - parseInt(a.jumlah)).slice(0, 3) const top4Sdgs = [...data].sort((a, b) => parseInt(b.jumlah) - parseInt(a.jumlah)).slice(0, 4)
setSdgsDesa(top3Sdgs) setSdgsDesa(top4Sdgs)
} catch { } catch {
setSdgsDesa([]) setSdgsDesa([])
} }
@@ -35,7 +36,7 @@ export default function SDGS() {
}, []) }, [])
return ( return (
<Stack p="sm"> <Stack p="sm" my={"xs"}>
<Container w={{ base: "100%", md: "80%" }} p="xl"> <Container w={{ base: "100%", md: "80%" }} p="xl">
<Center> <Center>
<Title <Title
@@ -52,63 +53,56 @@ export default function SDGS() {
</Text> </Text>
<Box py="lg"> <Box py="lg">
<Paper {sdgsDesa && sdgsDesa.length > 0 ? (
p={{ base: "md", md: "xl" }} <SimpleGrid cols={{ base: 1, sm: 4 }} spacing="xl" verticalSpacing="xl" pb={30}>
radius="2xl"
withBorder
shadow="lg"
style={{
background: "linear-gradient(145deg, #FFFFFF, #F9FAFB)",
border: "1px solid rgba(0,0,0,0.06)",
}}
>
{sdgsDesa && sdgsDesa.length > 0 ? (
<SimpleGrid cols={{ base: 1, sm: 3 }} spacing="xl" verticalSpacing="xl">
{sdgsDesa.map((item) => ( {sdgsDesa.map((item) => (
<Paper <motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
key={item.id} key={item.id}
p="lg"
radius="xl"
shadow="sm"
withBorder
style={{
background: "linear-gradient(180deg, #FFFFFF, #F6F8FA)",
border: "1px solid rgba(0,0,0,0.05)",
transition: "all 0.3s ease",
height: "100%", // biar tinggi antar card konsisten
display: "flex",
flexDirection: "column",
}}
> >
<Center mb="lg"> <Paper
<Box p="lg"
p="md" radius="xl"
style={{ shadow="sm"
background: "rgba(240, 249, 255, 0.8)", withBorder
backdropFilter: "blur(6px)", style={{
width: mobile ? 140 : 160, background: "linear-gradient(180deg, #FFFFFF, #F6F8FA)",
height: mobile ? 140 : 160, border: "1px solid rgba(0,0,0,0.05)",
display: "flex", transition: "all 0.3s ease",
alignItems: "center", height: "100%", // biar tinggi antar card konsisten
justifyContent: "center", display: "flex",
borderRadius: "1rem", flexDirection: "column",
boxShadow: "0 6px 16px rgba(0,0,0,0.06)", }}
}} >
> <Center mb="lg">
<Image <Box
src={item.image?.link ? item.image.link : "/placeholder-sdgs.png"} p="md"
alt={item.name} style={{
w={mobile ? 90 : 110} background: "rgba(240, 249, 255, 0.8)",
h={mobile ? 90 : 110} backdropFilter: "blur(6px)",
fit="contain" width: mobile ? 140 : 160,
loading="lazy" height: mobile ? 140 : 160,
/> display: "flex",
</Box> alignItems: "center",
</Center> justifyContent: "center",
borderRadius: "1rem",
{/* Stack isi teks & angka */} boxShadow: "0 6px 16px rgba(0,0,0,0.06)",
<Stack justify="space-between" align="center" gap="xs" h="100%"> }}
<Tooltip label="Nama tujuan SDGs Desa" position="top" withArrow> >
<Image
src={item.image?.link ? item.image.link : "/placeholder-sdgs.png"}
alt={item.name}
w={mobile ? 90 : 110}
h={mobile ? 90 : 110}
fit="contain"
loading="lazy"
/>
</Box>
</Center>
{/* Stack isi teks & angka */}
<Stack justify="space-between" align="center" gap="xs" h="100%">
<Text <Text
ta="center" ta="center"
fz={{ base: "lg", md: "xl" }} fz={{ base: "lg", md: "xl" }}
@@ -118,34 +112,33 @@ export default function SDGS() {
> >
{item.name} {item.name}
</Text> </Text>
</Tooltip>
<Title
<Title order={2}
order={2} ta="center"
ta="center" style={{
style={{ fontSize: mobile ? "2.4rem" : "3.2rem",
fontSize: mobile ? "2.4rem" : "3.2rem", fontWeight: 900,
fontWeight: 900, letterSpacing: "-0.5px",
letterSpacing: "-0.5px", color: "#124170",
color: "#124170", }}
}} >
> {item.jumlah}
{item.jumlah} </Title>
</Title> </Stack>
</Stack> </Paper>
</Paper> </motion.div>
))} ))}
</SimpleGrid> </SimpleGrid>
) : ( ) : (
<Center mih={200} style={{ flexDirection: "column" }}> <Center mih={200} style={{ flexDirection: "column" }}>
<IconMoodSad size={48} stroke={1.5} style={{ marginBottom: "1rem" }} /> <IconMoodSad size={48} stroke={1.5} style={{ marginBottom: "1rem" }} />
<Text fz="lg" c="dimmed"> <Text fz="lg" c="dimmed">
Data SDGs Desa belum tersedia Data SDGs Desa belum tersedia
</Text> </Text>
</Center> </Center>
)} )}
</Paper>
<Center> <Center>
<Button <Button
@@ -156,7 +149,19 @@ export default function SDGS() {
mt="md" mt="md"
variant="gradient" variant="gradient"
gradient={{ from: "#26667F", to: "#124170" }} gradient={{ from: "#26667F", to: "#124170" }}
style={{ boxShadow: "0 6px 14px rgba(18,65,112,0.25)"}} style={{
boxShadow: "0 6px 14px rgba(18,65,112,0.25)",
transition: "all 0.3s ease",
transform: "translateY(0)",
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = "translateY(-4px)";
e.currentTarget.style.boxShadow = "0 10px 20px rgba(18,65,112,0.35)";
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = "translateY(0)";
e.currentTarget.style.boxShadow = "0 6px 14px rgba(18,65,112,0.25)";
}}
> >
<Text c="white" fz={{ base: "md", md: "lg" }} fw="bold">Jelajahi Semua Tujuan SDGs Desa</Text> <Text c="white" fz={{ base: "md", md: "lg" }} fw="bold">Jelajahi Semua Tujuan SDGs Desa</Text>
</Button> </Button>

View File

@@ -19,7 +19,7 @@ export default function Page() {
<Box> <Box>
<Stack <Stack
bg={colors.grey[1]} bg={colors.grey[1]}
gap={"1.5rem"} gap={0}
> >
<LandingPage /> <LandingPage />
<Penghargaan /> <Penghargaan />

View File

@@ -35,13 +35,13 @@ const navbarListMenu = [
}, },
{ {
id: "1.7", id: "1.7",
name: "Daftar Informasi Publik Desa Darmasaba", name: "Daftar Informasi Publik",
href: "/darmasaba/ppid/daftar-informasi-publik-desa-darmasaba" href: "/darmasaba/ppid/daftar-informasi-publik"
}, },
{ {
id: "1.8", id: "1.8",
name: "IKM Desa Darmasaba", name: "Indeks Kepuasan Masyarakat",
href: "/darmasaba/ppid/ikm-desa-darmasaba" href: "/darmasaba/ppid/indeks-kepuasan-masyarakat"
}, },
] ]