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">
Informasi & Pelayanan Potensi Desa Digital
</Text>
<Image
src={state.findUnique.data?.image?.link || ''}
alt={state.findUnique.data?.name || 'Potensi Desa'}
radius="lg"
fit="cover"
{/* ✅ Bagian gambar dibuat konsisten tanpa CSS manual */}
<Box
w="100%"
h={{ base: 220, md: 400 }}
fallbackSrc="https://placehold.co/800x400?text=Gambar+tidak+tersedia"
loading="lazy"
/>
style={{
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.' }} />
</Stack>
</Paper>

View File

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

View File

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

View File

@@ -28,7 +28,7 @@ function MaskotDesa() {
}
return (
<Box pb={80}>
<Box>
<Stack align="center" gap="xl">
<Stack align="center" gap={10}>
<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() {
return (
<Box pb={80} px={{ base: "md", md: "xl" }}>
<Box px={{ base: "md", md: "xl" }}>
<Stack align="center" gap="lg">
<Box>
<Text

View File

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

View File

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

View File

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

View File

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

View File

@@ -24,7 +24,7 @@ function VisiMisiDesa() {
}
return (
<Box py="xl">
<Box>
<Stack align="center" gap="xl">
<Image
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 @typescript-eslint/no-explicit-any */
'use client'
import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi'
import colors from '@/con/colors'
import {
@@ -9,20 +9,28 @@ import {
Button,
Card,
Center,
Container,
Group,
Image,
Loader,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
Transition,
Transition
} 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 { useEffect } from 'react'
import { useEffect, useRef, useState } from 'react'
import { useProxy } from 'valtio/utils'
import BackButton from '../../desa/layanan/_com/BackButto'
@@ -36,35 +44,40 @@ export default function Page() {
paddingBottom: 48,
}}
>
<Container size="xl" py="xl">
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }} py="xl">
<BackButton />
<Stack align="center" gap="xl" mt="xl">
<Title
order={1}
ta="center"
c={colors['blue-button']}
fz={{ base: 28, md: 36, lg: 44 }}
>
Struktur Organisasi Dan SK Pengurus BumDes
Struktur Organisasi & SK Pengurus BumDes
</Title>
<Text ta="center" c="black" maw={800}>
Gambaran visual peran dan pegawai yang ditugaskan. Arahkan kursor
untuk melihat detail atau klik node untuk fokus tampilan.
Gambaran visual peran dan pengurus yang ditugaskan. Gunakan kontrol
di bawah untuk mencari, memperbesar, atau melihat lebih jelas.
</Text>
</Stack>
<Box mt="lg">
<StrukturOrganisasiBumDes />
</Box>
</Container>
</Box>
</Box>
)
}
function StrukturOrganisasiBumDes() {
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(() => {
void stateOrganisasi.findMany.load()
@@ -81,17 +94,15 @@ function StrukturOrganisasiBumDes() {
<Loader size="lg" />
<Text fw={600}>Memuat struktur organisasi</Text>
<Text c="dimmed" size="sm">
Mengambil data pegawai dan posisi. Mohon tunggu sebentar.
Mengambil data pengurus dan posisi. Mohon tunggu sebentar.
</Text>
</Stack>
</Center>
)
}
if (
!stateOrganisasi.findMany.data ||
stateOrganisasi.findMany.data.length === 0
) {
const data = stateOrganisasi.findMany.data || []
if (data.length === 0) {
return (
<Center py={40}>
<Stack align="center" gap="md">
@@ -109,11 +120,10 @@ function StrukturOrganisasiBumDes() {
<IconUsers size={56} />
</Center>
<Title order={3} mt="md">
Data pegawai belum tersedia
Data pengurus belum tersedia
</Title>
<Text c="dimmed" mt="xs">
Belum ada data pegawai yang tercatat untuk BumDes. Silakan coba
muat ulang atau periksa sumber data.
Belum ada data pengurus yang tercatat untuk BumDes.
</Text>
<Group justify="center" mt="lg">
<Button
@@ -124,15 +134,6 @@ function StrukturOrganisasiBumDes() {
>
Muat Ulang
</Button>
<Button
leftSection={<IconSearch size={16} />}
variant="subtle"
onClick={() =>
stateOrganisasi.findMany.load({ query: { q: '' } })
}
>
Cari Pegawai
</Button>
</Group>
</Paper>
</Stack>
@@ -140,161 +141,232 @@ function StrukturOrganisasiBumDes() {
)
}
// 📊 susun struktur organisasi
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) {
const posisiId = pegawai.posisi.id;
const posisiId = pegawai.posisi.id
if (!posisiMap.has(posisiId)) {
posisiMap.set(posisiId, {
...pegawai.posisi,
pegawaiList: [],
children: [],
});
})
}
posisiMap.get(posisiId)!.pegawaiList.push(pegawai);
posisiMap.get(posisiId)!.pegawaiList.push(pegawai)
}
// First, create a map of all unique positions
const allPositions = new Map();
aktifPegawai.forEach((pegawai: any) => {
if (!allPositions.has(pegawai.posisi.id)) {
allPositions.set(pegawai.posisi.id, {
...pegawai.posisi,
pegawaiList: [],
children: []
});
}
});
const root: any[] = []
posisiMap.forEach((posisi) => {
if (posisi.parentId) {
const parent = posisiMap.get(posisi.parentId)
if (parent) parent.children.push(posisi)
else root.push(posisi)
} else root.push(posisi)
})
// Then assign employees to their positions
aktifPegawai.forEach((pegawai: any) => {
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 {
const toOrgChartFormat = (node: any): any => {
const pegawai = node.pegawaiList?.[0]
return {
expanded: true,
type: 'person',
styleClass: 'p-person',
data: {
name: node.pegawaiList?.[0]?.namaLengkap || 'Belum ditugaskan',
title: node.nama || 'Tanpa jabatan',
image: node.pegawaiList?.[0]?.image?.link || '/img/default.png',
id: pegawai?.id,
name: pegawai?.namaLengkap || 'Belum Ditugaskan',
title: node.nama || 'Tanpa Jabatan',
image: pegawai?.image?.link || '/img/default.png',
description: node.deskripsi || '',
positionId: node.id || null,
},
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 (
<Box py={16} >
<Paper
radius="md"
p="md"
style={{
background: 'rgba(28,110,164,0.2)',
border: `1px solid rgba(255,255,255,0.1)`,
overflowX: 'auto',
}}
>
<OrganizationChart
value={chartData}
nodeTemplate={nodeTemplate}
/>
<Stack align="center" mt="xl">
{/* 🧭 Kontrol atas */}
<Paper shadow="xs" p="md" radius="md" bg={colors['blue-button']}>
<Group gap="sm" wrap="wrap" justify="center">
<TextInput
placeholder="Cari nama atau jabatan..."
leftSection={<IconSearch size={16} />}
onChange={(e) => debouncedSearch(e.target.value)}
styles={{
input: {
minWidth: 250,
},
}}
/>
<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>
</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 name = node?.data?.name || 'Tanpa Nama'
const title = node?.data?.title || 'Tanpa Jabatan'
const description = node?.data?.description || ''
return (
<Transition mounted transition="pop" duration={240}>
<Transition mounted transition="pop" duration={300}>
{(styles) => (
<Card
radius="lg"
shadow="md"
radius="xl"
withBorder
style={{
...styles,
width: 260,
padding: 16,
background: 'rgba(28,110,164,0.3)',
borderColor: 'rgba(255,255,255,0.15)',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
textAlign: 'center',
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',
}}
>
<Image
src={imageSrc}
alt={name}
radius="md"
width={120}
height={120}
fit="cover"
style={{
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' })
}
<Stack align="center" gap={10}>
<Box
style={{
width: 90,
height: 90,
borderRadius: '50%',
overflow: 'hidden',
border: '3px solid rgba(28, 110, 164, 0.4)',
}}
>
Kembali
</Button>
</Tooltip>
<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>
</Card>
)}
</Transition>
)
}

View File

@@ -57,7 +57,8 @@ function Page() {
/>
</GridCol>
</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 px={{ base: "md", md: 100 }}>
<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'
import keamananLingkunganState from '@/app/admin/(dashboard)/_state/keamanan/keamanan-lingkungan';
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 { IconSearch } from '@tabler/icons-react';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto';
import { useRouter } from 'next/navigation';
function Page() {
const state = useProxy(keamananLingkunganState)
const [expandedMap, setExpandedMap] = useState<Record<number, boolean>>({});
const router = useRouter()
const [search, setSearch] = useState('')
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const {
@@ -26,13 +27,6 @@ function Page() {
load(page, 3, debouncedSearch)
}, [page, debouncedSearch])
const toggleExpanded = (index: number, value: boolean) => {
setExpandedMap((prev) => ({
...prev,
[index]: value,
}));
};
if (loading || !data) {
return (
<Box py={10}>
@@ -65,60 +59,100 @@ function Page() {
</GridCol>
</Grid>
<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>
</Box>
<Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'}>
<SimpleGrid
pb={10}
cols={{
base: 1,
md: 3,
}}>
{data.map((v, k) => {
return (
<Paper radius={10} key={k} bg={colors["white-trans-1"]}>
<Stack gap={'xs'}>
<Center px={10} py={20}>
<Image loading="lazy" src={v.image?.link} alt='' />
</Center>
<Box px={'lg'}>
<Box pb={20}>
<Text pb={10} c={colors["blue-button"]} fw={"bold"} fz={"h3"}>
{v.name}
</Text>
<Spoiler
showLabel={
<Text fw="bold" fz="sm" c={colors['blue-button']}>
Show more
</Text>
}
hideLabel={
<Text fw="bold" fz="sm" c={colors['blue-button']}>
Hide details
</Text>
}
expanded={expandedMap[k] || false}
onExpandedChange={(val) => toggleExpanded(k, val)}
>
<Text pb={10} fz={"h4"} ta={'justify'} style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: v.deskripsi }} />
</Spoiler>
</Box>
</Box>
</Stack>
</Paper>
)
})}
cols={{ base: 1, sm: 2, md: 3 }} spacing="xl" mt="lg">
{data.map((v, k) => (
<Paper
key={k}
radius="xl"
shadow="md"
withBorder
p="lg"
bg={colors['white-trans-1']}
style={{
transition: 'all 200ms ease',
cursor: 'pointer',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
height: '100%',
}}
>
<Stack align="center" gap="sm" style={{ flexGrow: 1 }}>
<Box
style={{
width: '100%',
aspectRatio: '16/9',
borderRadius: '12px',
overflow: 'hidden',
position: 'relative',
}}
>
<Image
src={v.image?.link}
alt={v.name}
fit="cover"
loading="lazy"
style={{
width: '100%',
height: '100%',
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>
</Stack>
</Box>
<Center>
<Center mt="xl">
<Pagination
value={page}
onChange={(newPage) => load(newPage)} // ini penting!
onChange={(newPage) => load(newPage, 3, search)}
total={totalPages}
my="md"
size="lg"
radius="xl"
styles={{
control: {
border: `1px solid ${colors['blue-button']}`,
},
}}
/>
</Center>
</Stack>

View File

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

View File

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

View File

@@ -9,21 +9,16 @@ import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto';
import { useRouter } from 'next/navigation';
function Page() {
const state = useProxy(polsekTerdekatState.findFirst);
const router = useRouter()
const {
data,
loading,
load,
} = state;
const router = useRouter();
const { data, loading, load } = state;
useEffect(() => {
load();
}, []);
// kalau masih loading
// Loading state
if (loading) {
return (
<Stack py={10}>
@@ -32,104 +27,175 @@ function Page() {
);
}
// kalau data kosong
// Data kosong
if (!data) {
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box pb={10} px={{ base: 20, md: 100 }}>
<Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
Kantor Polisi Terdekat
</Text>
<Text pb={15} fz={'md'} >
Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung
</Text>
</Box>
<Center py="xl">
<Text fz="lg" fw="bold" c="red">
Data Polsek tidak ada
</Text>
</Center>
</Stack >
<Stack pos="relative" bg={colors.Bg} py="xl" gap={22}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box pb={10} px={{ base: 20, md: 100 }}>
<Text fz={{ base: 'h1', md: '2.5rem' }} c={colors['blue-button']} fw="bold">
Kantor Polisi Terdekat
</Text>
<Text pb={15} fz="md">
Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung
</Text>
</Box>
<Center py="xl">
<Text fz="lg" fw="bold" c="red">
Data Polsek tidak ada
</Text>
</Center>
</Stack>
);
}
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 }}>
<BackButton />
</Box>
<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
</Text>
<Text pb={15} fz={'h4'} >
<Text pb={15} fz="h4">
Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung
</Text>
</Box>
<Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'}>
<Paper radius={10} bg={colors["white-trans-1"]} p={'xl'}>
<Stack gap={'xs'}>
<SimpleGrid
cols={{
base: 1,
md: 2,
}}
>
{/* Content Sebelah Kiri */}
<Box px={{ base: 'md', md: 100 }}>
<Stack gap="lg">
<Paper radius={10} bg={colors['white-trans-1']} p="xl">
<Stack gap="xs">
<SimpleGrid cols={{ base: 1, md: 2 }}>
{loading ? (
<Center><Skeleton h={400} /></Center>
) : data ? (
<Center>
<Skeleton h={400} />
</Center>
) : (
<>
{/* === KIRI === */}
<Box>
<Text c={colors["blue-button"]} fw={'bold'} fz={'h2'}>{data.nama}</Text>
<Text c={colors["blue-button"]} fz={'sm'}>{data.jarakKeDesa}</Text>
<Flex py={10} gap={9} align={'center'}>
<IconPin size={25} color={colors["blue-button"]} />
<Text c={colors["blue-button"]} fz={'lg'}>{data.alamat}</Text>
</Flex>
<Flex gap={9} align={'center'}>
<IconPhone size={25} color={colors["blue-button"]} />
<Text c={colors["blue-button"]} fz={'lg'}>{data.nomorTelepon}</Text>
</Flex>
<Flex py={10} gap={9} align={'center'}>
<IconClock size={25} color={colors["blue-button"]} />
<Text c={colors["blue-button"]} fz={'lg'}>{data.jamOperasional}</Text>
</Flex>
<Box>
<Text c={colors["blue-button"]} fw={'bold'} fz={'h2'}>Layanan Yang Tersedia :</Text>
<SimpleGrid
py={10}
cols={{
base: 1,
md: 2,
<Text c={colors['blue-button']} fw="bold" fz="h2">
{data.nama}
</Text>
<Text c={colors['blue-button']} fz="sm">
{data.jarakKeDesa}
</Text>
{/* Alamat */}
<Flex
py={10}
gap={9}
align="flex-start"
wrap="wrap"
style={{ wordBreak: 'break-word' }}
>
<Box w={25} mt={3}>
<IconPin size={22} />
</Box>
<Text
fz="lg"
style={{
flex: 1,
wordBreak: 'break-word',
lineHeight: 1.4,
}}
>
<Box>
<Text c={colors["blue-button"]} fz={'lg'}>{data.layananPolsek.nama}</Text>
</Box>
{data.alamat}
</Text>
</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>
</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 pos={'relative'}>
{/* === KANAN === */}
<Box pos="relative">
<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 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 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>
</>
) : null}
)}
</SimpleGrid>
</Stack>
</Paper>

View File

@@ -56,8 +56,11 @@ function Page() {
/>
</GridCol>
</Grid>
<Text px={{ base: 'md', md: 100 }} ta={"justify"} fz={{ base: "h4", md: "h3" }} >
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.
<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).
</Text>
<Text px={{ base: 'md', md: 100 }} ta={"justify"} fz="md" >
Mereka bertugas memastikan desa tetap aman, tertib, dan kondusif bagi seluruh warga.
</Text>
</Box>
<Box px={{ base: "md", md: 100 }}>
@@ -82,7 +85,7 @@ function Page() {
{v.judul}
</Text>
<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>

View File

@@ -88,59 +88,83 @@ function Page() {
) : (
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="xl" mt="lg">
{data.map((v, k) => (
<Paper
key={k}
radius="xl"
shadow="md"
withBorder
p="lg"
bg={colors['white-trans-1']}
style={{
transition: 'all 200ms ease',
cursor: 'pointer',
}}
>
<Stack align="center" gap="sm">
<Box
style={{
width: '100%',
aspectRatio: '16/9',
borderRadius: '12px',
overflow: 'hidden',
position: 'relative',
}}
>
<Image
src={v.image.link}
alt={v.name}
fit="cover"
loading="lazy"
style={{
width: '100%',
height: '100%',
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="center" lineClamp={3}>
<span style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: v.deskripsi }} />
</Text>
<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>
</Stack>
</Paper>
<Paper
key={k}
radius="xl"
shadow="md"
withBorder
p="lg"
bg={colors['white-trans-1']}
style={{
transition: 'all 200ms ease',
cursor: 'pointer',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between', // ✅ biar button selalu di bawah
height: '100%', // ✅ bikin tinggi seragam
}}
>
<Stack align="center" gap="sm" style={{ flexGrow: 1 }}>
<Box
style={{
width: '100%',
aspectRatio: '16/9',
borderRadius: '12px',
overflow: 'hidden',
position: 'relative',
}}
>
<Image
src={v.image.link}
alt={v.name}
fit="cover"
loading="lazy"
style={{
width: '100%',
height: '100%',
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="center"
lineClamp={3}
lh={1.6}
style={{
minHeight: '4.8em', // tinggi tetap 3 baris
}}
>
<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>
)}

View File

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

View File

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

View File

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

View File

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

View File

@@ -91,7 +91,7 @@ function Page() {
<Box style={{ alignContent: 'center', alignItems: 'center' }}>
{iconMap[v.icon] ? React.createElement(iconMap[v.icon]) : null}
</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>
</Paper>
</Box>

View File

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

View File

@@ -55,7 +55,7 @@ function Page({ params }: PageProps) {
<Group justify="space-between" align="center" mb="md">
<Group gap="sm">
<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>
<TextInput
placeholder='pencarian'

View File

@@ -55,7 +55,7 @@ function Page({ params }: PageProps) {
<Group justify="space-between" align="center" mb="md">
<Group gap="sm">
<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>
<TextInput
placeholder='pencarian'

View File

@@ -55,7 +55,7 @@ function Page({ params }: PageProps) {
<Group justify="space-between" align="center" mb="md">
<Group gap="sm">
<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>
<TextInput
placeholder='pencarian'

View File

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

View File

@@ -47,7 +47,7 @@ function Page() {
<Group justify="space-between" align="center" mb="md">
<Group gap="sm">
<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>
<TextInput
placeholder='pencarian'

View File

@@ -46,7 +46,7 @@ function Page() {
<Group justify="space-between" align="center" mb="md">
<Group gap="sm">
<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>
<TextInput
placeholder='pencarian'

View File

@@ -47,7 +47,7 @@ function Page() {
<Group justify="space-between" align="center" mb="md">
<Group gap="sm">
<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>
<TextInput
placeholder='pencarian'

View File

@@ -37,13 +37,15 @@ function Page() {
<Box px={{ base: 'md', md: 100 }} pb={50}>
<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
</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">
Desa Darmasaba berkomitmen mencetak generasi muda yang cerdas, berkarakter, dan siap bersaing melalui program pendidikan yang inklusif dan berkelanjutan.
</Text>
<Divider size="sm" color={colors['blue-button']} mx="auto" maw={120} />
</Box>
<SimpleGrid
@@ -66,7 +68,7 @@ function Page() {
</Title>
</Group>
<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>
</Stack>
</Paper>
@@ -87,7 +89,7 @@ function Page() {
</Title>
</Group>
<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>
</Stack>
</Paper>

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID'
import ScrollToTopButton from '@/app/darmasaba/_com/scrollToTopButton'
import colors from '@/con/colors'
@@ -10,7 +9,6 @@ import {
Button,
Card,
Center,
Container,
Group,
Image,
Loader,
@@ -36,6 +34,7 @@ import { OrganizationChart } from 'primereact/organizationchart'
import { useEffect, useRef, useState } from 'react'
import { useProxy } from 'valtio/utils'
import BackButton from '../../desa/layanan/_com/BackButto'
import './struktur.css'
export default function Page() {
return (
@@ -47,10 +46,9 @@ export default function Page() {
paddingBottom: 48,
}}
>
<Container size="xl" py="xl">
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }} py={"xl"}>
<BackButton />
<Stack align="center" gap="xl" mt="xl">
<Title
order={1}
@@ -65,12 +63,12 @@ export default function Page() {
untuk melihat detail atau klik node untuk fokus tampilan.
</Text>
</Stack>
<Box mt="lg">
<StrukturOrganisasiPPID />
</Box>
</Container>
</Box>
{/* Tombol Scroll ke Atas */}
<ScrollToTopButton />
</Box>
)
@@ -84,7 +82,7 @@ function StrukturOrganisasiPPID() {
const [isFullscreen, setFullscreen] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
// debounce untuk pencarian
// debounce pencarian
const debouncedSearch = useRef(
debounce((value: string) => {
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 (
<Center py={40}>
<Stack align="center" gap="md">
@@ -151,9 +150,9 @@ function StrukturOrganisasiPPID() {
)
}
// Buat struktur organisasi
// 🧩 buat struktur organisasi
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) {
const posisiId = pegawai.posisi.id
@@ -176,19 +175,15 @@ function StrukturOrganisasiPPID() {
} else root.push(posisi)
})
function toOrgChartFormat(node: any): any {
const toOrgChartFormat = (node: any): any => {
const pegawai = node.pegawaiList?.[0]
return {
expanded: true,
type: 'person',
styleClass: 'p-person',
data: {
id: pegawai?.id || null,
name: pegawai?.namaLengkap || 'Belum ditugaskan',
title: node.nama || 'Tanpa jabatan',
id: pegawai?.id,
name: pegawai?.namaLengkap || 'Belum Ditugaskan',
title: node.nama || 'Tanpa Jabatan',
image: pegawai?.image?.link || '/img/default.png',
description: node.deskripsi || '',
positionId: node.id || null,
},
children: node.children?.map(toOrgChartFormat) || [],
}
@@ -213,7 +208,7 @@ function StrukturOrganisasiPPID() {
chartData = filterNodes(chartData)
}
// 🧭 fungsi fullscreen
// 🎬 fullscreen & zoom control
const toggleFullscreen = () => {
if (!document.fullscreenElement) {
chartContainerRef.current?.requestFullscreen()
@@ -224,138 +219,249 @@ function StrukturOrganisasiPPID() {
}
}
// 🧭 fungsi zoom
const handleZoomIn = () => setScale((prev) => Math.min(prev + 0.1, 2))
const handleZoomOut = () => setScale((prev) => Math.max(prev - 0.1, 0.5))
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 (
<Stack align="center" mt="xl">
{/* 🔍 Search + Zoom + Fullscreen controls */}
<Group mb="md" justify="center" gap="sm" align="center">
<TextInput
placeholder="Cari nama atau jabatan..."
leftSection={<IconSearch size={16} />}
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}
{/* 🔍 Controls */}
<Paper
shadow="xs"
p="md"
radius="md"
style={{
overflow: 'auto',
transform: `scale(${scale})`,
transformOrigin: 'center top',
transition: 'transform 0.25s ease',
background: colors['blue-button']
}}
>
<OrganizationChart
value={chartData}
nodeTemplate={(node) => nodeTemplate(node, router)}
/>
</Box>
<Group gap="sm" wrap="wrap" justify="center">
<TextInput
placeholder="Cari nama atau jabatan..."
leftSection={<IconSearch size={16} />}
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>
)
}
function nodeTemplate(node: any, router: ReturnType<typeof useTransitionRouter>) {
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 description = node?.data?.description || ''
const hasId = Boolean(node?.data?.id)
return (
<Transition mounted transition="pop" duration={240}>
<Transition mounted transition="pop" duration={300}>
{(styles) => (
<Card
radius="lg"
shadow="md"
radius="xl"
withBorder
style={{
...styles,
width: 260,
padding: 16,
background: 'rgba(28,110,164,0.3)',
borderColor: 'rgba(255,255,255,0.15)',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
textAlign: 'center',
width: 240,
minHeight: 280,
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)',
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 = ''
}
}}
>
<Image
src={imageSrc}
alt={name}
radius="md"
width={60}
height={60}
fit="cover"
style={{
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>
<Button
variant="light"
size="xs"
mt="md"
onClick={() => {
const id = node?.data?.id
router.push(`/darmasaba/ppid/struktur-ppid/${id}`)
<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',
}}
>
Lihat Detail
</Button>
<Image
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>
)}
</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">
{data.map((v: any, k: number) => (
<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">
<Box>
<Text fz="lg" fw={600} c="white" ta="center">

View File

@@ -1,10 +1,10 @@
'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 { 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() {
const [sdgsDesa, setSdgsDesa] = useState<Prisma.SdgsDesaGetPayload<{ include: { image: true } }>[]>([]);
@@ -114,11 +114,9 @@ function Page() {
/>
</Box>
<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' }}>
{item.name}
</Title>
</Tooltip>
<Text
ta="center"
fw={700}

View File

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

View File

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

View File

@@ -49,12 +49,12 @@ export function NavbarSubMenu({ item }: { item: MenuItem[] | null }) {
rightSection={<IconArrowRight size={18} />}
styles={(theme) => ({
root: {
background: link.href && pathname.startsWith(link.href) ? theme.colors.blue[0] : 'transparent',
color: link.href && pathname.startsWith(link.href) ? theme.colors.blue[7] : colors['blue-button'],
background: link.href && pathname.startsWith(link.href) ? colors['blue-button-2'] : 'transparent',
color: link.href && pathname.startsWith(link.href) ? colors['blue-button'] : 'gray',
fontWeight: link.href && pathname.startsWith(link.href) ? 600 : 500,
transition: "all 0.2s ease",
"&: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 (
<Stack p="sm" gap="xl" bg={colors.Bg}>
<Box>
<Box mt={"xl"}>
<Stack gap="sm">
<Text c={colors["blue-button"]} ta={"center"} fw={"bold"} fz={{ base: "1.8rem", md: "3.4rem" }}>
{textHeading.title}
@@ -72,12 +72,7 @@ function Apbdes() {
pos="relative"
style={{ overflow: 'hidden' }}
>
<Box
pos="absolute"
inset={0}
bg="rgba(0,0,0,0.55)"
style={{ backdropFilter: 'blur(4px)' }}
/>
<Box pos="absolute" inset={0} bg="rgba(0,0,0,0.45)" style={{ borderRadius: 16 }} />
<Stack justify="space-between" h="100%" p="xl" pos="relative">
<Text
c="white"
@@ -117,7 +112,7 @@ function Apbdes() {
)}
</SimpleGrid>
<Group justify="center" pb={10}>
<Group justify="center" pb={"xl"}>
<Button
component={Link}
href="/darmasaba/apbdes"

View File

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

View File

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

View File

@@ -3,11 +3,10 @@
import prestasiState from "@/app/admin/(dashboard)/_state/landing-page/prestasi-desa";
import colors from "@/con/colors";
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 { IconTrophy } from "@tabler/icons-react";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { useProxy } from "valtio/utils";
function Prestasi() {
const state = useProxy(prestasiState.prestasiDesa);
@@ -33,12 +32,9 @@ function Prestasi() {
<Stack p="sm" bg="linear-gradient(180deg, #ffffff 0%, #f8fbff 100%)">
<Container w={{ base: "100%", md: "80%" }} p="xl">
<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}>
Prestasi Desa
</Text>
</Group>
<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.
</Text>
@@ -63,14 +59,13 @@ function Prestasi() {
) : data.length === 0 ? (
<Center mih={200}>
<Stack align="center" gap="xs">
<IconTrophy size={48} color="gray" />
<Text fz="1.2rem" fw={500} c="dimmed">
Belum ada prestasi yang ditampilkan
</Text>
</Stack>
</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) => (
<BackgroundImage
key={k}
@@ -82,7 +77,7 @@ function Prestasi() {
pos="absolute"
inset={0}
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">
<Box>

View File

@@ -1,11 +1,12 @@
'use client'
import { useEffect, useState } from "react"
import { Box, Button, Center, Container, Image, Paper, SimpleGrid, Stack, Text, Title, useMantineTheme, Tooltip } from "@mantine/core"
import colors from "@/con/colors"
import { Box, Button, Center, Container, Image, Paper, SimpleGrid, Stack, Text, Title, useMantineTheme } from "@mantine/core"
import { useMediaQuery } from "@mantine/hooks"
import { Prisma } from "@prisma/client"
import Link from "next/link"
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() {
const theme = useMantineTheme()
@@ -25,8 +26,8 @@ export default function SDGS() {
setSdgsDesa([])
return
}
const top3Sdgs = [...data].sort((a, b) => parseInt(b.jumlah) - parseInt(a.jumlah)).slice(0, 3)
setSdgsDesa(top3Sdgs)
const top4Sdgs = [...data].sort((a, b) => parseInt(b.jumlah) - parseInt(a.jumlah)).slice(0, 4)
setSdgsDesa(top4Sdgs)
} catch {
setSdgsDesa([])
}
@@ -35,7 +36,7 @@ export default function SDGS() {
}, [])
return (
<Stack p="sm">
<Stack p="sm" my={"xs"}>
<Container w={{ base: "100%", md: "80%" }} p="xl">
<Center>
<Title
@@ -52,63 +53,56 @@ export default function SDGS() {
</Text>
<Box py="lg">
<Paper
p={{ base: "md", md: "xl" }}
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 && sdgsDesa.length > 0 ? (
<SimpleGrid cols={{ base: 1, sm: 4 }} spacing="xl" verticalSpacing="xl" pb={30}>
{sdgsDesa.map((item) => (
<Paper
<motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
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">
<Box
p="md"
style={{
background: "rgba(240, 249, 255, 0.8)",
backdropFilter: "blur(6px)",
width: mobile ? 140 : 160,
height: mobile ? 140 : 160,
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "1rem",
boxShadow: "0 6px 16px rgba(0,0,0,0.06)",
}}
>
<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%">
<Tooltip label="Nama tujuan SDGs Desa" position="top" withArrow>
<Paper
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">
<Box
p="md"
style={{
background: "rgba(240, 249, 255, 0.8)",
backdropFilter: "blur(6px)",
width: mobile ? 140 : 160,
height: mobile ? 140 : 160,
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "1rem",
boxShadow: "0 6px 16px rgba(0,0,0,0.06)",
}}
>
<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
ta="center"
fz={{ base: "lg", md: "xl" }}
@@ -118,34 +112,33 @@ export default function SDGS() {
>
{item.name}
</Text>
</Tooltip>
<Title
order={2}
ta="center"
style={{
fontSize: mobile ? "2.4rem" : "3.2rem",
fontWeight: 900,
letterSpacing: "-0.5px",
color: "#124170",
}}
>
{item.jumlah}
</Title>
</Stack>
</Paper>
<Title
order={2}
ta="center"
style={{
fontSize: mobile ? "2.4rem" : "3.2rem",
fontWeight: 900,
letterSpacing: "-0.5px",
color: "#124170",
}}
>
{item.jumlah}
</Title>
</Stack>
</Paper>
</motion.div>
))}
</SimpleGrid>
) : (
<Center mih={200} style={{ flexDirection: "column" }}>
<IconMoodSad size={48} stroke={1.5} style={{ marginBottom: "1rem" }} />
<Text fz="lg" c="dimmed">
Data SDGs Desa belum tersedia
</Text>
</Center>
)}
</Paper>
) : (
<Center mih={200} style={{ flexDirection: "column" }}>
<IconMoodSad size={48} stroke={1.5} style={{ marginBottom: "1rem" }} />
<Text fz="lg" c="dimmed">
Data SDGs Desa belum tersedia
</Text>
</Center>
)}
<Center>
<Button
@@ -156,7 +149,19 @@ export default function SDGS() {
mt="md"
variant="gradient"
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>
</Button>

View File

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

View File

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