Compare commits

...

2 Commits

Author SHA1 Message Date
0befe6a3f2 QC Kak Inno 28 Okt
QC Kak Ayu 28 Okt
QC Keano 28 Okt
2025-10-30 15:51:12 +08:00
a6663bbcee QC Kak Inno 27 Oct
QC Kak Ayu 27 Oct
QC Keano 27 Oct
QC Pak Jun 27 Oct
2025-10-28 17:34:38 +08:00
49 changed files with 1602 additions and 851 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

@@ -2,7 +2,7 @@
'use client'
import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi';
import colors from '@/con/colors';
import { BackgroundImage, Box, Button, Center, Flex, Group, Paper, SimpleGrid, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { BackgroundImage, Box, Button, Center, Flex, Group, Paper, SimpleGrid, Skeleton, Stack, Text } from '@mantine/core';
import { IconEye } from '@tabler/icons-react';
import { useTransitionRouter } from 'next-view-transitions';
import { useEffect, useState } from 'react';
@@ -114,7 +114,6 @@ function Page() {
</Text>
</Box>
<Group justify="center">
<Tooltip label="Lihat detail potensi" withArrow>
<Button
radius="xl"
size="md"
@@ -126,7 +125,6 @@ function Page() {
>
Lihat Detail
</Button>
</Tooltip>
</Group>
</Stack>
</BackgroundImage>

View File

@@ -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

@@ -95,7 +95,7 @@ function ProfilPerbekel() {
<Box>
<Stack gap={6}>
<Stack align="center" gap={6}>
<IconUser size={22} color={colors['blue-button']} />
<IconUser size={22} />
<Text fz={{ base: "1.2rem", md: "1.5rem" }} fw="bold">Biodata</Text>
</Stack>
<Text
@@ -111,7 +111,7 @@ function ProfilPerbekel() {
<Box>
<Stack gap={6}>
<Stack align="center" gap={6}>
<IconBriefcase size={22} color={colors['blue-button']} />
<IconBriefcase size={22} />
<Text fz={{ base: "1.2rem", md: "1.5rem" }} fw="bold">Pengalaman</Text>
</Stack>
<Text
@@ -138,7 +138,7 @@ function ProfilPerbekel() {
<Stack gap="xl">
<Box>
<Stack align="center" gap={6} >
<IconUsers size={22} color={colors['blue-button']} />
<IconUsers size={22} />
<Text fz={{ base: "1.2rem", md: "1.5rem" }} fw="bold">Pengalaman Organisasi</Text>
</Stack>
<Text
@@ -152,7 +152,7 @@ function ProfilPerbekel() {
<Box>
<Stack align="center" gap={6} mb={6}>
<IconTargetArrow size={22} color={colors['blue-button']} />
<IconTargetArrow size={22} />
<Text fz={{ base: "1.2rem", md: "1.5rem" }} fw="bold">Program Kerja Unggulan</Text>
</Stack>
<Box px={10}>

View File

@@ -52,7 +52,7 @@ function Page() {
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }}>
<Box px={{ base: 'md', md: 100 }} pb={80}>
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
Lowongan Kerja Lokal
</Text>

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

@@ -55,6 +55,7 @@ function Page() {
}}
>
<Paper p={'xl'} >
<Stack gap={"xs"}>
<Text fz={'h3'} fw={'bold'} c={colors['blue-button']}>Tujuan Ide Inovatif Ini</Text>
<List>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Mendorong partisipasi aktif masyarakat</ListItem>
@@ -62,6 +63,7 @@ function Page() {
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Memecahkan tantangan komunal</ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Mengembangkan potensi kreativitas warga</ListItem>
</List>
</Stack>
</Paper>
<Paper p={'xl'} >
<Flex align={'center'} justify={'space-between'}>

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'}>
@@ -71,11 +72,22 @@ function Page() {
{filteredData.map((v, k) => {
return (
<Paper p={'xl'} key={k}>
<Image src={v.image.link || ''} pb={10} radius={10} alt='' loading="lazy"/>
<Text fz={'h3'} fw={'bold'} c={colors['blue-button']}>{v.name}</Text>
<Box pr={'lg'} pb={10}>
<Text fz={'h4'} fw={'bold'} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: v.deskripsi }} />
</Box>
<Stack gap={"xs"}>
<Image src={v.image.link || ''} pb={10} radius={10} alt='' loading="lazy" />
<Text fz={'h3'} fw={'bold'}>{v.name}</Text>
<Box pr={'lg'} pb={10}>
<Text
size="md"
ta="justify"
lh={1} // line height biar enak dibaca
style={{
wordBreak: "break-word",
whiteSpace: "normal",
}}
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
/>
</Box>
</Stack>
</Paper>
)
})}

View File

@@ -75,18 +75,18 @@ function AdministrasiOnline() {
<Title order={3}>Ajukan Administrasi Online</Title>
<TextInput
label={<Text fz="sm" fw="bold">Nama</Text>}
placeholder="masukkan nama"
placeholder="Masukkan nama"
onChange={(val) => (state.administrasiOnline.create.form.name = val.target.value)}
/>
<TextInput
label={<Text fz="sm" fw="bold">Alamat</Text>}
placeholder="masukkan alamat"
placeholder="Masukkan alamat"
onChange={(val) => (state.administrasiOnline.create.form.alamat = val.target.value)}
/>
<TextInput
type="number"
label={<Text fz="sm" fw="bold">Nomor Telepon</Text>}
placeholder="masukkan nomor telepon"
placeholder="Masukkan nomor telepon"
onChange={(val) => (state.administrasiOnline.create.form.nomorTelepon = val.target.value)}
/>
<Select
@@ -95,7 +95,7 @@ function AdministrasiOnline() {
state.administrasiOnline.create.form.jenisLayananId = val ?? "";
}}
label={<Text fw={"bold"} fz={"sm"}>Jenis Layanan</Text>}
placeholder="Pilih kategori produk"
placeholder="Pilih jenis layanan"
data={
state.jenisLayanan.findMany.data?.map((v) => ({
value: v.id,

View File

@@ -0,0 +1,85 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
import colors from '@/con/colors';
import { Box, Center, Container, Image, Skeleton, Stack, Text } from '@mantine/core';
import { useParams } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
function Page() {
const params = useParams<{ id: string }>();
const id = Array.isArray(params.id) ? params.id[0] : params.id;
const state = useProxy(stateDashboardBerita.berita)
const [loading, setLoading] = useState(true)
useEffect(() => {
const loadData = async () => {
if (!id) return;
try {
setLoading(true);
await state.findUnique.load(id);
} catch (error) {
console.error('Error loading data:', error);
} finally {
setLoading(false);
}
}
loadData()
}, [id])
if (loading) {
return (
<Center>
<Skeleton height={500} />
</Center>
);
}
if (!state.findUnique.data) {
return (
<Center>
<Text>Data tidak ditemukan</Text>
</Center>
);
}
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"} px={{ base: "md", md: 0 }}>
<Box px={{ base: "md", md: 100 }}>
<BackButton />
</Box>
<Container w={{ base: "100%", md: "50%" }} >
<Box pb={20}>
<Text ta={"center"} fz={"2.4rem"} c={colors["blue-button"]} fw={"bold"}>
{state.findUnique.data?.judul}
</Text>
</Box>
<Image src={state.findUnique.data?.image?.link || ''} alt='' w={"100%"} loading="lazy" />
</Container>
<Box px={{ base: "md", md: 100 }}>
<Stack gap={"xs"}>
<Text
py={20}
fz={{ base: "sm", md: "lg" }}
lh={{ base: 1.6, md: 1.8 }} // ✅ line-height lebih rapat dan responsif
ta="justify"
style={{
wordBreak: "break-word",
whiteSpace: "normal",
}}
dangerouslySetInnerHTML={{
__html: state.findUnique.data?.content || "",
}}
/>
</Stack>
</Box>
</Stack>
);
}
export default Page;

View File

@@ -0,0 +1,62 @@
'use client'
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
import colors from '@/con/colors';
import { Box, Container, Group, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useParams } from 'next/navigation';
import { useProxy } from 'valtio/utils';
function Page() {
const detail = useProxy(stateDesaPengumuman.pengumuman.findUnique)
const params = useParams()
useShallowEffect(() => {
stateDesaPengumuman.pengumuman.findUnique.load(params?.id as string)
}, [])
if (!detail.data) {
return (
<Box>
<Skeleton h={400} />
</Box>
)
}
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="md">
{/* Header */}
<Box px={{ base: "md", md: 100 }}>
<BackButton />
</Box>
<Container size="lg" px="md">
<Stack gap="xs" >
<Group justify={"space-between"} align={"center"}>
<Text fz={{ base: "2rem", md: "2rem" }} c={colors["blue-button"]} fw="bold" >
{detail.data?.judul}
</Text>
<Group justify='end'>
<Paper bg={colors['blue-button']} p={5}>
<Text c={colors['white-1']}>{detail.data?.CategoryPengumuman?.name}</Text>
</Paper>
</Group>
</Group>
<Paper bg={colors["white-1"]} p="md">
<Text fz={"md"} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: detail.data?.content }} />
<Text fz={"md"} c={colors["blue-button"]} fw="bold" >
{new Date(detail.data?.createdAt).toLocaleDateString('id-ID', {
weekday: 'long',
day: 'numeric',
month: 'long',
year: 'numeric'
})}
</Text>
</Paper>
</Stack>
</Container>
</Stack>
);
}
export default Page;

View File

@@ -1,20 +1,23 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
import colors from '@/con/colors';
import { Box, Card, Divider, Grid, GridCol, Image, Paper, Stack, Text, Title } from '@mantine/core';
import { Badge, Box, Card, Divider, Grid, GridCol, Group, Image, Paper, Stack, Text, Title } from '@mantine/core';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
import BackButton from '../../../desa/layanan/_com/BackButto';
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
import { useTransitionRouter } from 'next-view-transitions';
import { motion } from "framer-motion";
dayjs.extend(relativeTime);
function InformasiDesa() {
const stateBerita = useProxy(stateDashboardBerita.berita)
const statePengumuman = useProxy(stateDesaPengumuman.pengumuman)
const router = useTransitionRouter()
const stateBerita = useProxy(stateDashboardBerita.berita);
const statePengumuman = useProxy(stateDesaPengumuman.pengumuman);
useEffect(() => {
stateBerita.findFirst.load();
@@ -23,116 +26,216 @@ function InformasiDesa() {
statePengumuman.findRecent.load();
}, []);
const dataBerita = stateBerita.findFirst.data
const dataPengumuman = statePengumuman.findFirst.data
const dataBerita = stateBerita.findFirst.data;
const dataPengumuman = statePengumuman.findFirst.data;
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 px={{ base: 'md', md: 100 }} >
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
<Box px={{ base: 'md', md: 100 }}>
<Title ta="center" fz={{ base: 'h1', md: '2.5rem' }} c={colors['blue-button']} fw="bold">
Informasi Desa
</Text>
</Title>
</Box>
<Box px={{ base: "md", md: 100 }}>
<Stack gap={10}>
<Box px={{ base: 'md', md: 100 }}>
<Stack gap={30}>
{/* === BERITA UTAMA === */}
{dataBerita && (
<Paper shadow="md" radius="md" p="md">
<Grid>
<GridCol span={{ md: 6, base: 12 }}>
<Image
src={dataBerita.image?.link || "/fallback.jpg"}
alt={dataBerita.judul}
radius="md"
fit="cover"
height={250}
maw={600}
loading="lazy"
/>
</GridCol>
<GridCol span={{ md: 6, base: 12 }}>
<Box>
<Text fz="sm" c="dimmed">{dataBerita.kategoriBerita?.name} {dayjs(dataBerita.createdAt).fromNow()}</Text>
<Title order={1} fw="bold">{dataBerita.judul}</Title>
<Text ta={"justify"} mt="xs" fz="md" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: dataBerita.content }} />
</Box>
</GridCol>
</Grid>
</Paper>
<motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
style={{ cursor: "pointer" }}
onClick={() => router.push(`/darmasaba/inovasi/layanan-online-desa/informasi-desa/detail-berita/${dataBerita.id}`)}
>
<Paper shadow="md" radius="lg" p="lg" withBorder>
<Grid align="center" gutter="xl">
<GridCol span={{ base: 12, md: 6 }}>
<Image
src={dataBerita.image?.link || '/fallback.jpg'}
alt={dataBerita.judul}
radius="md"
fit="cover"
height={280}
loading="lazy"
style={{ objectPosition: 'center' }}
/>
</GridCol>
<GridCol span={{ base: 12, md: 6 }}>
<Stack gap="xs">
<Title order={2} fw={800}>
{dataBerita.judul}
</Title>
<Group justify='space-between'>
<Badge bg={colors['blue-button']}>
{dataBerita.kategoriBerita?.name}
</Badge>
<Text fz="sm" c="dimmed">
{dayjs(dataBerita.createdAt).fromNow()}
</Text>
</Group>
<Text
ta="justify"
mt="xs"
fz="md"
lh={1.7}
lineClamp={6}
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
dangerouslySetInnerHTML={{ __html: dataBerita.content }}
/>
</Stack>
</GridCol>
</Grid>
</Paper>
</motion.div>
)}
<Stack py={10}>
<Title order={3}>Berita Terbaru</Title>
<Grid>
{/* === BERITA TERBARU === */}
<Stack>
<Title order={3} fw={700}>
Berita Terbaru
</Title>
<Grid gutter="xl">
{stateBerita.findRecent.data.map((item) => (
<GridCol span={{ base: 12, sm: 6, md: 3 }} key={item.id}>
<Card shadow="sm" radius="md" withBorder h="100%">
<Card.Section>
<Image
src={item.image?.link || "/placeholder.jpg"}
alt={item.judul}
height={160} // gambar fix height
fit="cover"
loading="lazy"
/>
</Card.Section>
<Stack gap="xs" mt="sm">
<Text fw={600} lineClamp={2}>
{item.judul}
</Text>
<Text size="sm" color="dimmed" lineClamp={2}>
{item.deskripsi}
</Text>
<Text size="xs" c="gray">
{dayjs(item.createdAt).fromNow()}
</Text>
</Stack>
</Card>
<motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
style={{ cursor: "pointer" }}
onClick={() => router.push(`/darmasaba/inovasi/layanan-online-desa/informasi-desa/detail-berita/${item.id}`)}
>
<Card
shadow="sm"
radius="md"
withBorder
h="100%"
>
<Card.Section>
<Image
src={item.image?.link || '/placeholder.jpg'}
alt={item.judul}
height={160}
fit="cover"
radius="sm"
loading="lazy"
/>
</Card.Section>
<Stack gap={4} mt="sm">
<Text ta="justify"
size="sm"
c="dimmed"
lineClamp={3}
style={{
wordBreak: "break-word",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "normal",
lineHeight: 1.5,
display: "-webkit-box",
WebkitLineClamp: 3,
WebkitBoxOrient: "vertical",
}}
dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }} />
<Text size="xs" c="gray">
{dayjs(item.createdAt).fromNow()}
</Text>
</Stack>
</Card>
</motion.div>
</GridCol>
))}
</Grid>
</Stack>
<Divider color={colors['blue-button']} my="md" />
<Grid>
<GridCol span={{ md: 6, base: 12 }}>
<Divider color={colors['blue-button']} my="lg" />
{/* === PENGUMUMAN === */}
<Grid gutter="xl" align="stretch">
<GridCol span={{ base: 12, md: 6 }}>
{dataPengumuman && (
<Paper h={"97%"} shadow="md" radius="md" p="md">
<Stack gap={"xs"}>
<Title order={1} fw="bold">{dataPengumuman.judul}</Title>
<Text fz="sm" c="dimmed">{dataPengumuman.CategoryPengumuman?.name} {dayjs(dataPengumuman.createdAt).fromNow()}</Text>
<Box>
<Text ta={"justify"} mt="xs" fz="md" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: dataPengumuman.content }} />
</Box>
</Stack>
</Paper>
<motion.div
onClick={() => router.push(`/darmasaba/inovasi/layanan-online-desa/informasi-desa/detail-pengumuman/${dataPengumuman.id}`)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
style={{ cursor: "pointer" }}
>
<Paper shadow="md" radius="lg" p="lg" h="100%" withBorder>
<Stack gap="xs">
<Title order={2} fw={800}>
{dataPengumuman.judul}
</Title>
<Group justify='space-between'>
<Badge bg={colors['blue-button']}>
{dataPengumuman.CategoryPengumuman?.name}
</Badge>
<Text fz="sm" c="dimmed">
{dayjs(dataPengumuman.createdAt).fromNow()}
</Text>
</Group>
<Text
ta="justify"
mt="xs"
fz="md"
lh={1.7}
lineClamp={8}
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
dangerouslySetInnerHTML={{ __html: dataPengumuman.content }}
/>
</Stack>
</Paper>
</motion.div>
)}
</GridCol>
<GridCol span={{ md: 6, base: 12 }}>
<Stack py={10}>
<Title order={3}>Pengumuman Terbaru</Title>
<Grid>
<GridCol span={{ base: 12, md: 6 }}>
<Stack>
<Title order={3} fw={700}>
Pengumuman Terbaru
</Title>
<Grid gutter="lg">
{statePengumuman.findRecent.data.map((item) => (
<GridCol span={{ base: 12, sm: 8, md: 6 }} key={item.id}>
<Card shadow="sm" radius="md" withBorder h="100%">
<Stack gap="xs" mt="sm">
<Text fw={600} lineClamp={2}>
{item.judul}
</Text>
<Text size="sm" color="dimmed" lineClamp={2}>
{item.deskripsi}
</Text>
<Text size="xs" c="gray">
{dayjs(item.createdAt).fromNow()}
</Text>
</Stack>
</Card>
<GridCol span={{ base: 12, sm: 6 }} key={item.id}>
<motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
style={{ cursor: "pointer" }}
onClick={() => router.push(`/darmasaba/inovasi/layanan-online-desa/informasi-desa/detail-pengumuman/${item.id}`)}
>
<Card
shadow="xs"
radius="md"
withBorder
h="100%"
p="md"
style={{ transition: '0.2s ease' }}
className="hover:shadow-md"
>
<Stack gap="xs">
<Text fw={600} lineClamp={2}>
{item.judul}
</Text>
<Text
ta="justify"
mt="xs"
fz="md"
lh={1.7}
lineClamp={2}
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
dangerouslySetInnerHTML={{ __html: item.content }}
/>
<Text size="xs" c="gray">
{dayjs(item.createdAt).fromNow()}
</Text>
</Stack>
</Card>
</motion.div>
</GridCol>
))}
</Grid>
</Stack>
</GridCol>
</Grid>

View File

@@ -100,33 +100,33 @@ function PengaduanMasyarakat() {
<Title order={3}>Ajukan Pengaduan Masyarakat</Title>
<TextInput
label={<Text fz="sm" fw="bold">Nama</Text>}
placeholder="masukkan nama"
placeholder="Masukkan nama"
onChange={(val) => (state.pengaduanMasyarakat.create.form.name = val.target.value)}
/>
<TextInput
label={<Text fz="sm" fw="bold">Email</Text>}
placeholder="masukkan email"
placeholder="Masukkan email"
onChange={(val) => (state.pengaduanMasyarakat.create.form.email = val.target.value)}
/>
<TextInput
type="number"
label={<Text fz="sm" fw="bold">Nomor Telepon</Text>}
placeholder="masukkan nomor telepon"
placeholder="Masukkan nomor telepon"
onChange={(val) => (state.pengaduanMasyarakat.create.form.nomorTelepon = val.target.value)}
/>
<TextInput
label={<Text fz="sm" fw="bold">NIK</Text>}
placeholder="masukkan nik"
placeholder="Masukkan nik"
onChange={(val) => (state.pengaduanMasyarakat.create.form.nik = val.target.value)}
/>
<TextInput
label={<Text fz="sm" fw="bold">Judul Pengaduan</Text>}
placeholder="masukkan judul pengaduan"
placeholder="Masukkan judul pengaduan"
onChange={(val) => (state.pengaduanMasyarakat.create.form.judulPengaduan = val.target.value)}
/>
<TextInput
label={<Text fz="sm" fw="bold">Lokasi Kejadian</Text>}
placeholder="masukkan lokasi kejadian"
placeholder="Masukkan lokasi kejadian"
onChange={(val) => (state.pengaduanMasyarakat.create.form.lokasiKejadian = val.target.value)}
/>
<Box>
@@ -144,7 +144,7 @@ function PengaduanMasyarakat() {
state.pengaduanMasyarakat.create.form.jenisPengaduanId = val ?? "";
}}
label={<Text fw={"bold"} fz={"sm"}>Jenis Pengaduan</Text>}
placeholder="Pilih kategori produk"
placeholder="Pilih jenis pengaduan"
data={
state.jenisPengaduan.findMany.data?.map((v) => ({
value: v.id,

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

@@ -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

@@ -54,14 +54,14 @@ function ArtikelKesehatanPage() {
<Image style={{ borderTopLeftRadius: '10px', borderTopRightRadius: '10px' }} src={item.image?.link} alt={item.title} height={200} fit="cover" loading="lazy" />
</Card.Section>
<Stack gap="xs" mt="md">
<Text fw="bold" fz="xl" c="dark">{item.title}</Text>
<Text fw="bold" fz="xl" c={colors['blue-button']}>{item.title}</Text>
<Group gap="xs">
<IconCalendar size={16} color={colors['blue-button']} />
<IconCalendar size={16} color='gray' />
<Text fz="sm" c="dimmed">
{new Date(item.createdAt).toLocaleDateString('id-ID', { year: 'numeric', month: 'long', day: 'numeric' })} Dinas Kesehatan
</Text>
</Group>
<Text fz="md" c="dark" lineClamp={3}>
<Text fz="md" lineClamp={3}>
{item.content}
</Text>
<Group justify="flex-start">

View File

@@ -73,14 +73,14 @@ function FasilitasKesehatanPage() {
</Badge>
</Group>
<Group gap="xs">
<IconMapPin size={18} stroke={1.5} color={colors['blue-button']} />
<Text fz="sm" c="dimmed">
<IconMapPin size={18} stroke={1.5} />
<Text fz="sm">
{item.informasiumum.alamat}
</Text>
</Group>
<Group gap="xs">
<IconClock size={18} stroke={1.5} color={colors['blue-button']} />
<Text fz="sm" c="dimmed">
<IconClock size={18} stroke={1.5} />
<Text fz="sm">
{item.informasiumum.jamOperasional}
</Text>
</Group>

View File

@@ -49,7 +49,7 @@ function JadwalKegiatanPage() {
>
<Stack gap="sm">
<Group justify="space-between">
<Text fw={700} fz="xl">
<Text fw={700} fz="xl" c={colors['blue-button']}>
{item.informasijadwalkegiatan.name}
</Text>
<Text fw={600} fz="sm" c={colors['blue-button']}>
@@ -62,12 +62,12 @@ function JadwalKegiatanPage() {
</Group>
<Group gap="xs">
<IconClockHour4 size={18} color={colors['blue-button']} />
<IconClockHour4 size={18} />
<Text fz="sm">{item.informasijadwalkegiatan.waktu}</Text>
</Group>
<Group gap="xs">
<IconMapPin size={18} color={colors['blue-button']} />
<IconMapPin size={18} />
<Text fz="sm">{item.informasijadwalkegiatan.lokasi}</Text>
</Group>

View File

@@ -73,7 +73,6 @@ function DetailInfoWabahPenyakitUser() {
<Text fz="lg" fw="bold">Deskripsi</Text>
<Text
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{ __html: data.deskripsiLengkap || '-' }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/>

View File

@@ -53,7 +53,7 @@ function Page() {
<Text fz={{ base: '2rem', md: '2.8rem' }} c={colors['blue-button']} fw={800}>
Kontak Darurat
</Text>
<Text c="dimmed" fz="md" mt={4}>
<Text fz="md" mt={4}>
Hubungi layanan penting dengan cepat dan mudah
</Text>
</GridCol>
@@ -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>
<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>
<Text ta="center" fw={700} fz="lg" c={colors['blue-button']}>
{v.name}
</Text>
<Text fz="sm" c="dimmed" 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>
))}
</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

@@ -52,7 +52,7 @@ function Page() {
<Text fz={{ base: 30, md: 40 }} c={colors['blue-button']} fw={800} lh={1.2}>
Penanganan Darurat
</Text>
<Text fz="md" c="dimmed" mt={4}>
<Text fz="md" mt={4}>
Informasi cepat dan jelas untuk situasi darurat kesehatan
</Text>
</GridCol>
@@ -105,36 +105,28 @@ function Page() {
onMouseLeave={(e) => (e.currentTarget.style.transform = 'translateY(0)')}
>
<Stack align="center" gap="md">
<Center>
<Box
<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: 180, // 🔥 tinggi fix biar semua seragam
borderRadius: 12,
overflow: 'hidden',
position: 'relative',
backgroundColor: '#f0f2f5', // fallback kalau gambar loading
height: '100%',
transition: 'transform 0.4s ease',
}}
>
<Image
src={v.image?.link || '/img/default.png'}
alt={v.name}
fit="cover"
width="100%"
height="100%"
loading="lazy"
style={{
objectFit: 'cover',
objectPosition: 'center',
transition: 'transform 0.4s ease',
}}
onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.05)')}
onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')}
/>
</Box>
/>
</Box>
</Center>
<Stack gap={4} w="100%">
<Text
fz="lg"
@@ -147,8 +139,7 @@ function Page() {
</Text>
<Box>
<Text
fz="sm"
c="dimmed"
fz="md"
lineClamp={3}
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
@@ -159,6 +150,8 @@ function Page() {
size="md"
component="a"
href={`/darmasaba/kesehatan/penanganan-darurat/${v.id}`}
bg={colors['blue-button']}
c="white"
>
Lihat Detail
</Button>

View File

@@ -28,10 +28,11 @@ function Page() {
}
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="xl">
<Stack bg={colors.Bg} py="xl" gap="xl">
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }}>
<Paper
px={{ base: 'md', md: 100 }}
py="xl"
@@ -70,7 +71,7 @@ function Page() {
<Group gap="xl">
<Group gap="xs">
<Tooltip label="Tanggal dibuat" withArrow>
<IconCalendar size={20} stroke={1.5} />
<IconCalendar color='gray' size={20} stroke={1.5} />
</Tooltip>
<Text size="sm" c="dimmed">
{state.findUnique.data.createdAt
@@ -84,13 +85,14 @@ function Page() {
</Group>
<Group gap="xs">
<Tooltip label="Dibuat oleh" withArrow>
<IconUser size={20} stroke={1.5} />
<IconUser color='gray' size={20} stroke={1.5} />
</Tooltip>
<Text size="sm" c="dimmed">Admin Desa</Text>
</Group>
</Group>
</Stack>
</Paper>
</Box>
</Stack>
);
}

View File

@@ -1,4 +1,5 @@
'use client'
import programKesehatan from "@/app/admin/(dashboard)/_state/kesehatan/program-kesehatan/programKesehatan";
import colors from "@/con/colors";
import {
Box,
@@ -15,9 +16,9 @@ import {
Stack,
Text,
TextInput,
Tooltip,
Transition,
Transition
} from "@mantine/core";
import { useDebouncedValue, useShallowEffect } from "@mantine/hooks";
import {
IconBarbell,
IconCalendar,
@@ -26,12 +27,10 @@ import {
IconUser,
IconUsersGroup,
} from "@tabler/icons-react";
import BackButton from "../../desa/layanan/_com/BackButto";
import { useProxy } from "valtio/utils";
import programKesehatan from "@/app/admin/(dashboard)/_state/kesehatan/program-kesehatan/programKesehatan";
import { useState } from "react";
import { useDebouncedValue, useShallowEffect } from "@mantine/hooks";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useProxy } from "valtio/utils";
import BackButton from "../../desa/layanan/_com/BackButto";
const manfaatProgram = [
{
@@ -194,7 +193,6 @@ export default function Page() {
<Text size="sm">Admin Desa</Text>
</Group>
</Group>
<Tooltip label="Lihat detail program" withArrow>
<Button
mt="lg"
fullWidth
@@ -211,7 +209,6 @@ export default function Page() {
>
Lihat Detail
</Button>
</Tooltip>
</Box>
</Stack>
</Paper>

View File

@@ -93,20 +93,23 @@ function Page() {
<Text fw={600} fz="lg" lineClamp={1}>{v.name}</Text>
<Badge color="blue" variant="light" radius="sm" fz="xs">Aktif</Badge>
</Group>
<Stack gap={4}>
<Group gap="xs">
<IconMapPin size={16} />
<Text fz="sm" c="dimmed" lineClamp={2}>{v.alamat}</Text>
<Stack gap={6}>
<Group gap="xs" align="flex-start" wrap="nowrap">
<Box pt={2}><IconMapPin size={16} /></Box>
<Text fz="sm" c="dimmed">{v.alamat}</Text>
</Group>
<Group gap="xs">
<IconPhone size={16} />
<Group gap="xs" align="flex-start" wrap="nowrap">
<Box pt={2}><IconPhone size={16} /></Box>
<Text fz="sm" c="dimmed">{v.kontak.kontakPuskesmas}</Text>
</Group>
<Group gap="xs">
<IconMail size={16} />
<Group gap="xs" align="flex-start" wrap="nowrap">
<Box pt={2}><IconMail size={16} /></Box>
<Text fz="sm" c="dimmed">{v.kontak.email}</Text>
</Group>
</Stack>
<Anchor
href={`/darmasaba/kesehatan/puskesmas/${v.id}`}
fz="sm"

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}
@@ -45,10 +44,8 @@ export function EdukasiCard({ icon, title, description, color = '#1e88e5' }: Edu
alignItems: 'center',
justifyContent: 'center'
}}
>
{title}
</Text>
</Tooltip>
dangerouslySetInnerHTML={{ __html: title }}
/>
</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

@@ -1,13 +1,13 @@
'use client'
import beasiswaDesaState from '@/app/admin/(dashboard)/_state/pendidikan/beasiswa-desa';
import colors from '@/con/colors';
import { Box, Button, Center, Group, Image, Modal, Pagination, Paper, Select, SimpleGrid, Skeleton, Stack, Stepper, StepperStep, Text, TextInput, Title } from '@mantine/core';
import { Box, Button, Center, Group, Image, Modal, Pagination, Paper, Select, SimpleGrid, Skeleton, Stack, Stepper, Text, TextInput, Title } from '@mantine/core';
import { useDisclosure, useShallowEffect } from '@mantine/hooks';
import { IconArrowRight, IconCoin, IconInfoCircle, IconSchool, IconUsers } from '@tabler/icons-react';
import { useTransitionRouter } from 'next-view-transitions';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto';
import { useTransitionRouter } from 'next-view-transitions';
const dataBeasiswa = [
{ id: 1, nama: 'Penerima Beasiswa', jumlah: '250+', icon: IconUsers },
@@ -27,7 +27,7 @@ function Page() {
tempatLahir: "",
tanggalLahir: "",
jenisKelamin: "",
kewarganegaraan: "",
kewarganegaraan: "WNI",
agama: "",
alamatKTP: "",
alamatDomisili: "",
@@ -50,9 +50,21 @@ function Page() {
close();
};
const timeline = [
{ label: "1 Maret 2025", desc: "Pembukaan Pendaftaran", date: new Date("2025-03-01") },
{ label: "15 Maret 2025", desc: "Seleksi Administrasi", date: new Date("2025-03-15") },
{ label: "1 April 2025", desc: "Tes Potensi Akademik", date: new Date("2025-04-01") },
{ label: "15 April 2025", desc: "Wawancara", date: new Date("2025-04-15") },
{ label: "1 Mei 2025", desc: "Pengumuman Hasil", date: new Date("2025-05-01") },
];
const [active, setActive] = useState(1);
const nextStep = () => setActive((current) => (current < 5 ? current + 1 : current));
const prevStep = () => setActive((current) => (current > 0 ? current - 1 : current));
useShallowEffect(() => {
const today = new Date();
// cari berapa banyak tanggal yang sudah lewat
const doneSteps = timeline.filter(item => today >= item.date).length;
setActive(doneSteps); // active step diset sesuai tanggal
}, []);
if (loading || !data) {
return (
@@ -115,7 +127,7 @@ function Page() {
{data.map((v, k) => (
<Paper key={k} p="xl" radius="xl" shadow="sm" bg={colors['white-trans-1']}>
<Title order={3} fw={700} c={colors['blue-button']} mb="xs">{v.judul}</Title>
<Text fz="sm" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: v.deskripsi }}/>
<Text fz="sm" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: v.deskripsi }} />
</Paper>
))}
</SimpleGrid>
@@ -139,19 +151,22 @@ function Page() {
Timeline Pendaftaran
</Title>
<Center>
<Stepper mt={20} active={active} onStepClick={setActive} orientation="vertical" allowNextStepsSelect={false}>
<StepperStep label="1 Maret 2025" description="Pembukaan Pendaftaran" />
<StepperStep label="15 Maret 2025" description="Seleksi Administrasi" />
<StepperStep label="1 April 2025" description="Tes Potensi Akademik" />
<StepperStep label="15 April 2025" description="Wawancara" />
<StepperStep label="1 Mei 2025" description="Pengumuman Hasil" />
<Stepper
mt={20}
active={active}
onStepClick={setActive}
orientation="vertical"
allowNextStepsSelect={false}
>
{timeline.map((item, index) => (
<Stepper.Step
key={index}
label={item.label}
description={item.desc}
/>
))}
</Stepper>
</Center>
<Group justify="center" mt="xl">
<Button variant="default" radius="xl" onClick={prevStep}>Kembali</Button>
<Button radius="xl" bg={colors['blue-button']} onClick={nextStep}>Lanjut</Button>
</Group>
</Box>
<Modal
@@ -194,7 +209,11 @@ function Page() {
<TextInput
label="Kewarganegaraan"
placeholder="Masukkan kewarganegaraan"
onChange={(val) => { beasiswaDesa.create.form.kewarganegaraan = val.target.value }} />
value={beasiswaDesa.create.form.kewarganegaraan || "WNI"} // tampilkan WNI kalau kosong
onChange={(e) => {
beasiswaDesa.create.form.kewarganegaraan = e.target.value;
}}
/>
<Select
label="Agama"
placeholder="Pilih agama"

View File

@@ -34,7 +34,7 @@ export default function BeasiswaPage() {
tempatLahir: "",
tanggalLahir: "",
jenisKelamin: "",
kewarganegaraan: "",
kewarganegaraan: "WNI",
agama: "",
alamatKTP: "",
alamatDomisili: "",
@@ -71,7 +71,7 @@ export default function BeasiswaPage() {
<Stack gap="md" maw={600}>
<Group>
<IconSchool size={30} color={colors["blue-button"]} />
<Title order={2}>Program Beasiswa Pendidikan Desa Darmasaba</Title>
<Title order={2} c={colors["blue-button"]}>Program Beasiswa Pendidikan Desa Darmasaba</Title>
</Group>
<Text>
Program ini bertujuan untuk mendukung pendidikan generasi muda di Desa Darmasaba
@@ -84,7 +84,7 @@ export default function BeasiswaPage() {
<Container size="lg" py="xl">
<Group mb="sm">
<IconInfoCircle size={24} color={colors["blue-button"]} />
<Title order={3}>Tentang Program</Title>
<Title order={3} c={colors["blue-button"]}>Tentang Program</Title>
</Group>
<Text>
Program Beasiswa Desa Darmasaba adalah inisiatif pemerintah desa untuk meningkatkan akses
@@ -94,12 +94,11 @@ export default function BeasiswaPage() {
{/* Tambahkan info tahun berjalan di sini */}
<Paper mt="md" p="md" radius="lg" shadow="xs" bg="#f8fbff" withBorder>
<Text fw={500} c={colors["blue-button"]}>
📅 Periode Beasiswa Tahun 2025
<Text fw={500}>
Periode Beasiswa Tahun 2025
</Text>
<Text fz="sm" c="dimmed">
Pendaftaran beasiswa dibuka mulai <strong>1 Januari 2025</strong> dan ditutup pada
<strong>31 Mei 2025</strong>.
Pendaftaran beasiswa dibuka mulai <strong>1 Januari 2025</strong> dan ditutup pada <strong>31 Mei 2025</strong>.
Pengumuman hasil seleksi akan diumumkan pada pertengahan Juni 2025 melalui website resmi Desa Darmasaba.
</Text>
</Paper>
@@ -109,7 +108,7 @@ export default function BeasiswaPage() {
<Container size="lg" py="xl">
<Group mb="sm">
<IconChecklist size={24} color={colors["blue-button"]} />
<Title order={3}>Syarat Pendaftaran</Title>
<Title order={3} c={colors["blue-button"]}>Syarat Pendaftaran</Title>
</Group>
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="lg">
@@ -140,7 +139,7 @@ export default function BeasiswaPage() {
<Container size="lg" py="xl">
<Group mb="sm">
<IconTimeline size={24} color={colors["blue-button"]} />
<Title order={3}>Proses Seleksi</Title>
<Title order={3} c={colors["blue-button"]}>Proses Seleksi</Title>
</Group>
<Timeline active={4} bulletSize={24} lineWidth={2}>
@@ -148,8 +147,8 @@ export default function BeasiswaPage() {
<Text c="dimmed" size="sm">
Calon peserta mengisi formulir pendaftaran dan mengunggah dokumen pendukung.
</Text>
<Text size="sm" fw={500} c={colors["blue-button"]} mt={4}>
Estimasi waktu: 1 Februari 31 Mei 2025
<Text size="sm" fw={500} mt={4}>
Estimasi waktu: 1 Februari 31 Mei 2025
</Text>
</Timeline.Item>
@@ -157,8 +156,8 @@ export default function BeasiswaPage() {
<Text c="dimmed" size="sm">
Panitia memverifikasi kelengkapan dan validitas berkas.
</Text>
<Text size="sm" fw={500} c={colors["blue-button"]} mt={4}>
Estimasi waktu: 57 hari kerja setelah penutupan pendaftaran
<Text size="sm" fw={500} mt={4}>
Estimasi waktu: 57 hari kerja setelah penutupan pendaftaran
</Text>
</Timeline.Item>
@@ -166,8 +165,8 @@ export default function BeasiswaPage() {
<Text c="dimmed" size="sm">
Peserta yang lolos administrasi akan diundang untuk wawancara langsung dengan tim seleksi.
</Text>
<Text size="sm" fw={500} c={colors["blue-button"]} mt={4}>
Estimasi waktu: 710 hari kerja setelah pengumuman seleksi administrasi
<Text size="sm" fw={500} mt={4}>
Estimasi waktu: 710 hari kerja setelah pengumuman seleksi administrasi
</Text>
</Timeline.Item>
@@ -175,14 +174,14 @@ export default function BeasiswaPage() {
<Text c="dimmed" size="sm">
Daftar penerima beasiswa diumumkan melalui website resmi Desa Darmasaba.
</Text>
<Text size="sm" fw={500} c={colors["blue-button"]} mt={4}>
Estimasi waktu: 5 hari kerja setelah tahap wawancara selesai
<Text size="sm" fw={500} mt={4}>
Estimasi waktu: 5 hari kerja setelah tahap wawancara selesai
</Text>
</Timeline.Item>
</Timeline>
<Text c="dimmed" size="sm" mt="lg" ta="center">
🗓 Total estimasi keseluruhan proses: sekitar 34 minggu setelah penutupan pendaftaran
Total estimasi keseluruhan proses: sekitar 34 minggu setelah penutupan pendaftaran
</Text>
</Container>
@@ -191,7 +190,7 @@ export default function BeasiswaPage() {
<Container size="lg" py="xl">
<Group mb="sm">
<IconQuote size={24} color={colors["blue-button"]} />
<Title order={3}>Cerita Sukses Penerima Beasiswa</Title>
<Title order={3} c={colors["blue-button"]}>Cerita Sukses Penerima Beasiswa</Title>
</Group>
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="lg">
@@ -219,12 +218,12 @@ export default function BeasiswaPage() {
<Container size="lg" py="xl" ta="center">
<Group justify="center" mb="sm">
<IconUserPlus size={28} color={colors["blue-button"]} />
<Title order={3}>Siap Bergabung dengan Program Ini?</Title>
<Title order={3} c={colors["blue-button"]}>Siap Bergabung dengan Program Ini?</Title>
</Group>
<Text c="dimmed" mb="md">
Segera daftar dan wujudkan mimpimu bersama Desa Darmasaba.
</Text>
<Button onClick={open} size="lg" radius="xl" color="blue">
<Button onClick={open} size="lg" radius="xl" bg={colors["blue-button"]}>
Daftar Sekarang
</Button>
</Container>
@@ -269,7 +268,11 @@ export default function BeasiswaPage() {
<TextInput
label="Kewarganegaraan"
placeholder="Masukkan kewarganegaraan"
onChange={(val) => { beasiswaDesa.create.form.kewarganegaraan = val.target.value }} />
value={beasiswaDesa.create.form.kewarganegaraan || "WNI"} // tampilkan WNI kalau kosong
onChange={(e) => {
beasiswaDesa.create.form.kewarganegaraan = e.target.value;
}}
/>
<Select
label="Agama"
placeholder="Pilih agama"

View File

@@ -1,10 +1,10 @@
'use client'
import stateBimbinganBelajarDesa from '@/app/admin/(dashboard)/_state/pendidikan/bimbingan-belajar-desa';
import colors from '@/con/colors';
import { Box, Paper, SimpleGrid, Skeleton, Stack, Text, Title, Tooltip, Divider, Badge, Group } from '@mantine/core';
import { Box, Divider, Group, Paper, SimpleGrid, Skeleton, Stack, Text, Title, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconBook2, IconCalendarTime, IconMapPin } from '@tabler/icons-react';
import { useProxy } from 'valtio/utils';
import { IconMapPin, IconCalendarTime, IconBook2 } from '@tabler/icons-react';
import BackButton from '../../desa/layanan/_com/BackButto';
function Page() {
@@ -55,9 +55,9 @@ function Page() {
<IconBook2 size={36} stroke={1.5} color={colors['blue-button']} />
</Box>
</Tooltip>
<Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
<Title order={3} fw={700} c={colors['blue-button']} mb="xs">
{stateTujuanProgram.findById.data?.judul}
</Badge>
</Title>
</Group>
<Text fz="md" style={{wordBreak: "break-word", whiteSpace: "normal"}} lh={1.6} dangerouslySetInnerHTML={{ __html: stateTujuanProgram.findById.data?.deskripsi }} />
</Stack>
@@ -70,9 +70,9 @@ function Page() {
<IconMapPin size={36} stroke={1.5} color={colors['blue-button']} />
</Box>
</Tooltip>
<Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
<Title order={3} fw={700} c={colors['blue-button']} mb="xs">
{stateLokasiDanJadwal.findById.data?.judul}
</Badge>
</Title>
</Group>
<Text fz="md" lh={1.6} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: stateLokasiDanJadwal.findById.data?.deskripsi }} />
</Stack>
@@ -85,9 +85,9 @@ function Page() {
<IconCalendarTime size={36} stroke={1.5} color={colors['blue-button']} />
</Box>
</Tooltip>
<Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
<Title order={3} fw={700} c={colors['blue-button']} mb="xs">
{stateFasilitas.findById.data?.judul}
</Badge>
</Title>
</Group>
<Text fz="md" lh={1.6} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: stateFasilitas.findById.data?.deskripsi }} />
</Stack>

View File

@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client';
import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud';
import colors from '@/con/colors';
import { Box, Button, Center, Container, Group, Paper, SimpleGrid, Skeleton, Stack, Text, Tooltip, ActionIcon } from '@mantine/core';
import { IconChalkboard, IconMicroscope, IconProps, IconRefresh, IconSchool, IconInfoCircle } from '@tabler/icons-react';
import { motion } from 'framer-motion';
@@ -119,11 +120,15 @@ export default function KategoriPage({ jenjangPendidikan }: { jenjangPendidikan:
</Text>
</Box>
<Button
leftSection={<IconRefresh size={16} />}
leftSection={<IconRefresh color={colors['blue-button']} size={16} />}
variant="outline"
size="xs"
onClick={handleRefresh}
loading={stats.some(stat => stat.loading)}
c={colors['blue-button']}
style={{
borderColor: colors['blue-button'],
}}
>
Segarkan Data
</Button>
@@ -143,7 +148,7 @@ export default function KategoriPage({ jenjangPendidikan }: { jenjangPendidikan:
aria-label="Tidak ada hasil"
>
<Center style={{ minHeight: 180, flexDirection: 'column' }}>
<Text fz="lg" fw={800} c="#2563eb">
<Text fz="lg" fw={800} c={colors['blue-button']}>
Tidak ditemukan
</Text>
<Text c="dimmed" mt="6px">
@@ -173,81 +178,81 @@ export default function KategoriPage({ jenjangPendidikan }: { jenjangPendidikan:
style={{ width: '100%' }}
>
<Skeleton visible={v.loading}>
<Paper
p="lg"
radius="lg"
style={{
background: 'white',
border: '1px solid #e2e8f0',
boxShadow: '0 8px 28px rgba(0,0,0,0.06)',
minHeight: 260,
}}
role="article"
aria-label={`${v.nama} kartu statistik`}
>
<Stack gap="sm" mb="md">
<Center>
<Box
style={{
width: 80,
height: 80,
borderRadius: 16,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: '#eff6ff',
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.6)',
}}
aria-hidden
>
{React.createElement(v.icon, {
color: '#2563eb',
size: 34,
stroke: 1.6,
})}
</Box>
</Center>
<Paper
p="lg"
radius="lg"
style={{
background: 'white',
border: '1px solid #e2e8f0',
boxShadow: '0 8px 28px rgba(0,0,0,0.06)',
minHeight: 260,
}}
role="article"
aria-label={`${v.nama} kartu statistik`}
>
<Stack gap="sm" mb="md">
<Center>
<Box
style={{
width: 80,
height: 80,
borderRadius: 16,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: '#eff6ff',
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.6)',
}}
aria-hidden
>
{React.createElement(v.icon, {
color: colors['blue-button'],
size: 34,
stroke: 1.6,
})}
</Box>
</Center>
<Group justify="center" align="center" gap="xs">
<Stack gap={0}>
<Text ta={"center"} fz={{ base: 18, md: 22 }} fw={800} c="#0f172a">
{v.jumlah.toLocaleString()}
</Text>
<Group gap={6} align="center">
<Text ta={"center"} fz="sm" fw={700} c="#2563eb">
{v.nama}
<Group justify="center" align="center" gap="xs">
<Stack gap={0}>
<Text ta={"center"} fz={{ base: 18, md: 22 }} fw={800} c="#0f172a">
{v.jumlah.toLocaleString()}
</Text>
<Tooltip label={v.helper ?? ''} position="right" withArrow>
<ActionIcon aria-label={`Info ${v.nama}`} variant="transparent" size="xs">
<IconInfoCircle size={16} style={{ color: '#2563eb' }} />
</ActionIcon>
</Tooltip>
</Group>
</Stack>
</Group>
</Stack>
<Group gap={6} align="center">
<Text ta={"center"} fz="sm" fw={700} c={colors['blue-button']}>
{v.nama}
</Text>
<Tooltip label={v.helper ?? ''} position="right" withArrow>
<ActionIcon aria-label={`Info ${v.nama}`} variant="transparent" size="xs">
<IconInfoCircle size={16} style={{ color: colors['blue-button'] }} />
</ActionIcon>
</Tooltip>
</Group>
</Stack>
</Group>
</Stack>
<Group justify="center" mt="8px">
<Button
radius="xl"
variant="outline"
aria-label={`Lihat detail ${v.nama}`}
style={{
borderColor: '#e2e8f0',
color: '#2563eb',
paddingLeft: 20,
paddingRight: 20,
}}
onClick={() => {
if (v.nama === "Lembaga Pendidikan") router.push(`/darmasaba/pendidikan/info-sekolah/${jenjangPendidikan}/lembaga`);
if (v.nama === "Siswa Terdaftar") router.push(`/darmasaba/pendidikan/info-sekolah/${jenjangPendidikan}/siswa`);
if (v.nama === "Tenaga Pengajar") router.push(`/darmasaba/pendidikan/info-sekolah/${jenjangPendidikan}/pengajar`);
}}
>
Lihat Detail
</Button>
</Group>
</Paper>
<Group justify="center" mt="8px">
<Button
radius="xl"
variant="outline"
aria-label={`Lihat detail ${v.nama}`}
style={{
borderColor: colors['blue-button'],
color: colors['blue-button'],
paddingLeft: 20,
paddingRight: 20,
}}
onClick={() => {
if (v.nama === "Lembaga Pendidikan") router.push(`/darmasaba/pendidikan/info-sekolah/${jenjangPendidikan}/lembaga`);
if (v.nama === "Siswa Terdaftar") router.push(`/darmasaba/pendidikan/info-sekolah/${jenjangPendidikan}/siswa`);
if (v.nama === "Tenaga Pengajar") router.push(`/darmasaba/pendidikan/info-sekolah/${jenjangPendidikan}/pengajar`);
}}
>
Lihat Detail
</Button>
</Group>
</Paper>
</Skeleton>
</motion.div>
))

View File

@@ -185,6 +185,8 @@ export default function LayoutSekolah({
radius="xl"
size="sm"
variant={aktif ? 'filled' : 'light'}
bg={colors['blue-button']}
c={aktif ? colors['white-1'] : 'gray'}
>
{k}
</Button>

View File

@@ -1,6 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud';
import colors from '@/con/colors';
import {
ActionIcon,
Box,
@@ -130,11 +131,15 @@ export default function SekolahPage() {
<Box>
<Group justify="start" mb="md">
<Button
leftSection={<IconRefresh size={16} />}
leftSection={<IconRefresh color={colors['blue-button']} size={16} />}
variant="outline"
size="xs"
onClick={handleRefresh}
loading={stats.some(stat => stat.loading)}
c={colors['blue-button']}
style={{
borderColor: colors['blue-button'],
}}
>
Segarkan Data
</Button>
@@ -154,7 +159,7 @@ export default function SekolahPage() {
aria-label="Tidak ada hasil"
>
<Center style={{ minHeight: 180, flexDirection: 'column' }}>
<Text fz="lg" fw={800} c="#2563eb">
<Text fz="lg" fw={800} c={colors['blue-button']}>
Tidak ditemukan
</Text>
<Text c="dimmed" mt="6px">
@@ -212,7 +217,7 @@ export default function SekolahPage() {
aria-hidden
>
{React.createElement(v.icon, {
color: '#2563eb',
color: colors['blue-button'],
size: 34,
stroke: 1.6,
})}
@@ -225,12 +230,12 @@ export default function SekolahPage() {
{v.jumlah.toLocaleString()}
</Text>
<Group gap={6} align="center">
<Text ta={"center"} fz="sm" fw={700} c="#2563eb">
<Text ta={"center"} fz="sm" fw={700} c={colors['blue-button']}>
{v.nama}
</Text>
<Tooltip label={v.helper ?? ''} position="right" withArrow>
<ActionIcon aria-label={`Info ${v.nama}`} variant="transparent" size="xs">
<IconInfoCircle size={16} style={{ color: '#2563eb' }} />
<IconInfoCircle size={16} style={{ color: colors['blue-button'] }} />
</ActionIcon>
</Tooltip>
</Group>
@@ -244,8 +249,8 @@ export default function SekolahPage() {
variant="outline"
aria-label={`Lihat detail ${v.nama}`}
style={{
borderColor: '#e2e8f0',
color: '#2563eb',
borderColor: colors['blue-button'],
color: colors['blue-button'],
paddingLeft: 20,
paddingRight: 20,
}}

View File

@@ -68,16 +68,16 @@ 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">
<Paper p="lg" radius="xl" shadow="sm" withBorder>
<Stack gap="sm">
<Text fz={{ base: 'lg', md: 'xl' }} fw="bold" c={colors["blue-button"]}>
<Text ta={"center"} fz={{ base: 'lg', md: 'xl' }} fw="bold" c={colors["blue-button"]}>
Tentang Informasi Publik
</Text>
<Text fz={{ base: 'sm', md: 'md' }} c="dimmed">
<Text ta={"center"} fz={{ base: 'sm', md: 'md' }} c="dimmed">
Daftar Informasi Publik Desa Darmasaba adalah kumpulan data yang dapat diakses oleh masyarakat sesuai dengan ketentuan peraturan yang berlaku.
</Text>
</Stack>
@@ -117,41 +117,45 @@ function Page() {
<TableTr key={item.id}>
<TableTd ta="center">{(page - 1) * 5 + index + 1}</TableTd>
<TableTd>
<Box w={150}>
<Box>
<Badge variant="light" size="lg" color="blue">
{item.jenisInformasi}
<Text fw={650} fz={"sm"} c={'blue'} lineClamp={1}>
{item.jenisInformasi}
</Text>
</Badge>
</Box>
</TableTd>
<TableTd>
<Box w={150}>
<Text lineClamp={1} fz="sm" c="dark" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
<Box>
<Text lineClamp={1} fz="sm" c="dark" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</Box>
</TableTd>
<TableTd ta="center">
<Box w={150}>
{item.tanggal ? new Date(item.tanggal).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric'
}) : '-'}
<Box>
<Text ta={"center"}>
{item.tanggal ? new Date(item.tanggal).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric'
}) : '-'}
</Text>
</Box>
</TableTd>
<TableTd style={{ textAlign: 'center' }}>
<Box w={150}>
<Box>
<Tooltip label="Lihat Detail" withArrow>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() => router.push(`/darmasaba/ppid/daftar-informasi-publik-desa-darmasaba/${item.id}`)}
>
Detail
</Button>
</Tooltip>
</Box>
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() => router.push(`/darmasaba/ppid/daftar-informasi-publik-desa-darmasaba/${item.id}`)}
>
Detail
</Button>
</Tooltip>
</Box>
</TableTd>
</TableTr>
))}

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

@@ -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

@@ -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
@@ -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

@@ -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"
},
]