Compare commits

..

12 Commits

Author SHA1 Message Date
d128313e71 Fix QC Keano FrontEnd
Fix QC Kak Ayu Admin 29 Okt
2025-11-03 17:36:00 +08:00
7b4bb1e58e QC Kak Inno FrontEnd Done
QC Kak Ayu FrontEnd Done
QC Keano 31 Okt
2025-11-03 10:28:03 +08:00
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
ed371bd0d9 Fix QC Kak Inno 24 Okt 25
Fix QC Kak Ayu 24 Okt 25
Fix QC Keano 24 Okt 25
Fix Detail Lowongan Kerja
2025-10-27 22:15:55 +08:00
f82c7b86e0 27 Oct 2025-10-27 10:54:50 +08:00
b5d6585cd5 27 Oct 2025-10-27 10:54:01 +08:00
aa98359ef7 Fix Revisi Kak Inno 22 Oktober && Fix Revisi Kak Ayu 22 Oktober 2025-10-23 17:45:45 +08:00
0ff0d5234a Fix QC Kak Inno 21 Oktober, QC Kak Ayu 21 Oktober, QC Keano, && QC Pak Jun 21 Oktober 2025-10-22 17:00:12 +08:00
827c1c191a Revisi QC Kak Inno tanggal 20 2025-10-22 09:58:16 +08:00
fb596f9033 Fix QC Kak Inno 17 Okt 25, Fix QC Kak Ayu 17 Okt 25, & Fix Qc Pak Jun 17 Okt 25 2025-10-21 12:17:30 +08:00
9055b40769 Fix navbar mobile add active page 2025-10-19 18:08:49 +08:00
137 changed files with 5105 additions and 2909 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -3,7 +3,7 @@
"version": "0.1.5", "version": "0.1.5",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "bun --bun next dev --hostname 0.0.0.0", "dev": "bun --bun next dev",
"build": "bun --bun next build", "build": "bun --bun next build",
"start": "bun --bun next start" "start": "bun --bun next start"
}, },
@@ -26,6 +26,7 @@
"@mantine/dropzone": "^8.1.1", "@mantine/dropzone": "^8.1.1",
"@mantine/form": "^8.1.0", "@mantine/form": "^8.1.0",
"@mantine/hooks": "^7.17.4", "@mantine/hooks": "^7.17.4",
"@mantine/modals": "^8.3.6",
"@mantine/tiptap": "^7.17.4", "@mantine/tiptap": "^7.17.4",
"@paljs/types": "^8.1.0", "@paljs/types": "^8.1.0",
"@prisma/client": "^6.3.1", "@prisma/client": "^6.3.1",
@@ -43,6 +44,7 @@
"@types/bun": "^1.2.2", "@types/bun": "^1.2.2",
"@types/leaflet": "^1.9.20", "@types/leaflet": "^1.9.20",
"@types/lodash": "^4.17.16", "@types/lodash": "^4.17.16",
"@types/nodemailer": "^7.0.2",
"add": "^2.0.6", "add": "^2.0.6",
"adm-zip": "^0.5.16", "adm-zip": "^0.5.16",
"animate.css": "^4.1.1", "animate.css": "^4.1.1",
@@ -52,9 +54,11 @@
"classnames": "^2.5.1", "classnames": "^2.5.1",
"colors": "^1.4.0", "colors": "^1.4.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"dotenv": "^17.2.3",
"elysia": "^1.3.5", "elysia": "^1.3.5",
"embla-carousel-autoplay": "^8.5.2", "embla-carousel": "^8.6.0",
"embla-carousel-react": "^7.1.0", "embla-carousel-autoplay": "^8.6.0",
"embla-carousel-react": "^8.6.0",
"extract-zip": "^2.0.1", "extract-zip": "^2.0.1",
"form-data": "^4.0.2", "form-data": "^4.0.2",
"framer-motion": "^12.23.5", "framer-motion": "^12.23.5",
@@ -71,12 +75,14 @@
"next": "^15.5.2", "next": "^15.5.2",
"next-view-transitions": "^0.3.4", "next-view-transitions": "^0.3.4",
"node-fetch": "^3.3.2", "node-fetch": "^3.3.2",
"nodemailer": "^7.0.10",
"p-limit": "^6.2.0", "p-limit": "^6.2.0",
"primeicons": "^7.0.0", "primeicons": "^7.0.0",
"primereact": "^10.9.6", "primereact": "^10.9.6",
"prisma": "^6.3.1", "prisma": "^6.3.1",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-exif-orientation-img": "^0.1.5",
"react-international-phone": "^4.6.0", "react-international-phone": "^4.6.0",
"react-leaflet": "^5.0.0", "react-leaflet": "^5.0.0",
"react-simple-toasts": "^6.1.0", "react-simple-toasts": "^6.1.0",

View File

@@ -6,9 +6,9 @@ import { proxy } from "valtio";
import { z } from "zod"; import { z } from "zod";
const templateForm = z.object({ const templateForm = z.object({
name: z.string().min(1, "Nama minimal 1 karakter"), name: z.string().min(5, "Nama minimal 5 karakter"),
deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"), deskripsi: z.string().min(5, "Deskripsi minimal 5 karakter"),
slug: z.string().min(1, "Deskripsi singkat minimal 1 karakter"), slug: z.string().min(5, "Deskripsi singkat minimal 5 karakter"),
icon: z.string().min(1, "Icon minimal 1 karakter"), icon: z.string().min(1, "Icon minimal 1 karakter"),
}); });
@@ -29,26 +29,33 @@ const programKreatifState = proxy({
const err = `[${cek.error.issues const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`) .map((v) => `${v.path.join(".")}`)
.join("\n")}] required`; .join("\n")}] required`;
return toast.error(err); toast.error(err);
return false; // ⬅️ ini penting
} }
try { try {
programKreatifState.create.loading = true; programKreatifState.create.loading = true;
const res = await ApiFetch.api.inovasi.programkreatif["create"].post( const res = await ApiFetch.api.inovasi.programkreatif["create"].post(
programKreatifState.create.form programKreatifState.create.form
); );
if (res.status === 200) { if (res.status === 200) {
programKreatifState.findMany.load(); programKreatifState.findMany.load();
return toast.success("success create"); toast.success("success create");
return true;
} }
console.log(res);
return toast.error("failed create"); toast.error("failed create");
return false;
} catch (error) { } catch (error) {
console.log((error as Error).message); console.error((error as Error).message);
toast.error("Terjadi kesalahan saat create");
return false;
} finally { } finally {
programKreatifState.create.loading = false; programKreatifState.create.loading = false;
} }
}, }
}, },
findMany: { findMany: {
data: null as any[] | null, data: null as any[] | null,

View File

@@ -55,9 +55,9 @@ function EditProgramKemiskinan() {
useEffect(() => { useEffect(() => {
if (!id) return; if (!id) return;
stateProgram.findUnique const loadData = async () => {
.load(id) try {
.then(() => { await stateProgram.findUnique.load(id);
const data = stateProgram.findUnique.data; const data = stateProgram.findUnique.data;
if (data) { if (data) {
setFormData({ setFormData({
@@ -70,12 +70,16 @@ function EditProgramKemiskinan() {
}, },
}); });
} }
}) } catch (err) {
.catch((err) => {
console.error('Error load data:', err); console.error('Error load data:', err);
toast.error('Gagal mengambil data program'); toast.error('Gagal mengambil data program');
}); }
}, [id, stateProgram.findUnique]); };
loadData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]); // ✅ hanya trigger saat id berubah
// generic handler untuk field top-level // generic handler untuk field top-level
const handleChange = useCallback( const handleChange = useCallback(

View File

@@ -5,7 +5,6 @@ import {
Box, Box,
Button, Button,
Group, Group,
Image,
Paper, Paper,
Stack, Stack,
Text, Text,
@@ -21,6 +20,7 @@ import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor'; import CreateEditor from '../../../_com/createEditor';
import desaDigitalState from '../../../_state/inovasi/desa-digital'; import desaDigitalState from '../../../_state/inovasi/desa-digital';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import ExifOrientationImg from 'react-exif-orientation-img';
export default function CreateDesaDigital() { export default function CreateDesaDigital() {
const stateDesaDigital = useProxy(desaDigitalState); const stateDesaDigital = useProxy(desaDigitalState);
@@ -173,17 +173,16 @@ export default function CreateDesaDigital() {
overflow: 'hidden', overflow: 'hidden',
}} }}
> >
<Image <ExifOrientationImg
src={previewImage} src={previewImage}
alt="Preview" alt="Preview"
radius="md" style={{
style={{ maxHeight: 220,
maxHeight: 220, objectFit: 'cover',
objectFit: 'cover', border: '1px solid #e0e0e0',
border: '1px solid #e0e0e0', borderRadius: 12,
}} }}
loading="lazy" />
/>
</Box> </Box>
)} )}
</Box> </Box>

View File

@@ -54,8 +54,8 @@ function DetailInfoTeknologiTepatGuna() {
{/* Card Utama */} {/* Card Utama */}
<Paper <Paper
withBorder withBorder
w={{ base: "100%", md: "70%", lg: "60%" }} w={{ base: "100%", md: "50%" }}
bg="#ECEEF8" bg={colors['white-1']}
p="lg" p="lg"
radius="md" radius="md"
shadow="sm" shadow="sm"
@@ -65,7 +65,7 @@ function DetailInfoTeknologiTepatGuna() {
Detail Info Teknologi Tepat Guna Detail Info Teknologi Tepat Guna
</Text> </Text>
<Paper bg={colors['BG-trans']} p="md" radius="md" shadow="xs"> <Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm"> <Stack gap="sm">
<Box> <Box>
<Text fz="lg" fw="bold">Judul</Text> <Text fz="lg" fw="bold">Judul</Text>

View File

@@ -1,6 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import { IconKey } from '@/app/admin/(dashboard)/_com/iconMap';
import programKreatifState from '@/app/admin/(dashboard)/_state/inovasi/program-kreatif'; import programKreatifState from '@/app/admin/(dashboard)/_state/inovasi/program-kreatif';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { import {
@@ -11,8 +12,7 @@ import {
Stack, Stack,
Text, Text,
TextInput, TextInput,
Title, Title
Tooltip,
} from '@mantine/core'; } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
@@ -20,7 +20,6 @@ import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import SelectIconProgramEdit from '../../../../_com/selectIconEdit'; import SelectIconProgramEdit from '../../../../_com/selectIconEdit';
import { IconKey } from '@/app/admin/(dashboard)/_com/iconMap';
interface FormProgramKreatif { interface FormProgramKreatif {
name: string; name: string;
@@ -41,6 +40,15 @@ function EditProgramKreatifDesa() {
icon: '', icon: '',
}); });
const [originalData, setOriginalData] = useState<FormProgramKreatif>({
name: '',
deskripsi: '',
slug: '',
icon: '',
});
const [isDataChanged, setIsDataChanged] = useState(false);
// Load data hanya sekali berdasarkan params.id // Load data hanya sekali berdasarkan params.id
useEffect(() => { useEffect(() => {
const loadProgramKreatif = async () => { const loadProgramKreatif = async () => {
@@ -51,12 +59,14 @@ function EditProgramKreatifDesa() {
const data = await stateProgramKreatif.update.load(id); const data = await stateProgramKreatif.update.load(id);
if (data) { if (data) {
stateProgramKreatif.update.id = id; stateProgramKreatif.update.id = id;
setFormData({ const loadedData = {
name: data.name || '', name: data.name || '',
slug: data.slug || '', slug: data.slug || '',
deskripsi: data.deskripsi || '', deskripsi: data.deskripsi || '',
icon: data.icon || '', icon: data.icon || '',
}); };
setFormData(loadedData);
setOriginalData(loadedData);
} }
} catch (error) { } catch (error) {
console.error('Error loading program kreatif:', error); console.error('Error loading program kreatif:', error);
@@ -67,12 +77,49 @@ function EditProgramKreatifDesa() {
loadProgramKreatif(); loadProgramKreatif();
}, [params?.id]); }, [params?.id]);
// Deteksi perubahan data
useEffect(() => {
const hasChanged =
formData.name !== originalData.name ||
formData.slug !== originalData.slug ||
formData.deskripsi !== originalData.deskripsi ||
formData.icon !== originalData.icon;
setIsDataChanged(hasChanged);
}, [formData, originalData]);
// Prevent browser back/refresh jika ada perubahan
useEffect(() => {
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
if (isDataChanged) {
e.preventDefault();
e.returnValue = '';
}
};
window.addEventListener('beforeunload', handleBeforeUnload);
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
}, [isDataChanged]);
const handleChange = const handleChange =
(field: keyof FormProgramKreatif) => (field: keyof FormProgramKreatif) =>
(value: string) => { (value: string) => {
setFormData((prev) => ({ ...prev, [field]: value })); setFormData((prev) => ({ ...prev, [field]: value }));
}; };
const handleBackClick = () => {
if (isDataChanged) {
const confirmed = window.confirm(
'Anda memiliki perubahan yang belum disimpan. Apakah Anda yakin ingin keluar dari halaman ini? Semua perubahan akan hilang.'
);
if (confirmed) {
router.back();
}
} else {
router.back();
}
};
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
stateProgramKreatif.update.form = { stateProgramKreatif.update.form = {
@@ -82,6 +129,11 @@ function EditProgramKreatifDesa() {
icon: formData.icon.trim(), icon: formData.icon.trim(),
}; };
await stateProgramKreatif.update.submit(); await stateProgramKreatif.update.submit();
// Reset isDataChanged agar tidak muncul konfirmasi setelah save
setOriginalData(formData);
setIsDataChanged(false);
router.push('/admin/inovasi/program-kreatif-desa'); router.push('/admin/inovasi/program-kreatif-desa');
} catch (error) { } catch (error) {
console.error('Error updating program kreatif:', error); console.error('Error updating program kreatif:', error);
@@ -92,16 +144,14 @@ function EditProgramKreatifDesa() {
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md"> <Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow> <Button
<Button variant="subtle"
variant="subtle" onClick={handleBackClick}
onClick={() => router.back()} p="xs"
p="xs" radius="md"
radius="md" >
> <IconArrowBack color={colors['blue-button']} size={24} />
<IconArrowBack color={colors['blue-button']} size={24} /> </Button>
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark"> <Title order={4} ml="sm" c="dark">
Edit Program Kreatif Desa Edit Program Kreatif Desa
</Title> </Title>
@@ -172,4 +222,4 @@ function EditProgramKreatifDesa() {
); );
} }
export default EditProgramKreatifDesa; export default EditProgramKreatifDesa;

View File

@@ -32,10 +32,14 @@ function CreateProgramKreatifDesa() {
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
await stateCreate.create.create(); const success = await stateCreate.create.create();
resetForm();
router.push("/admin/inovasi/program-kreatif-desa"); if (success) {
resetForm();
router.push("/admin/inovasi/program-kreatif-desa");
}
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import laporanPublikState from '@/app/admin/(dashboard)/_state/keamanan/laporan-publik'; import laporanPublikState from '@/app/admin/(dashboard)/_state/keamanan/laporan-publik';
@@ -52,15 +53,14 @@ function EditLaporanPublik() {
try { try {
const data = await stateLaporan.edit.load(id); const data = await stateLaporan.edit.load(id);
if (data) { if (data) {
setFormData((prev) => ({ setFormData({
...prev, judul: data.judul ?? '',
judul: data.judul ?? prev.judul, lokasi: data.lokasi ?? '',
lokasi: data.lokasi ?? prev.lokasi, tanggalWaktu: data.tanggalWaktu ?? '',
tanggalWaktu: data.tanggalWaktu ?? prev.tanggalWaktu, status: (data.status as Status) ?? 'Proses',
status: (data.status as Status) ?? prev.status, penanganan: data.penanganan?.[0]?.deskripsi ?? '',
penanganan: data.penanganan?.[0]?.deskripsi ?? prev.penanganan, kronologi: data.kronologi ?? '',
kronologi: data.kronologi ?? prev.kronologi, });
}));
} }
} catch (error) { } catch (error) {
console.error("Error loading laporan publik:", error); console.error("Error loading laporan publik:", error);
@@ -69,7 +69,8 @@ function EditLaporanPublik() {
}; };
loadLaporanPublik(); loadLaporanPublik();
}, [params?.id, stateLaporan.edit]); }, [params?.id]);
const handleChange = (field: string, value: string | Status) => { const handleChange = (field: string, value: string | Status) => {

View File

@@ -183,7 +183,7 @@ function EditArtikelKesehatan() {
{/* Gambar */} {/* Gambar */}
<Box> <Box>
<Text fw="bold" fz="sm" mb={6}> <Text fw="bold" fz="sm" mb={6}>
Gambar Berita Gambar Artikel Kesehatan
</Text> </Text>
<Dropzone <Dropzone
onDrop={handleFileChange} onDrop={handleFileChange}
@@ -240,15 +240,15 @@ function EditArtikelKesehatan() {
/> />
{/* Pendahuluan */} {/* Pendahuluan */}
<InputText <Box>
label="Pendahuluan" <Text fw="bold">Pendahuluan</Text>
value={formData.introduction.content} <EditEditor
onChange={(value) => value={formData.introduction.content}
setFormData((prev) => ({ ...prev, introduction: { content: value } })) onChange={(value) =>
} setFormData((prev) => ({ ...prev, introduction: { ...prev.introduction, content: value } }))
placeholder="Masukkan pendahuluan" }
/> />
</Box>
{/* Gejala */} {/* Gejala */}
<Box> <Box>
<Text fw="bold">Gejala</Text> <Text fw="bold">Gejala</Text>

View File

@@ -115,7 +115,7 @@ function CreateArtikelKesehatan() {
<Stack gap="md"> <Stack gap="md">
<Box> <Box>
<Text fw="bold" fz="sm" mb={6}> <Text fw="bold" fz="sm" mb={6}>
Gambar Berita Gambar Artikel Kesehatan
</Text> </Text>
<Dropzone <Dropzone
onDrop={(files) => { onDrop={(files) => {
@@ -163,7 +163,7 @@ function CreateArtikelKesehatan() {
</Box> </Box>
)} )}
</Box> </Box>
<TextInput <TextInput
label={"Judul"} label={"Judul"}
placeholder="Masukkan judul" placeholder="Masukkan judul"
@@ -182,16 +182,15 @@ function CreateArtikelKesehatan() {
}} }}
required required
/> />
<TextInput <Box>
label={"Pendahuluan"} <Text fz="sm" fw="bold">Pendahuluan</Text>
placeholder="Masukkan pendahuluan" <CreateEditor
required value={stateArtikelKesehatan.create.form.introduction.content}
defaultValue={stateArtikelKesehatan.create.form.introduction.content} onChange={(e) => {
onChange={(e) => { stateArtikelKesehatan.create.form.introduction.content = e;
stateArtikelKesehatan.create.form.introduction.content = e.target.value; }}
}} />
/> </Box>
{/* Gejala */} {/* Gejala */}
<Box> <Box>
<Text fz="md" fw="bold">Gejala</Text> <Text fz="md" fw="bold">Gejala</Text>

View File

@@ -1,5 +1,4 @@
'use client' 'use client'
import colors from '@/con/colors';
import { Box, Button, Grid, GridCol, Paper, Skeleton, Stack, Text, Title } from '@mantine/core'; import { Box, Button, Grid, GridCol, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconEdit } from '@tabler/icons-react'; import { IconEdit } from '@tabler/icons-react';
@@ -26,10 +25,10 @@ function Page() {
} }
return ( return (
<Box p={{ base: 'md', md: 'xl' }}> <Box p="md">
<Paper withBorder radius="md" p={{ base: 'md', md: 'lg' }} bg={colors['white-1']}> <Paper withBorder p={{ base: 'md', md: 'lg' }} radius="md">
{/* Header */} {/* Header */}
<Grid align="center" mb="lg"> <Grid align="center" mb={{ base: 'md', md: 'lg' }}>
<GridCol span={{ base: 12, md: 11 }}> <GridCol span={{ base: 12, md: 11 }}>
<Title order={3} fw={600} c="dark"> <Title order={3} fw={600} c="dark">
Preview Bentuk Konservasi Berdasarkan Adat Preview Bentuk Konservasi Berdasarkan Adat
@@ -55,8 +54,8 @@ function Page() {
{/* Konten */} {/* Konten */}
<Stack gap="md"> <Stack gap="md">
<Paper radius="md" p={{ base: 'md', md: 'xl' }} bg={colors['BG-trans']} shadow="sm"> <Paper p={{ base: 'md', md: 'xl' }} bg="#ECEEF8" radius="md">
<Box mb="md"> <Box mb="md" px={{ base: 0, md: 20 }}>
<Text <Text
fz={{ base: 'xl', md: '2xl' }} fz={{ base: 'xl', md: '2xl' }}
fw={600} fw={600}
@@ -67,7 +66,7 @@ function Page() {
style={{ wordBreak: "break-word", whiteSpace: "normal" }} style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/> />
</Box> </Box>
<Box> <Box px={{ base: 0, md: 20 }}>
<Text <Text
fz={{ base: 'md', md: 'lg' }} fz={{ base: 'md', md: 'lg' }}
ta="justify" ta="justify"

View File

@@ -1,10 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors';
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { IconRecycle, IconTrash } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { IconTrash, IconRecycle } from '@tabler/icons-react';
import colors from '@/con/colors';
function LayoutTabsPengelolaanSampahBankSampah({ children }: { children: React.ReactNode }) { function LayoutTabsPengelolaanSampahBankSampah({ children }: { children: React.ReactNode }) {
const router = useRouter(); const router = useRouter();
@@ -16,14 +16,12 @@ function LayoutTabsPengelolaanSampahBankSampah({ children }: { children: React.R
value: "listpengelolaansampahbanksampah", value: "listpengelolaansampahbanksampah",
href: "/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah", href: "/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah",
icon: <IconTrash size={18} stroke={1.8} />, icon: <IconTrash size={18} stroke={1.8} />,
tooltip: "Kelola data pengelolaan sampah bank sampah",
}, },
{ {
label: "Keterangan Bank Sampah Terdekat", label: "Keterangan Bank Sampah Terdekat",
value: "keteranganbanksampahterdekat", value: "keteranganbanksampahterdekat",
href: "/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat", href: "/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat",
icon: <IconRecycle size={18} stroke={1.8} />, icon: <IconRecycle size={18} stroke={1.8} />,
tooltip: "Kelola data bank sampah terdekat",
}, },
]; ];
@@ -74,14 +72,8 @@ function LayoutTabsPengelolaanSampahBankSampah({ children }: { children: React.R
}} }}
> >
{tabs.map((tab, i) => ( {tabs.map((tab, i) => (
<Tooltip
key={i}
label={tab.tooltip}
position="bottom"
withArrow
transitionProps={{ transition: 'pop', duration: 200 }}
>
<TabsTab <TabsTab
key={i}
value={tab.value} value={tab.value}
leftSection={tab.icon} leftSection={tab.icon}
style={{ style={{
@@ -92,7 +84,6 @@ function LayoutTabsPengelolaanSampahBankSampah({ children }: { children: React.R
> >
{tab.label} {tab.label}
</TabsTab> </TabsTab>
</Tooltip>
))} ))}
</TabsList> </TabsList>
</ScrollArea> </ScrollArea>

View File

@@ -65,26 +65,32 @@ function ListDataPerpustakaan({ search }: { search: string }) {
<Table striped highlightOnHover withRowBorders style={{ minWidth: '700px' }}> <Table striped highlightOnHover withRowBorders style={{ minWidth: '700px' }}>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>No</TableTh> <TableTh style={{ width: '5%' }}>No</TableTh>
<TableTh>Judul</TableTh> <TableTh style={{ width: '25%' }}>Judul</TableTh>
<TableTh>Kategori</TableTh> <TableTh style={{ width: '25%' }}>Kategori</TableTh>
<TableTh>Detail</TableTh> <TableTh style={{ width: '23%' }}>Deskripsi</TableTh>
<TableTh style={{ width: '22%' }}>Detail</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.length > 0 ? ( {filteredData.length > 0 ? (
filteredData.map((item, index) => ( filteredData.map((item, index) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd style={{ width: '5%' }}>
<Text truncate fz="sm">{index + 1}</Text> <Text truncate fz="sm">{index + 1}</Text>
</TableTd> </TableTd>
<TableTd> <TableTd style={{ width: '20%' }}>
<Text truncate fz="sm">{item.judul}</Text> <Text truncate fz="sm">{item.judul}</Text>
</TableTd> </TableTd>
<TableTd> <TableTd style={{ width: '20%' }}>
<Text truncate fz="sm">{item.kategori.name}</Text> <Text truncate fz="sm">{item.kategori.name}</Text>
</TableTd> </TableTd>
<TableTd> <TableTd style={{ width: '20%' }}>
<Box w={150}>
<Text dangerouslySetInnerHTML={{ __html: item.deskripsi }} lineClamp={1} truncate fz="sm"/>
</Box>
</TableTd>
<TableTd style={{ width: '20%' }}>
<Button <Button
variant="light" variant="light"
color="blue" color="blue"

View File

@@ -132,7 +132,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
<Tooltip label="Keluar" position="bottom" withArrow> <Tooltip label="Keluar" position="bottom" withArrow>
<ActionIcon <ActionIcon
onClick={() => { onClick={() => {
router.push("/login"); router.push("/darmasaba");
}} }}
color={colors["blue-button"]} color={colors["blue-button"]}
radius="xl" radius="xl"

View File

@@ -25,26 +25,40 @@ const searchState = proxy({
searchState.results = []; searchState.results = [];
return; return;
} }
searchState.loading = true; searchState.loading = true;
try {
const res = await ApiFetch.api.search.findMany.get({
query: {
query: searchState.query,
page: searchState.page,
limit: searchState.limit,
type: searchState.type,
},
});
console.log("Search API Response:", res);
const rawItems = res.data?.data || [];
const parsedItems = structuredClone(rawItems); // ✅ penting!
console.log("✅ Parsed items:", parsedItems);
if (searchState.page === 1) {
searchState.results = parsedItems;
} else {
searchState.results.push(...parsedItems);
}
const res = await ApiFetch.api.search.findMany.get({ console.log("Search results render:", searchState.results);
query: {
query: searchState.query,
page: searchState.page,
limit: searchState.limit,
type: searchState.type,
},
});
if (searchState.page === 1) {
searchState.results = res.data?.data || []; searchState.nextPage = res.data?.nextPage || null;
} else { } catch (error) {
searchState.results.push(...(res.data?.data || [])); console.error("Search fetch error:", error);
} finally {
searchState.loading = false;
} }
searchState.nextPage = res.data?.nextPage || null;
searchState.loading = false;
}, },
async next() { async next() {

View File

@@ -0,0 +1,54 @@
import { NextResponse } from 'next/server';
import nodemailer from 'nodemailer';
export async function POST(request: Request) {
try {
const { email } = await request.json();
// Input validation
if (!email) {
return NextResponse.json(
{ success: false, message: 'Email is required' },
{ status: 400 }
);
}
// Email regex validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return NextResponse.json(
{ success: false, message: 'Invalid email format' },
{ status: 400 }
);
}
// Configure nodemailer
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS,
},
});
// Send email
await transporter.sendMail({
from: `"Tim Info" <${process.env.EMAIL_USER}>`,
to: email,
subject: '✅ Berhasil Berlangganan!',
html: `<p>Terima kasih telah berlangganan info terbaru dari kami!</p>`,
});
return NextResponse.json({
success: true,
message: 'Subscription successful! Please check your email.',
});
} catch (error) {
console.error('Error in subscribe API:', error);
return NextResponse.json(
{ success: false, message: 'Internal server error' },
{ status: 500 }
);
}
}

View File

@@ -62,11 +62,23 @@ function Page() {
Informasi dan Pelayanan Administrasi Digital Informasi dan Pelayanan Administrasi Digital
</Text> </Text>
</Box> </Box>
<Image src={state.findUnique.data?.image?.link || ''} alt='' w={"100%"} loading="lazy"/> <Image src={state.findUnique.data?.image?.link || ''} alt='' w={"100%"} loading="lazy" />
</Container> </Container>
<Box px={{ base: "md", md: 100 }}> <Box px={{ base: "md", md: 100 }}>
<Stack gap={"xs"}> <Stack gap={"xs"}>
<Text py={20} fz={{ base: "sm", md: "lg" }} ta={"justify"} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: state.findUnique.data?.content || '' }} /> <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> </Stack>
</Box> </Box>
</Stack> </Stack>

View File

@@ -186,12 +186,11 @@ function Page() {
stateCreate.create.form.kategoriId = ''; stateCreate.create.form.kategoriId = '';
} }
}} }}
searchable // searchable
clearable clearable
nothingFoundMessage="Tidak ditemukan" nothingFoundMessage="Tidak ditemukan"
required required
/> />
<Button bg={colors['blue-button']} onClick={handleSubmit}> <Button bg={colors['blue-button']} onClick={handleSubmit}>
Simpan Simpan
</Button> </Button>

View File

@@ -7,34 +7,52 @@ import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function PelayananPerizinanBerusaha() { function PelayananPerizinanBerusaha() {
const state = useProxy(stateLayananDesa) const state = useProxy(stateLayananDesa);
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false);
const [active, setActive] = useState(1); const [active, setActive] = useState(0);
const nextStep = () => setActive((current) => (current < 6 ? current + 1 : current));
const prevStep = () => setActive((current) => (current > 0 ? current - 1 : current)); const totalSteps = 6;
const nextStep = () => {
if (active < totalSteps - 1) {
setActive(active + 1);
} else if (active === totalSteps - 1) {
setActive(totalSteps); // Mark as completed
}
};
const prevStep = () => {
if (active > 0) {
setActive(active - 1);
}
};
useEffect(() => { useEffect(() => {
const loadData = async () => { const loadData = async () => {
try { try {
setLoading(true); setLoading(true);
await state.pelayananPerizinanBerusaha.findById.load('edit') await state.pelayananPerizinanBerusaha.findById.load('edit');
} catch (error) { } catch (error) {
console.error('Gagal memuat data:', error); console.error('Gagal memuat data:', error);
} finally { } finally {
setLoading(false); setLoading(false);
} }
} };
loadData() loadData();
}, []) }, []);
const data = state.pelayananPerizinanBerusaha.findById.data; const data = state.pelayananPerizinanBerusaha.findById.data;
if (!data && !loading) { if (!data && !loading) {
return ( return (
<Center mih={300}> <Center mih={300}>
<Stack align="center" gap="sm"> <Stack align="center" gap="sm">
<Text fz="lg" fw={500} c="dimmed">Belum ada informasi layanan yang tersedia</Text> <Text fz="lg" fw={500} c="dimmed">
<Button component="a" href="https://oss.go.id" target="_blank" radius="xl">Kunjungi OSS</Button> Belum ada informasi layanan yang tersedia
</Text>
<Button component="a" href="https://oss.go.id" target="_blank" radius="xl">
Kunjungi OSS
</Button>
</Stack> </Stack>
</Center> </Center>
); );
@@ -47,72 +65,111 @@ function PelayananPerizinanBerusaha() {
<Loader size="lg" color="blue" /> <Loader size="lg" color="blue" />
</Center> </Center>
) : ( ) : (
<Stack gap="lg"> <Stack gap="lg">
<Box> <Box>
<Title order={2} fw={700} fz={{ base: 22, md: 32 }} mb="sm"> <Title order={2} fw={700} fz={{ base: 22, md: 32 }} mb="sm">
Perizinan Berusaha Berbasis Risiko melalui OSS Perizinan Berusaha Berbasis Risiko melalui OSS
</Title> </Title>
<Text fz={{ base: 'sm', md: 'md' }} c="dimmed"> <Text fz={{ base: 'sm', md: 'md' }} c="dimmed">
Sistem Online Single Submission (OSS) untuk pendaftaran NIB Sistem Online Single Submission (OSS) untuk pendaftaran NIB
</Text> </Text>
</Box> </Box>
<Text fz={{ base: 'sm', md: 'md' }} ta="justify" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: data?.deskripsi || '' }} /> <Text
fz={{ base: 'sm', md: 'md' }}
ta="justify"
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
dangerouslySetInnerHTML={{ __html: data?.deskripsi || '' }}
/>
<Box> <Box>
<Text fw={600} mb="sm" fz={{ base: 'sm', md: 'lg' }}>Alur pendaftaran NIB:</Text> <Text fw={600} mb="sm" fz={{ base: 'sm', md: 'lg' }}>
<Stepper active={active} onStepClick={setActive} orientation="vertical" color="blue" radius="md" Alur pendaftaran NIB:
styles={{ </Text>
step: { padding: '14px 0' }, <Stepper
stepBody: { marginLeft: 8 } active={active}
}} onStepClick={(step) => {
> if (step <= active) { // Only allow clicking on previous or current steps
<StepperStep label="Langkah 1" description="Daftar Akun"> setActive(step);
<Text fz="sm">Membuat akun di portal OSS</Text> }
</StepperStep> }}
<StepperStep label="Langkah 2" description="Isi Data Perusahaan"> orientation="vertical"
<Text fz="sm">Lengkapi informasi perusahaan, data pemegang saham, dan alamat</Text> color="blue"
</StepperStep> radius="md"
<StepperStep label="Langkah 3" description="Pilih KBLI"> styles={{
<Text fz="sm">Menentukan kode KBLI sesuai jenis usaha</Text> step: { padding: '14px 0' },
</StepperStep> stepBody: { marginLeft: 8 }
<StepperStep label="Langkah 4" description="Unggah Dokumen"> }}
<Text fz="sm">Unggah akta pendirian, surat izin, dan dokumen wajib lainnya</Text> >
</StepperStep> <StepperStep label="Langkah 1" description="Daftar Akun">
<StepperStep label="Langkah 5" description="Verifikasi Instansi"> <Text fz="sm">Membuat akun di portal OSS</Text>
<Text fz="sm">Menunggu verifikasi dan persetujuan dari pihak berwenang</Text> </StepperStep>
</StepperStep> <StepperStep label="Langkah 2" description="Isi Data Perusahaan">
<StepperStep label="Langkah 6" description="Terbit NIB"> <Text fz="sm">Lengkapi informasi perusahaan, data pemegang saham, dan alamat</Text>
<Text fz="sm">Menerima NIB sebagai identitas resmi usaha</Text> </StepperStep>
</StepperStep> <StepperStep label="Langkah 3" description="Pilih KBLI">
<StepperCompleted> <Text fz="sm">Menentukan kode KBLI sesuai jenis usaha</Text>
<Center> </StepperStep>
<Stack align="center" gap="xs"> <StepperStep label="Langkah 4" description="Unggah Dokumen">
<IconCheck size={40} color="green" /> <Text fz="sm">Unggah akta pendirian, surat izin, dan dokumen wajib lainnya</Text>
<Text fz="sm" fw={500}>Proses pendaftaran selesai</Text> </StepperStep>
</Stack> <StepperStep label="Langkah 5" description="Verifikasi Instansi">
</Center> <Text fz="sm">Menunggu verifikasi dan persetujuan dari pihak berwenang</Text>
</StepperCompleted> </StepperStep>
</Stepper> <StepperStep label="Langkah 6" description="Terbit NIB">
<Text fz="sm">Menerima NIB sebagai identitas resmi usaha</Text>
</StepperStep>
<StepperCompleted>
<Center>
<Stack align="center" gap="xs">
<IconCheck size={40} color="green" />
<Text fz="sm" fw={500}>Proses pendaftaran selesai</Text>
</Stack>
</Center>
</StepperCompleted>
</Stepper>
{active < totalSteps && (
<Group justify="center" mt="lg"> <Group justify="center" mt="lg">
<Button variant="light" leftSection={<IconArrowLeft size={18} />} onClick={prevStep} disabled={active === 0}> <Button
variant="light"
leftSection={<IconArrowLeft size={18} />}
onClick={prevStep}
disabled={active === 0}
>
Kembali Kembali
</Button> </Button>
<Button rightSection={<IconArrowRight size={18} />} onClick={nextStep}>
Lanjut
</Button>
</Group>
</Box>
<Text fz="sm" ta="justify" c="dimmed" mt="md"> {active < totalSteps ? (
Catatan: Persyaratan dan prosedur dapat berubah sewaktu-waktu. Untuk informasi resmi terbaru, silakan kunjungi situs{" "} <Button
<a href="https://oss.go.id/" target="_blank" rel="noopener noreferrer">oss.go.id</a> atau hubungi instansi pemerintah terkait. rightSection={active < totalSteps - 1 ? <IconArrowRight size={18} /> : null}
</Text> onClick={nextStep}
</Stack> >
{active === totalSteps - 1 ? 'Selesai' : 'Lanjut'}
</Button>
) : (
<Button
variant="light"
onClick={() => setActive(0)}
>
Mulai Lagi
</Button>
)}
</Group>
)}
</Box>
<Text fz="sm" ta="justify" c="dimmed" mt="md">
Catatan: Persyaratan dan prosedur dapat berubah sewaktu-waktu. Untuk informasi resmi terbaru, silakan kunjungi situs{' '}
<a href="https://oss.go.id/" target="_blank" rel="noopener noreferrer">
oss.go.id
</a>{' '}
atau hubungi instansi pemerintah terkait.
</Text>
</Stack>
)} )}
</Box> </Box>
); );
} }
export default PelayananPerizinanBerusaha; export default PelayananPerizinanBerusaha;

View File

@@ -47,13 +47,13 @@ function PelayananSuratKeterangan({ search }: { search: string }) {
<Box pb="xl"> <Box pb="xl">
<Group justify="space-between" align="center" mb="md"> <Group justify="space-between" align="center" mb="md">
<Group gap="xs"> <Group gap="xs">
<IconFileDescription size={28} stroke={1.8} color={colors["blue-button"]} /> <IconFileDescription size={28} stroke={1.8} />
<Text fz={{ base: "h4", md: "h2" }} fw={700}> <Text fz={{ base: "h4", md: "h2" }} fw={700}>
Layanan Surat Keterangan Layanan Surat Keterangan
</Text> </Text>
</Group> </Group>
<Tooltip label="Pilih layanan surat keterangan sesuai kebutuhan Anda" withArrow> <Tooltip label="Pilih layanan surat keterangan sesuai kebutuhan Anda" withArrow>
<IconInfoCircle size={22} stroke={1.8} color={colors["blue-button"]} /> <IconInfoCircle size={22} stroke={1.8} />
</Tooltip> </Tooltip>
</Group> </Group>

View File

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

View File

@@ -2,7 +2,7 @@
'use client' 'use client'
import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi'; import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { BackgroundImage, Box, Button, Center, Flex, Group, Paper, SimpleGrid, Skeleton, Stack, Text, 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 { IconEye } from '@tabler/icons-react';
import { useTransitionRouter } from 'next-view-transitions'; import { useTransitionRouter } from 'next-view-transitions';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -12,6 +12,7 @@ import BackButton from '../layanan/_com/BackButto';
function Page() { function Page() {
const router = useTransitionRouter() const router = useTransitionRouter()
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [hoveredId, setHoveredId] = useState<string | null>(null)
const state = useProxy(potensiDesaState) const state = useProxy(potensiDesaState)
useEffect(() => { useEffect(() => {
@@ -43,7 +44,7 @@ function Page() {
<Text fz={{ base: "2rem", md: "3rem" }} fw={900} c={colors["blue-button"]} lh={1.2}> <Text fz={{ base: "2rem", md: "3rem" }} fw={900} c={colors["blue-button"]} lh={1.2}>
Potensi Desa Darmasaba Potensi Desa Darmasaba
</Text> </Text>
<Text fz="lg" c="dimmed" ta="justify"> <Text fz="lg" ta="justify">
Temukan berbagai potensi unggulan, peluang, dan daya tarik yang menjadikan Desa Darmasaba istimewa. Temukan berbagai potensi unggulan, peluang, dan daya tarik yang menjadikan Desa Darmasaba istimewa.
</Text> </Text>
</Stack> </Stack>
@@ -88,20 +89,55 @@ function Page() {
src={v.image?.link || ''} src={v.image?.link || ''}
h={360} h={360}
radius="xl" radius="xl"
style={{ overflow: 'hidden', position: 'relative' }} onMouseEnter={() => setHoveredId(v.id)}
onMouseLeave={() => setHoveredId(null)}
style={{
overflow: 'hidden',
position: 'relative',
cursor: 'pointer',
transition: 'transform 0.3s ease'
}}
> >
{/* Overlay with smooth transition */}
<Box <Box
pos="absolute" pos="absolute"
inset={0} inset={0}
bg="linear-gradient(180deg, rgba(0,0,0,0.25) 0%, rgba(0,0,0,0.7) 100%)" bg={hoveredId === v.id
? "linear-gradient(180deg, rgba(0,0,0,0.4) 0%, rgba(0,0,0,0.75) 100%)"
: "linear-gradient(180deg, rgba(0,0,0,0.1) 0%, rgba(0,0,0,0.15) 100%)"
}
style={{
transition: 'background 0.3s ease'
}}
/> />
<Stack justify="space-between" h="100%" gap="md" p="lg" pos="relative"> <Stack justify="space-between" h="100%" gap="md" p="lg" pos="relative">
{/* Kategori badge - always visible */}
<Group> <Group>
<Paper radius="lg" py={6} px={12} shadow="md" withBorder bg="rgba(255,255,255,0.85)"> <Paper
radius="lg"
py={6}
px={12}
shadow="md"
withBorder
bg="rgba(255,255,255,0.9)"
style={{
transition: 'all 0.3s ease'
}}
>
<Text fz="sm" fw={600}>{v.kategori?.nama}</Text> <Text fz="sm" fw={600}>{v.kategori?.nama}</Text>
</Paper> </Paper>
</Group> </Group>
<Box>
{/* Nama potensi - visible on hover */}
<Box
style={{
opacity: hoveredId === v.id ? 1 : 0,
transform: hoveredId === v.id ? 'translateY(0)' : 'translateY(10px)',
transition: 'all 0.3s ease',
pointerEvents: hoveredId === v.id ? 'auto' : 'none'
}}
>
<Text <Text
fw={800} fw={800}
c="white" c="white"
@@ -113,20 +149,28 @@ function Page() {
{v.name} {v.name}
</Text> </Text>
</Box> </Box>
<Group justify="center">
<Tooltip label="Lihat detail potensi" withArrow> {/* Button - visible on hover */}
<Button <Group
radius="xl" justify="center"
size="md" style={{
leftSection={<IconEye size={18} />} opacity: hoveredId === v.id ? 1 : 0,
bg={colors["blue-button"]} transform: hoveredId === v.id ? 'translateY(0)' : 'translateY(10px)',
variant="gradient" transition: 'all 0.3s ease',
gradient={{ from: colors["blue-button"], to: "#4dabf7", deg: 45 }} pointerEvents: hoveredId === v.id ? 'auto' : 'none'
onClick={() => router.push(`/darmasaba/desa/potensi/${v.id}`)} }}
> >
Lihat Detail <Button
</Button> radius="xl"
</Tooltip> size="md"
leftSection={<IconEye size={18} />}
bg={colors["blue-button"]}
variant="gradient"
gradient={{ from: colors["blue-button"], to: "#4dabf7", deg: 45 }}
onClick={() => router.push(`/darmasaba/desa/potensi/${v.id}`)}
>
Lihat Detail
</Button>
</Group> </Group>
</Stack> </Stack>
</BackgroundImage> </BackgroundImage>
@@ -149,4 +193,4 @@ function Page() {
); );
} }
export default Page; export default Page;

View File

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

View File

@@ -2,7 +2,7 @@
'use client' 'use client'
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile' import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'
import colors from '@/con/colors' import colors from '@/con/colors'
import { Box, Center, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core' import { Box, Center, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'
import { useEffect } from 'react' import { useEffect } from 'react'
import { useProxy } from 'valtio/utils' import { useProxy } from 'valtio/utils'
@@ -24,7 +24,7 @@ function LambangDesa() {
} }
return ( return (
<Box pb={90}> <Box>
<Stack align="center" gap="lg"> <Stack align="center" gap="lg">
<Box pb="lg"> <Box pb="lg">
<Center> <Center>
@@ -58,16 +58,14 @@ function LambangDesa() {
borderColor: '#e0e9ff', borderColor: '#e0e9ff',
}} }}
> >
<Tooltip label="Deskripsi lambang desa" position="top-start" withArrow>
<Text <Text
fz={{ base: 'md', md: 'lg' }} fz={{ base: '1.125rem', md: '1.375rem' }}
lh={1.8} lh={1.8}
c="dark" c="dark"
ta="justify" ta="justify"
style={{ fontWeight: 400, wordBreak: "break-word", whiteSpace: "normal", }} style={{ fontWeight: 400, wordBreak: "break-word", whiteSpace: "normal", }}
dangerouslySetInnerHTML={{ __html: data.deskripsi }} dangerouslySetInnerHTML={{ __html: data.deskripsi }}
/> />
</Tooltip>
</Paper> </Paper>
</Stack> </Stack>
</Box> </Box>

View File

@@ -1,11 +1,11 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import { Box, Card, Center, Group, Image, Loader, Paper, Stack, Text, Tooltip } from '@mantine/core'; import colors from '@/con/colors';
import { Box, Card, Center, Group, Image, Loader, Paper, Stack, Text } from '@mantine/core';
import { IconPhoto } from '@tabler/icons-react';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { IconPhoto } from '@tabler/icons-react';
import colors from '@/con/colors';
function MaskotDesa() { function MaskotDesa() {
const state = useProxy(stateProfileDesa.maskotDesa); const state = useProxy(stateProfileDesa.maskotDesa);
@@ -28,7 +28,7 @@ function MaskotDesa() {
} }
return ( return (
<Box pb={80}> <Box>
<Stack align="center" gap="xl"> <Stack align="center" gap="xl">
<Stack align="center" gap={10}> <Stack align="center" gap={10}>
<Image src="/pudak-icon.png" alt="Ikon Desa" w={{ base: 160, md: 240 }} loading="lazy"/> <Image src="/pudak-icon.png" alt="Ikon Desa" w={{ base: 160, md: 240 }} loading="lazy"/>
@@ -54,8 +54,8 @@ function MaskotDesa() {
<Group justify="center" gap="lg" mt="lg"> <Group justify="center" gap="lg" mt="lg">
{data.images.length > 0 ? ( {data.images.length > 0 ? (
data.images.map((img, index) => ( data.images.map((img, index) => (
<Tooltip key={index} label={img.label} position="bottom" withArrow>
<Card <Card
key={index}
radius="lg" radius="lg"
shadow="md" shadow="md"
withBorder withBorder
@@ -79,7 +79,6 @@ function MaskotDesa() {
{img.label} {img.label}
</Text> </Text>
</Card> </Card>
</Tooltip>
)) ))
) : ( ) : (
<Stack align="center" gap="xs" mt="lg"> <Stack align="center" gap="xs" mt="lg">

View File

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

View File

@@ -2,10 +2,10 @@
'use client' 'use client'
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Image, Paper, SimpleGrid, Skeleton, Stack, Text, Divider, Tooltip } from '@mantine/core'; import { Box, Divider, Image, Paper, SimpleGrid, Skeleton, Stack, Text } from '@mantine/core';
import { IconBriefcase, IconTargetArrow, IconUser, IconUsers } from '@tabler/icons-react';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { IconUser, IconBriefcase, IconUsers, IconTargetArrow } from '@tabler/icons-react';
function ProfilPerbekel() { function ProfilPerbekel() {
const state = useProxy(stateProfileDesa.profilPerbekel) const state = useProxy(stateProfileDesa.profilPerbekel)
@@ -25,12 +25,12 @@ function ProfilPerbekel() {
} }
return ( return (
<Box pb={80} px="md"> <Box px="md">
<Stack align="center" gap={0} mb={40}> <Stack align="center" gap={0} mb={40}>
<Text <Text
c={colors['blue-button']} c={colors['blue-button']}
ta="center" ta="center"
fw="bold" fw="bold"
fz={{ base: "2rem", md: "2.8rem" }} fz={{ base: "2rem", md: "2.8rem" }}
style={{ letterSpacing: "0.5px" }} style={{ letterSpacing: "0.5px" }}
> >
@@ -41,11 +41,11 @@ function ProfilPerbekel() {
<SimpleGrid cols={{ base: 1, md: 2 }} spacing="xl" pb={50}> <SimpleGrid cols={{ base: 1, md: 2 }} spacing="xl" pb={50}>
<Box> <Box>
<Paper <Paper
bg={colors['white-trans-1']} bg={colors['white-trans-1']}
w="100%" w="100%"
radius="xl" radius="xl"
shadow="md" shadow="md"
withBorder withBorder
> >
<Stack gap={0}> <Stack gap={0}>
@@ -70,9 +70,9 @@ function ProfilPerbekel() {
<Text c={colors['white-1']} fz={{ base: "lg", md: "h3" }}> <Text c={colors['white-1']} fz={{ base: "lg", md: "h3" }}>
Perbekel Desa Darmasaba Perbekel Desa Darmasaba
</Text> </Text>
<Text <Text
c={colors['white-1']} c={colors['white-1']}
fw="bolder" fw="bolder"
fz={{ base: "xl", md: "h2" }} fz={{ base: "xl", md: "h2" }}
mt={8} mt={8}
> >
@@ -83,89 +83,85 @@ function ProfilPerbekel() {
</Paper> </Paper>
</Box> </Box>
<Paper <Paper
p="xl" p="xl"
bg={colors['white-trans-1']} bg={colors['white-trans-1']}
w="100%" w="100%"
radius="xl" radius="xl"
shadow="md" shadow="md"
withBorder withBorder
> >
<Stack gap="xl"> <Stack gap="xl">
<Box> <Box>
<Tooltip label="Informasi pribadi perbekel" withArrow> <Stack gap={6}>
<Stack gap={6}> <Stack align="center" gap={6}>
<Stack align="center" gap={6}> <IconUser size={22} />
<IconUser size={22} color={colors['blue-button']} /> <Text fz={{ base: "1.2rem", md: "1.5rem" }} fw="bold">Biodata</Text>
<Text fz={{ base: "1.2rem", md: "1.5rem" }} fw="bold">Biodata</Text>
</Stack>
<Text
fz={{ base: "1rem", md: "1.2rem" }}
ta="justify"
lh={1.6}
dangerouslySetInnerHTML={{ __html: data.biodata }}
style={{wordBreak: "break-word", whiteSpace: "normal"}}
/>
</Stack> </Stack>
</Tooltip> <Text
fz={{ base: "1rem", md: "1.2rem" }}
ta="justify"
lh={1.6}
dangerouslySetInnerHTML={{ __html: data.biodata }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/>
</Stack>
</Box> </Box>
<Box> <Box>
<Tooltip label="Pengalaman kerja perbekel" withArrow> <Stack gap={6}>
<Stack gap={6}> <Stack align="center" gap={6}>
<Stack align="center" gap={6}> <IconBriefcase size={22} />
<IconBriefcase size={22} color={colors['blue-button']} /> <Text fz={{ base: "1.2rem", md: "1.5rem" }} fw="bold">Pengalaman</Text>
<Text fz={{ base: "1.2rem", md: "1.5rem" }} fw="bold">Pengalaman</Text>
</Stack>
<Text
fz={{ base: "1rem", md: "1.2rem" }}
ta="justify"
lh={1.6}
dangerouslySetInnerHTML={{ __html: data.pengalaman }}
style={{wordBreak: "break-word", whiteSpace: "normal"}}
/>
</Stack> </Stack>
</Tooltip> <Text
fz={{ base: "1rem", md: "1.2rem" }}
ta="left"
lh={1.6}
dangerouslySetInnerHTML={{ __html: data.pengalaman }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/>
</Stack>
</Box> </Box>
</Stack> </Stack>
</Paper> </Paper>
</SimpleGrid> </SimpleGrid>
<Paper <Paper
p="xl" p="xl"
bg={colors['white-trans-1']} bg={colors['white-trans-1']}
w="100%" w="100%"
radius="xl" radius="xl"
shadow="md" shadow="md"
withBorder withBorder
> >
<Stack gap="xl"> <Stack gap="xl">
<Box> <Box>
<Stack align="center" gap={6} > <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> <Text fz={{ base: "1.2rem", md: "1.5rem" }} fw="bold">Pengalaman Organisasi</Text>
</Stack> </Stack>
<Text <Text
fz={{ base: "1rem", md: "1.2rem" }} fz={{ base: "1rem", md: "1.2rem" }}
ta="justify" ta="justify"
lh={1.6} lh={1.6}
dangerouslySetInnerHTML={{ __html: data.pengalamanOrganisasi }} dangerouslySetInnerHTML={{ __html: data.pengalamanOrganisasi }}
style={{wordBreak: "break-word", whiteSpace: "normal"}} style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/> />
</Box> </Box>
<Box> <Box>
<Stack align="center" gap={6} mb={6}> <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> <Text fz={{ base: "1.2rem", md: "1.5rem" }} fw="bold">Program Kerja Unggulan</Text>
</Stack> </Stack>
<Box px={10}> <Box px={10}>
<Text <Text
fz={{ base: "1rem", md: "1.2rem" }} fz={{ base: "1rem", md: "1.2rem" }}
ta="justify" ta="justify"
lh={1.6} lh={1.6}
dangerouslySetInnerHTML={{ __html: data.programUnggulan }} dangerouslySetInnerHTML={{ __html: data.programUnggulan }}
style={{wordBreak: "break-word", whiteSpace: "normal"}} style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/> />
</Box> </Box>
</Box> </Box>

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
'use client' 'use client'
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import { Box, Center, Image, Paper, SimpleGrid, Skeleton, Stack, Text, Title, Tooltip } from '@mantine/core'; import { Box, Center, Image, Paper, SimpleGrid, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconUser } from '@tabler/icons-react'; import { IconUser } from '@tabler/icons-react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
@@ -36,7 +36,7 @@ function SemuaPerbekel() {
} }
return ( return (
<Box pb={80}> <Box>
<Stack align="center" gap="lg"> <Stack align="center" gap="lg">
<Box> <Box>
<Text <Text
@@ -77,23 +77,17 @@ function SemuaPerbekel() {
</Box> </Box>
<Stack gap={4} align="center"> <Stack gap={4} align="center">
<Tooltip label="Nama Perbekel" withArrow>
<Text fw={700} fz="lg" ta="center"> <Text fw={700} fz="lg" ta="center">
{v.nama} {v.nama}
</Text> </Text>
</Tooltip>
<Tooltip label="Wilayah menjabat" withArrow> <Text c="dimmed" fz="sm" ta="center">
<Text c="dimmed" fz="sm" ta="center">
{v.daerah} {v.daerah}
</Text> </Text>
</Tooltip>
<Tooltip label="Periode jabatan" withArrow> <Text c="blue" fw={600} fz="sm" ta="center">
<Text c="blue" fw={600} fz="sm" ta="center">
{v.periode} {v.periode}
</Text> </Text>
</Tooltip>
</Stack> </Stack>
</Stack> </Stack>
</Paper> </Paper>

View File

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

View File

@@ -1,167 +1,3 @@
// 'use client'
// import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
// import colors from '@/con/colors';
// import { Box, Grid, GridCol, Paper, SimpleGrid, Stack, Text, Title } from '@mantine/core';
// import { useProxy } from 'valtio/utils';
// import BackButton from '../../desa/layanan/_com/BackButto';
// import { useShallowEffect } from '@mantine/hooks';
// function Page() {
// const state = useProxy(PendapatanAsliDesa.ApbDesa);
// useShallowEffect(() => {
// state.findMany.load();
// }, []);
// useShallowEffect(() => {
// PendapatanAsliDesa.pembiayaan.findMany.load();
// PendapatanAsliDesa.belanja.findMany.load();
// PendapatanAsliDesa.pendapatan.findMany.load();
// }, []);
// // Get the latest APB data
// const latestApb = state.findMany.data?.[0];
// // Calculate totals
// const totalPendapatan = latestApb?.pendapatan?.reduce((sum, item) => sum + (item?.value || 0), 0) || 0;
// const totalBelanja = latestApb?.belanja?.reduce((sum, item) => sum + (item?.value || 0), 0) || 0;
// const totalPembiayaan = latestApb?.pembiayaan?.reduce((sum, item) => sum + (item?.value || 0), 0) || 0;
// return (
// <Stack pos="relative" bg={colors.Bg} py="xl" gap="lg">
// <Box px={{ base: 'md', md: 100 }}>
// <BackButton />
// </Box>
// <Text ta="center" fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw="bold">
// Pendapatan Asli Desa
// </Text>
// <Box px={{ base: "md", md: 100 }}>
// <Stack gap="lg" justify="center">
// <Paper bg={colors['white-1']} p="xl">
// <SimpleGrid cols={{ base: 1, md: 3 }} spacing="md">
// {/* Pendapatan Card */}
// <Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
// <Stack gap={"xs"}>
// <Title order={3}>Pendapatan</Title>
// {PendapatanAsliDesa.pendapatan.findMany.data?.map((item) => (
// <Box key={item.id}>
// <Grid>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="md" fw={500}>{item.name}</Text>
// </GridCol>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', {
// style: 'currency',
// currency: 'IDR',
// minimumFractionDigits: 0
// }).format(item.value)}</Text>
// </GridCol>
// </Grid>
// </Box>
// ))}
// <Grid>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="lg" fw={600} mb="xs">Total Pendapatan</Text>
// </GridCol>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="xl" fw={700} c={colors['blue-button']}>
// {new Intl.NumberFormat('id-ID', {
// style: 'currency',
// currency: 'IDR',
// minimumFractionDigits: 0
// }).format(totalPendapatan)}
// </Text>
// </GridCol>
// </Grid>
// </Stack>
// </Box>
// {/* Belanja Card */}
// <Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
// <Stack gap={"xs"}>
// <Title order={3}>Belanja</Title>
// {PendapatanAsliDesa.belanja.findMany.data?.map((item) => (
// <Box key={item.id}>
// <Grid>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="md" fw={500}>{item.name}</Text>
// </GridCol>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', {
// style: 'currency',
// currency: 'IDR',
// minimumFractionDigits: 0
// }).format(item.value)}</Text>
// </GridCol>
// </Grid>
// </Box>
// ))}
// <Grid>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="lg" fw={600} mb="xs">Total Belanja</Text>
// </GridCol>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="xl" fw={700} c="orange">
// {new Intl.NumberFormat('id-ID', {
// style: 'currency',
// currency: 'IDR',
// minimumFractionDigits: 0
// }).format(totalBelanja)}
// </Text>
// </GridCol>
// </Grid>
// </Stack>
// </Box>
// {/* Pembiayaan Card */}
// <Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
// <Stack gap={"xs"}>
// <Title order={3}>Pembiayaan</Title>
// {PendapatanAsliDesa.pembiayaan.findMany.data?.map((item) => (
// <Box key={item.id}>
// <Grid>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="md" fw={500}>{item.name}</Text>
// </GridCol>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', {
// style: 'currency',
// currency: 'IDR',
// minimumFractionDigits: 0
// }).format(item.value)}</Text>
// </GridCol>
// </Grid>
// </Box>
// ))}
// <Grid>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="lg" fw={600} mb="xs">Total Pembiayaan</Text>
// </GridCol>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="xl" fw={700} c="green">
// {new Intl.NumberFormat('id-ID', {
// style: 'currency',
// currency: 'IDR',
// minimumFractionDigits: 0
// }).format(totalPembiayaan)}
// </Text>
// </GridCol>
// </Grid>
// </Stack>
// </Box>
// </SimpleGrid>
// </Paper>
// </Stack>
// </Box>
// </Stack>
// );
// }
// export default Page;
'use client' 'use client'
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa'; import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
import colors from '@/con/colors'; import colors from '@/con/colors';
@@ -206,32 +42,41 @@ function Page() {
<Stack gap="lg" justify="center"> <Stack gap="lg" justify="center">
<Paper bg={colors['white-1']} p="xl"> <Paper bg={colors['white-1']} p="xl">
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="md"> <SimpleGrid cols={{ base: 1, md: 3 }} spacing="md">
{/* Pendapatan Card */} {/* Pendapatan Card */}
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}> <Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
<Stack gap={"xs"}> <Stack gap={"xs"}>
<Title order={3}>Pendapatan</Title> <Title order={3}>Pendapatan</Title>
{PendapatanAsliDesa.pendapatan.findMany.data?.map((item) => ( {latestApb?.pendapatan?.map((item) => (
<Box key={item.id}> <Box key={item.id}>
<Grid> <Grid>
<GridCol span={{ base: 12, md: 6 }}> <GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text fz="md" fw={500}>{item.name}</Text> <Text fz="md" fw={500}>{item.name}</Text>
</GridCol> </GridCol>
<GridCol span={{ base: 12, md: 6 }}> <GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', { <Text
style: 'currency', fz="md"
currency: 'IDR', fw={500}
minimumFractionDigits: 0 style={{
}).format(item.value)}</Text> wordBreak: 'break-word',
whiteSpace: 'normal',
textAlign: 'right',
}}
>
{new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(item.value)}
</Text>
</GridCol> </GridCol>
</Grid> </Grid>
</Box> </Box>
))} ))}
<Grid> <Grid>
<GridCol span={{ base: 12, md: 6 }}> <GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text fz="lg" fw={600} mb="xs">Total Pendapatan</Text> <Text fz="lg" fw={600} mb="xs">Total Pendapatan</Text>
</GridCol> </GridCol>
<GridCol span={{ base: 12, md: 6 }}> <GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text fz="xl" fw={700} c={colors['blue-button']}> <Text style={{
wordBreak: 'break-word',
whiteSpace: 'normal'
}} fz="xl" fw={700} c={colors['blue-button']}>
{new Intl.NumberFormat('id-ID', { {new Intl.NumberFormat('id-ID', {
style: 'currency', style: 'currency',
currency: 'IDR', currency: 'IDR',
@@ -247,18 +92,28 @@ function Page() {
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}> <Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
<Stack gap={"xs"}> <Stack gap={"xs"}>
<Title order={3}>Belanja</Title> <Title order={3}>Belanja</Title>
{PendapatanAsliDesa.belanja.findMany.data?.map((item) => ( {latestApb?.belanja?.map((item) => (
<Box key={item.id}> <Box key={item.id}>
<Grid> <Grid>
<GridCol span={{ base: 12, md: 6 }}> <GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text fz="md" fw={500}>{item.name}</Text> <Text fz="md" fw={500}>{item.name}</Text>
</GridCol> </GridCol>
<GridCol span={{ base: 12, md: 6 }}> <GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', { <Text
style: 'currency', fz="md"
currency: 'IDR', fw={500}
minimumFractionDigits: 0 style={{
}).format(item.value)}</Text> wordBreak: 'break-word',
whiteSpace: 'normal',
textAlign: 'right',
}}
>
{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(item.value)}
</Text>
</GridCol> </GridCol>
</Grid> </Grid>
</Box> </Box>
@@ -284,18 +139,28 @@ function Page() {
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}> <Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
<Stack gap={"xs"}> <Stack gap={"xs"}>
<Title order={3}>Pembiayaan</Title> <Title order={3}>Pembiayaan</Title>
{PendapatanAsliDesa.pembiayaan.findMany.data?.map((item) => ( {latestApb?.pembiayaan?.map((item) => (
<Box key={item.id}> <Box key={item.id}>
<Grid> <Grid>
<GridCol span={{ base: 12, md: 6 }}> <GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text fz="md" fw={500}>{item.name}</Text> <Text fz="md" fw={500}>{item.name}</Text>
</GridCol> </GridCol>
<GridCol span={{ base: 12, md: 6 }}> <GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', { <Text
style: 'currency', fz="md"
currency: 'IDR', fw={500}
minimumFractionDigits: 0 style={{
}).format(item.value)}</Text> wordBreak: 'break-word',
whiteSpace: 'normal',
textAlign: 'right',
}}
>
{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(item.value)}
</Text>
</GridCol> </GridCol>
</Grid> </Grid>
</Box> </Box>
@@ -366,5 +231,4 @@ function Page() {
); );
} }
export default Page; export default Page;

View File

@@ -48,7 +48,7 @@ function Page() {
p={10} p={10}
mb={50} mb={50}
h={400} h={400}
w={150} w={Math.max(data.length * 120, 800)} // auto lebar sesuai jumlah data
data={data.map((item) => ({ data={data.map((item) => ({
id: item.id, id: item.id,
Pekerjaan: item.pekerjaan, Pekerjaan: item.pekerjaan,

View File

@@ -0,0 +1,136 @@
'use client'
import lowonganKerjaState from '@/app/admin/(dashboard)/_state/ekonomi/lowongan-kerja';
import colors from '@/con/colors';
import { Box, Button, Center, Group, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconBrandWhatsapp, IconBriefcase, IconCurrencyDollar, IconMapPin, IconPhone } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function DetailLowonganKerjaUser() {
const state = useProxy(lowonganKerjaState);
const router = useRouter();
const params = useParams();
const [loading, setLoading] = useState(true);
useShallowEffect(() => {
const loadData = async () => {
await state.findUnique.load(params?.id as string);
setLoading(false);
};
loadData();
}, []);
const data = state.findUnique.data;
if (loading || !data) {
return (
<Center py="xl">
<Skeleton height={500} w={{ base: '90%', md: '70%' }} radius="lg" />
</Center>
);
}
return (
<Stack bg={colors.Bg} py="xl" px={{ base: 'md', md: 100 }} align="center">
<Box w={{ base: '100%', md: '70%' }}>
<Button
variant="subtle"
color="blue"
leftSection={<IconArrowBack size={20} />}
mb="md"
onClick={() => router.back()}
>
Kembali
</Button>
<Paper
radius="lg"
shadow="md"
withBorder
p="xl"
bg={colors['white-1']}
>
<Stack gap="lg">
{/* Judul */}
<Text fz={{ base: '1.6rem', md: '2rem' }} fw={700} c={colors['blue-button']}>
{data.posisi}
</Text>
<Text c="dimmed" fz="sm">
Diposting: {new Date(data.createdAt).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric'
})}
</Text>
{/* Info Ringkas */}
<Stack gap="sm" mt="md">
<Group gap="xs">
<IconBriefcase size={20} color={colors['blue-button']} />
<Text fz="md" fw={600}>{data.namaPerusahaan}</Text>
</Group>
<Group gap="xs">
<IconMapPin size={20} color={colors['blue-button']} />
<Text fz="md">{data.lokasi}</Text>
</Group>
<Group gap="xs">
<IconPhone size={20} color={colors['blue-button']} />
<Text fz="md">{data.notelp}</Text>
</Group>
<Group gap="xs">
<IconCurrencyDollar size={20} color={colors['blue-button']} />
<Text fz="md">{data.gaji || '-'}</Text>
</Group>
<Group gap="xs">
<IconBriefcase size={20} color={colors['blue-button']} />
<Text fz="md">{data.tipePekerjaan}</Text>
</Group>
</Stack>
<Box>
<Text fw={600} fz="lg" mb={4}>
Deskripsi Pekerjaan
</Text>
<Text
fz="sm"
lh={1.6}
style={{ wordBreak: 'break-word' }}
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
/>
</Box>
<Box>
<Text fw={600} fz="lg" mb={4}>
Kualifikasi
</Text>
<Text
fz="sm"
lh={1.6}
style={{ wordBreak: 'break-word' }}
dangerouslySetInnerHTML={{ __html: data.kualifikasi || '-' }}
/>
</Box>
<Center>
<Button
radius="md"
size="md"
mt="md"
bg={colors['blue-button']}
onClick={() => window.open(`https://wa.me/${data.notelp}`, '_blank')}
leftSection={<IconBrandWhatsapp size={20} />}
>
Hubungi Sekarang
</Button>
</Center>
</Stack>
</Paper>
</Box>
</Stack>
);
}
export default DetailLowonganKerjaUser;

View File

@@ -12,13 +12,13 @@ import BackButton from '../../desa/layanan/_com/BackButto';
const formatCurrency = (value: string | number) => { const formatCurrency = (value: string | number) => {
// Convert to string if it's a number // Convert to string if it's a number
const numStr = typeof value === 'number' ? value.toString() : value; const numStr = typeof value === 'number' ? value.toString() : value;
// Remove all non-digit characters // Remove all non-digit characters
const digitsOnly = numStr.replace(/\D/g, ''); const digitsOnly = numStr.replace(/\D/g, '');
// Format with thousand separators // Format with thousand separators
const formatted = digitsOnly.replace(/\B(?=(\d{3})+(?!\d))/g, '.'); const formatted = digitsOnly.replace(/\B(?=(\d{3})+(?!\d))/g, '.');
return `Rp.${formatted}`; return `Rp.${formatted}`;
}; };
@@ -52,7 +52,7 @@ function Page() {
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
<BackButton /> <BackButton />
</Box> </Box>
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }} pb={80}>
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}> <Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
Lowongan Kerja Lokal Lowongan Kerja Lokal
</Text> </Text>
@@ -103,7 +103,7 @@ function Page() {
</Box> </Box>
</Flex> </Flex>
</Box> </Box>
<Button onClick={() => router.push(`https://wa.me/${v.notelp?.replace(/\D/g, '')}`)}>Lamar Sekarang</Button> <Button onClick={() => router.push(`/darmasaba/ekonomi/lowongan-kerja-lokal/${v.id}`)}>Detail</Button>
</Stack> </Stack>
</Paper> </Paper>
) )

View File

@@ -0,0 +1,157 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, Image, Skeleton, Group, Badge, Divider } from '@mantine/core';
import { IconArrowBack, IconMapPin, IconPhone, IconStar } from '@tabler/icons-react';
import { useRouter, useParams } from 'next/navigation';
import React from 'react';
import { useProxy } from 'valtio/utils';
import { useShallowEffect } from '@mantine/hooks';
import pasarDesaState from '@/app/admin/(dashboard)/_state/ekonomi/pasar-desa/pasar-desa';
function DetailProdukPasarUser() {
const router = useRouter();
const params = useParams();
const statePasar = useProxy(pasarDesaState);
useShallowEffect(() => {
statePasar.pasarDesa.findUnique.load(params?.id as string);
}, []);
const data = statePasar.pasarDesa.findUnique.data;
if (!data) {
return (
<Stack py={10}>
<Skeleton height={400} radius="md" />
</Stack>
);
}
return (
<Box py={20}>
{/* Tombol kembali */}
<Box px={{ base: 'md', md: 100 }}>
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={20} color={colors['blue-button']} />}
mb={15}
>
Kembali ke daftar produk
</Button>
</Box>
<Paper
w={{ base: '100%', md: '70%' }}
mx="auto"
p="lg"
radius="md"
shadow="sm"
bg={colors['white-1']}
>
<Stack gap="lg">
{/* Gambar Produk */}
{data.image?.link ? (
<Image
src={data.image.link}
alt={data.nama}
radius="md"
h={250}
w="100%"
fit="cover"
loading="lazy"
/>
) : (
<Box
h={300}
bg="gray.1"
display="flex"
style={{ alignItems: 'center', justifyContent: 'center', borderRadius: 'md' }}
>
<Text c="dimmed">Tidak ada gambar</Text>
</Box>
)}
{/* Detail Produk */}
<Stack gap="xs">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
{data.nama || 'Produk Tanpa Nama'}
</Text>
<Group>
<Badge color="green" size="lg" radius="md">
Rp {data.harga?.toLocaleString('id-ID')}
</Badge>
{data.rating && (
<Group gap={4}>
<IconStar size={18} color="#FFD43B" />
<Text fz="md" fw={500}>{data.rating}</Text>
</Group>
)}
</Group>
</Stack>
<Divider my="sm" />
{/* Info Tambahan */}
<Stack gap="sm">
<Box>
<Text fz="lg" fw={600}>Kategori</Text>
<Group gap="xs" mt={4}>
{data.KategoriToPasar && data.KategoriToPasar.length > 0 ? (
data.KategoriToPasar.map((kategori) => (
<Badge key={kategori.id} color="blue" variant="light">
{kategori.kategori.nama}
</Badge>
))
) : (
<Text fz="sm" c="dimmed">Tidak ada kategori</Text>
)}
</Group>
</Box>
{data.alamatUsaha && (
<Group gap={6}>
<IconMapPin size={18} color={colors['blue-button']} />
<Text fz="md">{data.alamatUsaha}</Text>
</Group>
)}
{data.kontak && (
<Group gap={6}>
<IconPhone size={18} color={colors['blue-button']} />
<Text fz="md">{data.kontak}</Text>
</Group>
)}
</Stack>
<Divider my="sm" />
{/* Deskripsi */}
<Box>
<Text fz="lg" fw={600}>Deskripsi Produk</Text>
<Text fz="md" c="dimmed" mt={4}>
Tidak ada deskripsi.
</Text>
</Box>
{/* Tombol Aksi User */}
{data.kontak && (
<Button
mt="md"
color="green"
size="lg"
radius="md"
component="a"
href={`https://wa.me/${data.kontak.replace(/[^0-9]/g, '')}`}
target="_blank"
>
Hubungi Penjual via WhatsApp
</Button>
)}
</Stack>
</Paper>
</Box>
);
}
export default DetailProdukPasarUser;

View File

@@ -71,8 +71,11 @@ function Page() {
/> />
</GridCol> </GridCol>
</Grid> </Grid>
<Text px={{ base: 'md', md: 100 }} ta={"justify"} fz={{ base: "h4", md: "h3" }} > <Text px={{ base: 'md', md: 100 }} ta={"justify"} fz="md" >
Pasar Desa Online merupakan Media Promosi yang bertujuan untuk membantu warga desa dalam memasarkan dan memperkenalkan produknya kepada masyarakat. Pasar Desa Online adalah media promosi untuk membantu warga memasarkan
</Text>
<Text px={{ base: 'md', md: 100 }} ta={"justify"} fz="md" >
dan memperkenalkan produk mereka.
</Text> </Text>
</Box> </Box>
<Box px={{ base: "md", md: 100 }}> <Box px={{ base: "md", md: 100 }}>
@@ -105,7 +108,7 @@ function Page() {
return ( return (
<Stack key={k}> <Stack key={k}>
<motion.div <motion.div
onClick={() => router.push(`https://wa.me/${v.kontak?.replace(/\D/g, '')}`)} onClick={() => router.push(`/darmasaba/ekonomi/pasar-desa/${v.id}`)}
whileHover={{ scale: 1.05 }} whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.8 }} whileTap={{ scale: 0.8 }}
> >
@@ -117,7 +120,7 @@ function Page() {
h={200} h={200}
w='100%' w='100%'
style={{ objectFit: 'cover' }} style={{ objectFit: 'cover' }}
loading="lazy" loading="lazy"
/> />
<Text py={10} fw={'bold'} fz={'lg'}>{v.nama}</Text> <Text py={10} fw={'bold'} fz={'lg'}>{v.nama}</Text>
<Text fz={'md'}>Rp {v.harga.toLocaleString('id-ID')}</Text> <Text fz={'md'}>Rp {v.harga.toLocaleString('id-ID')}</Text>

View File

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

View File

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

View File

@@ -0,0 +1,83 @@
'use client'
import desaDigitalState from '@/app/admin/(dashboard)/_state/inovasi/desa-digital';
import colors from '@/con/colors';
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 BackButton from '../../../desa/layanan/_com/BackButto';
function DetailDesaDigitalUser() {
const stateDesaDigital = useProxy(desaDigitalState);
const params = useParams();
useShallowEffect(() => {
stateDesaDigital.findUnique.load(params?.id as string);
}, []);
if (!stateDesaDigital.findUnique.data) {
return (
<Stack py={40} align="center">
<Skeleton height={400} radius="md" w={{ base: "90%", md: "60%" }} />
<Text fz="lg" c="dimmed">Memuat data...</Text>
</Stack>
);
}
const data = stateDesaDigital.findUnique.data;
return (
<Stack bg={colors.Bg} py="xl" align="center" px={{ base: "md", md: "lg" }}>
{/* Tombol Back */}
<Box w={{ base: "100%", md: "60%" }}>
<BackButton/>
</Box>
{/* Card Detail */}
<Paper
w={{ base: "100%", md: "60%" }}
bg={colors["white-1"]}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text ta={"center"} fz={{ base: "xl", md: "2xl" }} fw="bold" c={colors["blue-button"]}>
{data?.name || "Desa Digital"}
</Text>
{/* Gambar */}
{data?.image?.link ? (
<Center>
<Image
src={data.image.link}
alt={data.name || "Gambar Desa Digital"}
radius="md"
fit="cover"
w={{ base: "100%", md: "80%" }}
h={{ base: 250, md: 350 }}
style={{ objectPosition: "center", borderRadius: 12 }}
/>
</Center>
) : (
<Center>
<Text c="dimmed">Tidak ada gambar</Text>
</Center>
)}
{/* Deskripsi */}
<Box pt="md">
<Text
fz={{ base: "md", md: "lg" }}
c="dimmed"
style={{ lineHeight: 1.6 }}
dangerouslySetInnerHTML={{ __html: data?.deskripsi || "-" }}
/>
</Box>
</Stack>
</Paper>
</Stack>
);
}
export default DetailDesaDigitalUser;

View File

@@ -1,17 +1,19 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Stack, Box, Text, Paper, SimpleGrid, Image, Skeleton, Center, Pagination, Grid, GridCol, TextInput } from '@mantine/core'; import { Stack, Box, Text, Paper, SimpleGrid, Image, Skeleton, Center, Pagination, Grid, GridCol, TextInput, Button } from '@mantine/core';
import React, { useState } from 'react'; import React, { useState } from 'react';
import BackButton from '../../desa/layanan/_com/BackButto'; import BackButton from '../../desa/layanan/_com/BackButto';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import desaDigitalState from '@/app/admin/(dashboard)/_state/inovasi/desa-digital'; import desaDigitalState from '@/app/admin/(dashboard)/_state/inovasi/desa-digital';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconSearch } from '@tabler/icons-react'; import { IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
function Page() { function Page() {
const [search, setSearch] = useState("") const [search, setSearch] = useState("")
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const state = useProxy(desaDigitalState) const state = useProxy(desaDigitalState)
const router = useRouter()
const { const {
data, data,
page, page,
@@ -39,10 +41,10 @@ function Page() {
<BackButton /> <BackButton />
</Box> </Box>
<Box px={{ base: 'md', md: 100 }} > <Box px={{ base: 'md', md: 100 }} >
<Grid align='center'> <Grid align='center'>
<GridCol span={{ base: 12, md: 9 }}> <GridCol span={{ base: 12, md: 9 }}>
<Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}> <Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
Desa Digital / Smart Village Desa Digital / Smart Village
</Text> </Text>
</GridCol> </GridCol>
<GridCol span={{ base: 12, md: 3 }}> <GridCol span={{ base: 12, md: 3 }}>
@@ -56,7 +58,8 @@ function Page() {
/> />
</GridCol> </GridCol>
</Grid> </Grid>
<Text fz={'h4'}>Mewujudkan Desa Darmasaba sebagai pusat inovasi digital yang memberdayakan masyarakat, meningkatkan kesejahteraan, dan menciptakan peluang ekonomi berbasis teknologi.</Text> <Text fz={'md'}>Menjadikan Desa Darmasaba pusat inovasi digital untuk pemberdayaan masyarakat</Text>
<Text fz={'md'}>dan peningkatan ekonomi berbasis teknologi.</Text>
</Box> </Box>
<Box px={{ base: "md", md: 100 }}> <Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'} justify='center'> <Stack gap={'lg'} justify='center'>
@@ -69,12 +72,62 @@ function Page() {
> >
{filteredData.map((v, k) => { {filteredData.map((v, k) => {
return ( return (
<Paper p={'xl'} key={k}> <Paper
<Image src={v.image.link? v.image.link : ''} pb={10} radius={10} alt='' loading="lazy"/> key={k}
<Text fz={'h3'} fw={'bold'} c={colors['blue-button']}>{v.name}</Text> radius="xl"
<Box> shadow="md"
<Text fz={"md"} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: v.deskripsi }} /> withBorder
</Box> 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 gap={"xs"}>
<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 fz={'h3'} fw={'bold'} c={colors['blue-button']}>{v.name}</Text>
<Box>
<Text lineClamp={3} truncate="end" fz={"md"} style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: v.deskripsi }} />
</Box>
</Stack>
<Center mt="md">
<Button
bg={colors['blue-button']}
radius="lg"
w="100%"
onClick={() => router.push(`/darmasaba/inovasi/desa-digital-smart-village/${v.id}`)}
>
Detail
</Button>
</Center>
</Paper> </Paper>
) )
})} })}

View File

@@ -57,7 +57,8 @@ function Page() {
/> />
</GridCol> </GridCol>
</Grid> </Grid>
<Text fz={'h4'}>Desa Darmasaba berkomitmen mengembangkan teknologi tepat guna yang sesuai dengan kebutuhan masyarakat, mendukung pembangunan berkelanjutan, dan meningkatkan kualitas hidup warga.</Text> <Text fz={'md'}>Desa Darmasaba berkomitmen mengembangkan teknologi tepat guna yang sesuai dengan kebutuhan masyarakat,</Text>
<Text fz={'md'}>mendukung pembangunan berkelanjutan, dan meningkatkan kualitas hidup warga.</Text>
</Box> </Box>
<Box px={{ base: "md", md: 100 }}> <Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'} p={'lg'}> <Stack gap={'lg'} p={'lg'}>
@@ -71,11 +72,22 @@ function Page() {
{filteredData.map((v, k) => { {filteredData.map((v, k) => {
return ( return (
<Paper p={'xl'} key={k}> <Paper p={'xl'} key={k}>
<Image src={v.image.link || ''} pb={10} radius={10} alt='' loading="lazy"/> <Stack gap={"xs"}>
<Text fz={'h3'} fw={'bold'} c={colors['blue-button']}>{v.name}</Text> <Image src={v.image.link || ''} pb={10} radius={10} alt='' loading="lazy" />
<Box pr={'lg'} pb={10}> <Text fz={'h3'} fw={'bold'}>{v.name}</Text>
<Text fz={'h4'} fw={'bold'} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: v.deskripsi }} /> <Box pr={'lg'} pb={10}>
</Box> <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> </Paper>
) )
})} })}

View File

@@ -75,18 +75,18 @@ function AdministrasiOnline() {
<Title order={3}>Ajukan Administrasi Online</Title> <Title order={3}>Ajukan Administrasi Online</Title>
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Nama</Text>} label={<Text fz="sm" fw="bold">Nama</Text>}
placeholder="masukkan nama" placeholder="Masukkan nama"
onChange={(val) => (state.administrasiOnline.create.form.name = val.target.value)} onChange={(val) => (state.administrasiOnline.create.form.name = val.target.value)}
/> />
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Alamat</Text>} label={<Text fz="sm" fw="bold">Alamat</Text>}
placeholder="masukkan alamat" placeholder="Masukkan alamat"
onChange={(val) => (state.administrasiOnline.create.form.alamat = val.target.value)} onChange={(val) => (state.administrasiOnline.create.form.alamat = val.target.value)}
/> />
<TextInput <TextInput
type="number" type="number"
label={<Text fz="sm" fw="bold">Nomor Telepon</Text>} 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)} onChange={(val) => (state.administrasiOnline.create.form.nomorTelepon = val.target.value)}
/> />
<Select <Select
@@ -95,7 +95,7 @@ function AdministrasiOnline() {
state.administrasiOnline.create.form.jenisLayananId = val ?? ""; state.administrasiOnline.create.form.jenisLayananId = val ?? "";
}} }}
label={<Text fw={"bold"} fz={"sm"}>Jenis Layanan</Text>} label={<Text fw={"bold"} fz={"sm"}>Jenis Layanan</Text>}
placeholder="Pilih kategori produk" placeholder="Pilih jenis layanan"
data={ data={
state.jenisLayanan.findMany.data?.map((v) => ({ state.jenisLayanan.findMany.data?.map((v) => ({
value: v.id, 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 */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita'; import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
import colors from '@/con/colors'; 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 dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime'; import relativeTime from 'dayjs/plugin/relativeTime';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import BackButton from '../../../desa/layanan/_com/BackButto'; import BackButton from '../../../desa/layanan/_com/BackButto';
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman'; import { useTransitionRouter } from 'next-view-transitions';
import { motion } from "framer-motion";
dayjs.extend(relativeTime); dayjs.extend(relativeTime);
function InformasiDesa() { function InformasiDesa() {
const stateBerita = useProxy(stateDashboardBerita.berita) const router = useTransitionRouter()
const statePengumuman = useProxy(stateDesaPengumuman.pengumuman) const stateBerita = useProxy(stateDashboardBerita.berita);
const statePengumuman = useProxy(stateDesaPengumuman.pengumuman);
useEffect(() => { useEffect(() => {
stateBerita.findFirst.load(); stateBerita.findFirst.load();
@@ -23,116 +26,216 @@ function InformasiDesa() {
statePengumuman.findRecent.load(); statePengumuman.findRecent.load();
}, []); }, []);
const dataBerita = stateBerita.findFirst.data const dataBerita = stateBerita.findFirst.data;
const dataPengumuman = statePengumuman.findFirst.data const dataPengumuman = statePengumuman.findFirst.data;
return ( return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}> <Stack pos="relative" bg={colors.Bg} py="xl" gap={22}>
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
<BackButton /> <BackButton />
</Box> </Box>
<Box 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 Informasi Desa
</Text> </Title>
</Box> </Box>
<Box px={{ base: "md", md: 100 }}>
<Stack gap={10}> <Box px={{ base: 'md', md: 100 }}>
<Stack gap={30}>
{/* === BERITA UTAMA === */}
{dataBerita && ( {dataBerita && (
<Paper shadow="md" radius="md" p="md"> <motion.div
<Grid> whileHover={{ scale: 1.05 }}
<GridCol span={{ md: 6, base: 12 }}> whileTap={{ scale: 0.95 }}
<Image style={{ cursor: "pointer" }}
src={dataBerita.image?.link || "/fallback.jpg"} onClick={() => router.push(`/darmasaba/inovasi/layanan-online-desa/informasi-desa/detail-berita/${dataBerita.id}`)}
alt={dataBerita.judul} >
radius="md" <Paper shadow="md" radius="lg" p="lg" withBorder>
fit="cover" <Grid align="center" gutter="xl">
height={250} <GridCol span={{ base: 12, md: 6 }}>
maw={600} <Image
loading="lazy" src={dataBerita.image?.link || '/fallback.jpg'}
/> alt={dataBerita.judul}
</GridCol> radius="md"
<GridCol span={{ md: 6, base: 12 }}> fit="cover"
<Box> height={280}
<Text fz="sm" c="dimmed">{dataBerita.kategoriBerita?.name} {dayjs(dataBerita.createdAt).fromNow()}</Text> loading="lazy"
<Title order={1} fw="bold">{dataBerita.judul}</Title> style={{ objectPosition: 'center' }}
<Text ta={"justify"} mt="xs" fz="md" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: dataBerita.content }} /> />
</Box> </GridCol>
</GridCol> <GridCol span={{ base: 12, md: 6 }}>
</Grid> <Stack gap="xs">
</Paper> <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> {/* === BERITA TERBARU === */}
<Grid> <Stack>
<Title order={3} fw={700}>
Berita Terbaru
</Title>
<Grid gutter="xl">
{stateBerita.findRecent.data.map((item) => ( {stateBerita.findRecent.data.map((item) => (
<GridCol span={{ base: 12, sm: 6, md: 3 }} key={item.id}> <GridCol span={{ base: 12, sm: 6, md: 3 }} key={item.id}>
<Card shadow="sm" radius="md" withBorder h="100%"> <motion.div
<Card.Section> whileHover={{ scale: 1.05 }}
<Image whileTap={{ scale: 0.95 }}
src={item.image?.link || "/placeholder.jpg"} style={{ cursor: "pointer" }}
alt={item.judul} onClick={() => router.push(`/darmasaba/inovasi/layanan-online-desa/informasi-desa/detail-berita/${item.id}`)}
height={160} // gambar fix height >
fit="cover" <Card
loading="lazy" shadow="sm"
/> radius="md"
</Card.Section> withBorder
<Stack gap="xs" mt="sm"> h="100%"
<Text fw={600} lineClamp={2}> >
{item.judul} <Card.Section>
</Text> <Image
<Text size="sm" color="dimmed" lineClamp={2}> src={item.image?.link || '/placeholder.jpg'}
{item.deskripsi} alt={item.judul}
</Text> height={160}
<Text size="xs" c="gray"> fit="cover"
{dayjs(item.createdAt).fromNow()} radius="sm"
</Text> loading="lazy"
</Stack> />
</Card> </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> </GridCol>
))} ))}
</Grid> </Grid>
</Stack> </Stack>
<Divider color={colors['blue-button']} my="md" />
<Grid> <Divider color={colors['blue-button']} my="lg" />
<GridCol span={{ md: 6, base: 12 }}>
{/* === PENGUMUMAN === */}
<Grid gutter="xl" align="stretch">
<GridCol span={{ base: 12, md: 6 }}>
{dataPengumuman && ( {dataPengumuman && (
<Paper h={"97%"} shadow="md" radius="md" p="md"> <motion.div
<Stack gap={"xs"}> onClick={() => router.push(`/darmasaba/inovasi/layanan-online-desa/informasi-desa/detail-pengumuman/${dataPengumuman.id}`)}
<Title order={1} fw="bold">{dataPengumuman.judul}</Title> whileHover={{ scale: 1.05 }}
<Text fz="sm" c="dimmed">{dataPengumuman.CategoryPengumuman?.name} {dayjs(dataPengumuman.createdAt).fromNow()}</Text> whileTap={{ scale: 0.95 }}
<Box> style={{ cursor: "pointer" }}
<Text ta={"justify"} mt="xs" fz="md" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: dataPengumuman.content }} /> >
</Box> <Paper shadow="md" radius="lg" p="lg" h="100%" withBorder>
</Stack> <Stack gap="xs">
</Paper> <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>
<GridCol span={{ md: 6, base: 12 }}>
<Stack py={10}> <GridCol span={{ base: 12, md: 6 }}>
<Title order={3}>Pengumuman Terbaru</Title> <Stack>
<Grid> <Title order={3} fw={700}>
Pengumuman Terbaru
</Title>
<Grid gutter="lg">
{statePengumuman.findRecent.data.map((item) => ( {statePengumuman.findRecent.data.map((item) => (
<GridCol span={{ base: 12, sm: 8, md: 6 }} key={item.id}> <GridCol span={{ base: 12, sm: 6 }} key={item.id}>
<Card shadow="sm" radius="md" withBorder h="100%"> <motion.div
<Stack gap="xs" mt="sm"> whileHover={{ scale: 1.05 }}
<Text fw={600} lineClamp={2}> whileTap={{ scale: 0.95 }}
{item.judul} style={{ cursor: "pointer" }}
</Text> onClick={() => router.push(`/darmasaba/inovasi/layanan-online-desa/informasi-desa/detail-pengumuman/${item.id}`)}
<Text size="sm" color="dimmed" lineClamp={2}> >
{item.deskripsi} <Card
</Text> shadow="xs"
<Text size="xs" c="gray"> radius="md"
{dayjs(item.createdAt).fromNow()} withBorder
</Text> h="100%"
</Stack> p="md"
</Card> 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> </GridCol>
))} ))}
</Grid> </Grid>
</Stack> </Stack>
</GridCol> </GridCol>
</Grid> </Grid>

View File

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

View File

@@ -1,16 +1,16 @@
'use client' 'use client'
import colors from '@/con/colors';
import { Box, Paper, Stack, Text, Skeleton } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useParams, useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import programKreatifState from '@/app/admin/(dashboard)/_state/inovasi/program-kreatif'; import programKreatifState from '@/app/admin/(dashboard)/_state/inovasi/program-kreatif';
import colors from '@/con/colors';
import { Box, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useParams } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import { IconMapper, IconKey } from '@/app/admin/(dashboard)/_com/iconMap'; import { IconKey, IconMapper } from '@/app/admin/(dashboard)/_com/iconMap';
import BackButton from '../../../desa/layanan/_com/BackButto';
function Page() { function Page() {
const stateProgramKreatif = useProxy(programKreatifState); const stateProgramKreatif = useProxy(programKreatifState);
const router = useRouter();
const params = useParams(); const params = useParams();
useShallowEffect(() => { useShallowEffect(() => {
@@ -31,14 +31,7 @@ function Page() {
<Box px={{ base: 'md', md: 100 }} py="md"> <Box px={{ base: 'md', md: 100 }} py="md">
{/* Tombol Kembali */} {/* Tombol Kembali */}
<Box mb="md"> <Box mb="md">
<Text <BackButton/>
c={colors['blue-button']}
fw="bold"
style={{ cursor: 'pointer' }}
onClick={() => router.back()}
>
&larr; Kembali
</Text>
</Box> </Box>
{/* Konten Utama */} {/* Konten Utama */}

View File

@@ -1,14 +1,14 @@
'use client' 'use client'
import { IconKey, IconMapper } from '@/app/admin/(dashboard)/_com/iconMap';
import programKreatifState from '@/app/admin/(dashboard)/_state/inovasi/program-kreatif'; import programKreatifState from '@/app/admin/(dashboard)/_state/inovasi/program-kreatif';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Group, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core'; import { Box, Button, Center, Grid, GridCol, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconSearch } from '@tabler/icons-react';
import { useTransitionRouter } from 'next-view-transitions'; import { useTransitionRouter } from 'next-view-transitions';
import React, { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto'; import BackButton from '../../desa/layanan/_com/BackButto';
import { IconSearch } from '@tabler/icons-react';
import { IconKey, IconMapper } from '@/app/admin/(dashboard)/_com/iconMap';
// const data = [ // const data = [
// { // {
@@ -75,17 +75,23 @@ function Page() {
<BackButton /> <BackButton />
</Box> </Box>
<Box px={{ base: 'md', md: 100 }} > <Box px={{ base: 'md', md: 100 }} >
<Group justify="space-between" mb="md" align='center'> <Grid align='center'>
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}> <GridCol span={{ base: 12, md: 9 }}>
Program Kreatif Desa <Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
</Text> Program Kreatif Desa
<TextInput </Text>
placeholder="Cari program kreatif..." </GridCol>
leftSection={<IconSearch size={20} />} <GridCol span={{ base: 12, md: 3 }}>
value={search} <TextInput
onChange={(e) => setSearch(e.currentTarget.value)} radius={"lg"}
/> placeholder='Cari Program Kreatif'
</Group> value={search}
onChange={(e) => setSearch(e.target.value)}
leftSection={<IconSearch size={20} />}
w={{ base: "50%", md: "100%" }}
/>
</GridCol>
</Grid>
</Box> </Box>
<Box px={{ base: "md", md: 100 }}> <Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'} justify='center'> <Stack gap={'lg'} justify='center'>
@@ -111,7 +117,7 @@ function Page() {
)} )}
</Center> </Center>
<Text ta={'center'} fz={'h3'} fw={'bold'} c={colors['blue-button']}>{v.name}</Text> <Text ta={'center'} fz={'h3'} fw={'bold'} c={colors['blue-button']}>{v.name}</Text>
<Text py={10} ta={'center'} fz={'lg'} c={'black'}>{v.slug}</Text> <Text lineClamp={2} lh={"1.9"} py={10} ta={'center'} fz={'lg'} c={'black'}>{v.slug}</Text>
<Center> <Center>
<Button onClick={() => router.push(`/darmasaba/inovasi/program-kreatif-desa/${v.id}`)} bg={colors['blue-button']}>Selengkapnya</Button> <Button onClick={() => router.push(`/darmasaba/inovasi/program-kreatif-desa/${v.id}`)} bg={colors['blue-button']}>Selengkapnya</Button>
</Center> </Center>

View File

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

View File

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

View File

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

View File

@@ -1,15 +1,15 @@
'use client' 'use client'
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import laporanPublikState from '@/app/admin/(dashboard)/_state/keamanan/laporan-publik'; import laporanPublikState from '@/app/admin/(dashboard)/_state/keamanan/laporan-publik';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, ColorSwatch, Flex, Group, Modal, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Center, ColorSwatch, Flex, Group, Modal, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
import { DateTimePicker } from '@mantine/dates'; import { DateTimePicker } from '@mantine/dates';
import { useDebouncedValue, useDisclosure, useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useDisclosure, useShallowEffect } from '@mantine/hooks';
import { IconArrowRight, IconPlus, IconSearch } from '@tabler/icons-react'; import { IconArrowRight, IconPlus, IconSearch } from '@tabler/icons-react';
import { useTransitionRouter } from 'next-view-transitions';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto'; import BackButton from '../../desa/layanan/_com/BackButto';
import { useTransitionRouter } from 'next-view-transitions';
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
function Page() { function Page() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
@@ -53,7 +53,7 @@ function Page() {
return ( return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}> <Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
<Flex justify="space-between" align="center"> <Group justify="space-between" align="center">
<BackButton /> <BackButton />
<TextInput <TextInput
radius={"lg"} radius={"lg"}
@@ -61,9 +61,9 @@ function Page() {
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
leftSection={<IconSearch size={20} />} leftSection={<IconSearch size={20} />}
w={{ base: "50%", md: "100%" }} w={{ base: "100%", md: "30%" }}
/> />
</Flex> </Group>
</Box> </Box>
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
<Group justify="space-between"> <Group justify="space-between">
@@ -118,7 +118,7 @@ function Page() {
return ( return (
<Paper radius={'lg'} key={k} bg={colors['white-trans-1']} p={'xl'}> <Paper radius={'lg'} key={k} bg={colors['white-trans-1']} p={'xl'}>
<Stack> <Stack>
<Title c={colors['blue-button']} order={1}>{v.judul}</Title> <Text c={colors['blue-button']} lineClamp={3} truncate="end" fz="h4" fw="bold">{v.judul}</Text>
<Text fs={'italic'} fz={'xl'}> <Text fs={'italic'} fz={'xl'}>
{v.tanggalWaktu {v.tanggalWaktu
? new Date(v.tanggalWaktu).toLocaleString('id-ID') ? new Date(v.tanggalWaktu).toLocaleString('id-ID')

View File

@@ -45,7 +45,7 @@ function DetailPencegahanKriminalitas() {
const data = kriminalitasState.findUnique.data; const data = kriminalitasState.findUnique.data;
return ( return (
<Box py="md" px="md"> <Box py="md" px={{ base: 'md', md: 100 }}>
<Group mb="md"> <Group mb="md">
<Button <Button
variant="light" variant="light"

View File

@@ -48,7 +48,7 @@ function Page() {
<Text fz={{ base: 'h1', md: '2.5rem' }} c={colors['blue-button']} fw="bold"> <Text fz={{ base: 'h1', md: '2.5rem' }} c={colors['blue-button']} fw="bold">
Pencegahan Kriminalitas Pencegahan Kriminalitas
</Text> </Text>
<Text c={colors['blue-button']} fz={{ base: 'h4', md: 'h3' }}> <Text fz='md'>
Keamanan Komunitas & Pencegahan Kriminal Keamanan Komunitas & Pencegahan Kriminal
</Text> </Text>
</Box> </Box>
@@ -78,7 +78,7 @@ function Page() {
<Text fz={{ base: 'h1', md: '2.5rem' }} c={colors['blue-button']} fw="bold"> <Text fz={{ base: 'h1', md: '2.5rem' }} c={colors['blue-button']} fw="bold">
Pencegahan Kriminalitas Pencegahan Kriminalitas
</Text> </Text>
<Text c={colors['blue-button']} fz={{ base: 'h4', md: 'h3' }}> <Text fz='md'>
Keamanan Komunitas & Pencegahan Kriminal Keamanan Komunitas & Pencegahan Kriminal
</Text> </Text>
</Box> </Box>
@@ -92,31 +92,63 @@ function Page() {
Program Keamanan Berjalan Program Keamanan Berjalan
</Text> </Text>
<Stack pt={30} gap="lg"> <Stack pt={30} gap="lg">
{data.length > 0 ? ( <Box
data.map((item) => ( style={{
<a key={item.id} href={`/darmasaba/keamanan/pencegahan-kriminalitas/${item.id}`}> minHeight: 300, // sesuaikan: tinggi area yg muat 3 item
<Paper p="md" bg={colors['blue-button']} radius="md" shadow="sm"> }}
<Stack gap={"xs"}> >
{data.length > 0 ? (
data.map((item) => (
<Paper
key={item.id}
p="md"
radius="md"
shadow="sm"
style={{
cursor: 'pointer',
backgroundColor: colors['blue-button'],
transition: 'all 0.2s ease',
}}
onClick={() =>
router.push(`/darmasaba/keamanan/pencegahan-kriminalitas/${item.id}`)
}
onMouseEnter={(e) =>
(e.currentTarget.style.backgroundColor = '#1a3e7a')
}
onMouseLeave={(e) =>
(e.currentTarget.style.backgroundColor = colors['blue-button'])
}
>
<Stack gap="xs">
<Text fz="h3" c={colors['white-1']}> <Text fz="h3" c={colors['white-1']}>
{item.judul} {item.judul}
</Text> </Text>
</Stack> </Stack>
</Paper> </Paper>
</a> ))
)) ) : (
) : ( <Text c="dimmed">Tidak ada data pencegahan kriminalitas yang cocok</Text>
<Text color="dimmed"> )}
Tidak ada data pencegahan kriminalitas yang cocok </Box>
</Text>
)}
<Button <Button
mt={20} mt={20}
fullWidth fullWidth
radius="xl" radius="xl"
size="md" size="md"
bg={colors['blue-button']} variant="outline"
rightSection={<IconArrowRight size={20} color={colors['white-1']} />} color="blue"
onClick={() => router.push(`/darmasaba/keamanan/pencegahan-kriminalitas/program-lainnya`)} rightSection={<IconArrowRight size={20} />}
styles={{
root: {
fontWeight: 600,
borderWidth: 2,
},
}}
onClick={() =>
router.push(
`/darmasaba/keamanan/pencegahan-kriminalitas/program-lainnya`
)
}
> >
Jelajahi Program Lainnya Jelajahi Program Lainnya
</Button> </Button>
@@ -142,9 +174,7 @@ function Page() {
<Text py={10} fz={{ base: 'h3', md: 'h2' }} fw="bold" c={colors['blue-button']}> <Text py={10} fz={{ base: 'h3', md: 'h2' }} fw="bold" c={colors['blue-button']}>
{findFirst.data?.judul} {findFirst.data?.judul}
</Text> </Text>
<Text fz="h4" c={colors['blue-button']}> <Text fz="h4" dangerouslySetInnerHTML={{ __html: findFirst.data?.deskripsiSingkat }} />
{findFirst.data?.deskripsiSingkat}
</Text>
</Paper> </Paper>
) : null} ) : null}
</Box> </Box>

View File

@@ -1,32 +1,42 @@
'use client' 'use client'
import { import {
Box, Box,
Button,
Card, Card,
Center, Center,
Group, Group,
Pagination, Pagination,
Paper,
Skeleton, Skeleton,
Stack, Stack,
Text, Text,
Title, Title
Tooltip,
Button,
Paper,
} from '@mantine/core'; } from '@mantine/core';
import { IconSearch, IconArrowRight } from '@tabler/icons-react'; import { IconArrowRight, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import HeaderSearch from '@/app/admin/(dashboard)/_com/header';
import { useProxy } from 'valtio/utils';
import pencegahanKriminalitasState from '@/app/admin/(dashboard)/_state/keamanan/pencegahan-kriminalitas'; import pencegahanKriminalitasState from '@/app/admin/(dashboard)/_state/keamanan/pencegahan-kriminalitas';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowLeft } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import HeaderSearch from '@/app/admin/(dashboard)/_com/header'; import { useProxy } from 'valtio/utils';
function PencegahanKriminalitas() { function PencegahanKriminalitas() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const router = useRouter();
return ( return (
<Box> <Box pt={20} px={{ base: 'md', md: 100 }}>
<Group mb="md">
<Button
variant="light"
color="blue"
onClick={() => router.back()}
leftSection={<IconArrowLeft size={20} />}
>
Kembali
</Button>
</Group>
<HeaderSearch <HeaderSearch
title="Program Pencegahan Kriminalitas" title="Program Pencegahan Kriminalitas"
placeholder="Cari program atau deskripsi..." placeholder="Cari program atau deskripsi..."
@@ -82,10 +92,9 @@ function ListPencegahanKriminalitas({ search }: { search: string }) {
c="dimmed" c="dimmed"
lineClamp={2} lineClamp={2}
dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat || '' }} dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat || '' }}
style={{wordBreak: "break-word", whiteSpace: "normal"}} style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/> />
<Group justify="flex-end" mt="sm"> <Group justify="flex-end" mt="sm">
<Tooltip label="Lihat detail program" withArrow>
<Button <Button
size="sm" size="sm"
variant="gradient" variant="gradient"
@@ -95,7 +104,6 @@ function ListPencegahanKriminalitas({ search }: { search: string }) {
> >
Lihat Detail Lihat Detail
</Button> </Button>
</Tooltip>
</Group> </Group>
</Stack> </Stack>
</Card> </Card>

View File

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

View File

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

View File

@@ -78,15 +78,12 @@ function Page() {
<Box> <Box>
<Text fz="h4" fw="bold">Pendahuluan</Text> <Text fz="h4" fw="bold">Pendahuluan</Text>
<Divider my="xs" /> <Divider my="xs" />
<Text fz="md" lh={1.6} ta="justify"> <Text fz="md" lh={1.6} ta="justify" dangerouslySetInnerHTML={{ __html: state.findUnique.data.introduction?.content }} />
{state.findUnique.data.introduction?.content}
</Text>
</Box> </Box>
<Box> <Box>
<Text fz="h4" fw="bold">Kenali Gejala DBD</Text> <Text fz="h4" fw="bold">{state.findUnique.data.symptom?.title}</Text>
<Divider my="xs" /> <Divider my="xs" />
<Text fz="md" fw="semibold">{state.findUnique.data.symptom?.title}</Text>
<Text fz="md" lh={1.6} ta="justify" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: state.findUnique.data.symptom?.content }} /> <Text fz="md" lh={1.6} ta="justify" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: state.findUnique.data.symptom?.content }} />
</Box> </Box>

View File

@@ -1,7 +1,7 @@
'use client' 'use client'
import artikelKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/artikelKesehatan'; import artikelKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/artikelKesehatan';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Anchor, Box, Card, Divider, Group, Image, Loader, Paper, Stack, Text, Title, Tooltip } from '@mantine/core'; import { Box, Button, Card, Divider, Group, Image, Loader, Paper, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconCalendar, IconChevronRight } from '@tabler/icons-react'; import { IconCalendar, IconChevronRight } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@@ -28,9 +28,9 @@ function ArtikelKesehatanPage() {
<Box> <Box>
<Paper p="xl" bg={colors['white-trans-1']} radius="xl" shadow="md"> <Paper p="xl" bg={colors['white-trans-1']} radius="xl" shadow="md">
<Stack gap="lg"> <Stack gap="lg">
<Title order={2} ta="center" c={colors['blue-button']}> <Text ta="center" fw={700} fz="32px" c={colors['blue-button']}>
Artikel Kesehatan Artikel Kesehatan
</Title> </Text>
<Divider size="sm" color={colors['blue-button']} /> <Divider size="sm" color={colors['blue-button']} />
{state.findMany.data.length === 0 ? ( {state.findMany.data.length === 0 ? (
<Box py="xl" ta="center"> <Box py="xl" ta="center">
@@ -51,31 +51,30 @@ function ArtikelKesehatanPage() {
onMouseLeave={(e) => (e.currentTarget.style.transform = 'translateY(0)')} onMouseLeave={(e) => (e.currentTarget.style.transform = 'translateY(0)')}
> >
<Card.Section> <Card.Section>
<Image src={item.image?.link} alt={item.title} height={200} fit="cover" loading="lazy"/> <Image style={{ borderTopLeftRadius: '10px', borderTopRightRadius: '10px' }} src={item.image?.link} alt={item.title} height={200} fit="cover" loading="lazy" />
</Card.Section> </Card.Section>
<Stack gap="xs" mt="md"> <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"> <Group gap="xs">
<IconCalendar size={16} color={colors['blue-button']} /> <IconCalendar size={16} color='gray' />
<Text fz="sm" c="dimmed"> <Text fz="sm" c="dimmed">
{new Date(item.createdAt).toLocaleDateString('id-ID', { year: 'numeric', month: 'long', day: 'numeric' })} Dinas Kesehatan {new Date(item.createdAt).toLocaleDateString('id-ID', { year: 'numeric', month: 'long', day: 'numeric' })} Dinas Kesehatan
</Text> </Text>
</Group> </Group>
<Text fz="md" c="dark" lineClamp={3}> <Text fz="md" lineClamp={3}>
{item.content} {item.content}
</Text> </Text>
<Tooltip label="Baca artikel lengkap"> <Group justify="flex-start">
<Anchor <Button
bg={colors['blue-button']}
radius="lg"
size="sm"
rightSection={<IconChevronRight size={18} />}
onClick={() => router.push(`/darmasaba/kesehatan/data-kesehatan-warga/artikel-kesehatan-page/${item.id}`)} onClick={() => router.push(`/darmasaba/kesehatan/data-kesehatan-warga/artikel-kesehatan-page/${item.id}`)}
variant="light"
c={colors['blue-button']}
> >
<Group gap="xs"> Baca Selengkapnya
<Text fw="bold" fz="md">Baca Selengkapnya</Text> </Button>
<IconChevronRight size={18} /> </Group>
</Group>
</Anchor>
</Tooltip>
</Stack> </Stack>
</Card> </Card>
)) ))

View File

@@ -3,9 +3,9 @@
import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan'; import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto'; import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { ActionIcon, Anchor, AspectRatio, Badge, Box, Button, Card, Chip, CopyButton, Divider, Grid, Group, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, ThemeIcon, Title, Tooltip } from '@mantine/core'; import { ActionIcon, AspectRatio, Badge, Box, Button, Card, CopyButton, Divider, Grid, Group, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, ThemeIcon, Title, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconBrandWhatsapp, IconCheck, IconCopy, IconDeviceLandlinePhone, IconHeart, IconInfoCircle, IconMail, IconMapPin, IconMoodEmpty, IconSearch, IconStethoscope, IconUser, IconUsersGroup, IconWallet } from '@tabler/icons-react'; import { IconBrandWhatsapp, IconCheck, IconCopy, IconDeviceLandlinePhone, IconHeart, IconInfoCircle, IconMail, IconMapPin, IconMoodEmpty, IconSearch, IconUser } from '@tabler/icons-react';
import { useParams } from 'next/navigation'; import { useParams } from 'next/navigation';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
@@ -149,11 +149,6 @@ function Page() {
</CopyButton> </CopyButton>
</Group> </Group>
</Group> </Group>
<Group gap="xs" mt="sm" wrap="wrap">
<Chip defaultChecked radius="xl" variant="light" icon={<IconStethoscope size={16} />}>Layanan Medis</Chip>
<Chip radius="xl" variant="light" icon={<IconUsersGroup size={16} />}>Ramah Keluarga</Chip>
<Chip radius="xl" variant="light" icon={<IconWallet size={16} />}>Pembayaran Non-Tunai</Chip>
</Group>
</Stack> </Stack>
</Card> </Card>
</Box> </Box>
@@ -210,7 +205,6 @@ function Page() {
<Button variant="light" leftSection={<IconBrandWhatsapp size={18} />} component="a" href={`https://wa.me/${kontak.whatsapp.replace(/\D/g, '')}`} target="_blank" aria-label="Hubungi WhatsApp">WhatsApp</Button> <Button variant="light" leftSection={<IconBrandWhatsapp size={18} />} component="a" href={`https://wa.me/${kontak.whatsapp.replace(/\D/g, '')}`} target="_blank" aria-label="Hubungi WhatsApp">WhatsApp</Button>
<Button variant="light" leftSection={<IconMail size={18} />} component="a" href={`mailto:${kontak.email}`} aria-label="Kirim Email">Email</Button> <Button variant="light" leftSection={<IconMail size={18} />} component="a" href={`mailto:${kontak.email}`} aria-label="Kirim Email">Email</Button>
</Group> </Group>
<Anchor target="_blank" underline="hover">Kunjungi situs resmi</Anchor>
</Stack> </Stack>
</Card> </Card>
@@ -246,15 +240,8 @@ function Page() {
</Table> </Table>
</Stack> </Stack>
</Card> </Card>
</Stack>
</Grid.Col>
</Grid>
</Box>
<Box px={{ base: 'md', md: 100 }}> <Card radius="xl" p="lg" withBorder>
<Grid gutter="lg">
<Grid.Col span={{ base: 12, md: 8 }}>
<Card radius="xl" p="lg" withBorder>
<Stack gap="md"> <Stack gap="md">
<Title order={3}>Fasilitas Pendukung</Title> <Title order={3}>Fasilitas Pendukung</Title>
<Divider /> <Divider />
@@ -270,8 +257,7 @@ function Page() {
)} )}
</Stack> </Stack>
</Card> </Card>
</Grid.Col>
<Grid.Col span={{ base: 12, md: 4 }}>
<Card radius="xl" p="lg" withBorder> <Card radius="xl" p="lg" withBorder>
<Stack gap="md"> <Stack gap="md">
<Title order={3}>Layanan & Tarif</Title> <Title order={3}>Layanan & Tarif</Title>
@@ -309,10 +295,11 @@ function Page() {
)} )}
</Stack> </Stack>
</Card> </Card>
</Stack>
</Grid.Col> </Grid.Col>
</Grid> </Grid>
</Box> </Box>
<Box px={{ base: 'md', md: 100 }} pb="xl"> <Box px={{ base: 'md', md: 100 }} pb="xl">
<Paper radius="xl" p="lg" withBorder> <Paper radius="xl" p="lg" withBorder>
<Stack gap="md"> <Stack gap="md">

View File

@@ -1,11 +1,11 @@
'use client' 'use client'
import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan'; import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Anchor, Badge, Box, Card, Divider, Group, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { Badge, Box, Button, Card, Divider, Group, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconChevronRight, IconClock, IconMapPin } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { IconMapPin, IconClock, IconArrowRight } from '@tabler/icons-react';
function FasilitasKesehatanPage() { function FasilitasKesehatanPage() {
const state = useProxy(fasilitasKesehatanState.fasilitasKesehatan); const state = useProxy(fasilitasKesehatanState.fasilitasKesehatan);
@@ -36,72 +36,73 @@ function FasilitasKesehatanPage() {
</Text> </Text>
<Divider size="sm" color={colors['blue-button']} /> <Divider size="sm" color={colors['blue-button']} />
<Stack gap="lg"> <Stack gap="lg">
{state.findMany.data.length === 0 ? ( {state.findMany.data.length === 0 ? (
<Box py="xl" ta="center"> <Box py="xl" ta="center">
<Text fz="lg" c="dimmed"> <Text fz="lg" c="dimmed">
Belum ada fasilitas kesehatan yang tersedia Belum ada fasilitas kesehatan yang tersedia
</Text> </Text>
</Box> </Box>
) : ( ) : (
state.findMany.data.map((item) => ( state.findMany.data.map((item) => (
<Card <Card
key={item.id} key={item.id}
withBorder withBorder
radius="xl" radius="xl"
shadow="sm" shadow="sm"
p="lg" p="lg"
style={{ style={{
background: 'linear-gradient(135deg, #fdfdfd, #f7faff)', background: 'linear-gradient(135deg, #fdfdfd, #f7faff)',
transition: 'transform 0.2s ease, box-shadow 0.2s ease', transition: 'transform 0.2s ease, box-shadow 0.2s ease',
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
(e.currentTarget as HTMLElement).style.transform = 'translateY(-4px)'; (e.currentTarget as HTMLElement).style.transform = 'translateY(-4px)';
(e.currentTarget as HTMLElement).style.boxShadow = '0 8px 20px rgba(0,0,0,0.08)'; (e.currentTarget as HTMLElement).style.boxShadow = '0 8px 20px rgba(0,0,0,0.08)';
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
(e.currentTarget as HTMLElement).style.transform = 'translateY(0px)'; (e.currentTarget as HTMLElement).style.transform = 'translateY(0px)';
(e.currentTarget as HTMLElement).style.boxShadow = '0 4px 12px rgba(0,0,0,0.05)'; (e.currentTarget as HTMLElement).style.boxShadow = '0 4px 12px rgba(0,0,0,0.05)';
}} }}
> >
<Stack gap="sm"> <Stack gap="sm">
<Group justify="space-between" align="center"> <Group justify="space-between" align="center">
<Text fw={700} fz="lg" c={colors['blue-button']}> <Text fw={700} fz="lg" c={colors['blue-button']}>
{item.name} {item.name}
</Text> </Text>
<Badge color="blue" radius="sm" variant="light" fz="xs"> <Badge color="blue" radius="sm" variant="light" fz="xs">
Aktif Aktif
</Badge> </Badge>
</Group> </Group>
<Group gap="xs"> <Group gap="xs">
<IconMapPin size={18} stroke={1.5} color={colors['blue-button']} /> <IconMapPin size={18} stroke={1.5} />
<Text fz="sm" c="dimmed"> <Text fz="sm">
{item.informasiumum.alamat} {item.informasiumum.alamat}
</Text> </Text>
</Group> </Group>
<Group gap="xs"> <Group gap="xs">
<IconClock size={18} stroke={1.5} color={colors['blue-button']} /> <IconClock size={18} stroke={1.5} />
<Text fz="sm" c="dimmed"> <Text fz="sm">
{item.informasiumum.jamOperasional} {item.informasiumum.jamOperasional}
</Text> </Text>
</Group> </Group>
<Anchor <Group justify="flex-start">
onClick={() => <Button
router.push( bg={colors['blue-button']}
`/darmasaba/kesehatan/data-kesehatan-warga/fasilitas-kesehatan-page/${item.id}` radius="lg"
) size="sm"
} rightSection={<IconChevronRight size={18} />}
c={colors['blue-button']} onClick={() =>
fz="sm" router.push(
fw={600} `/darmasaba/kesehatan/data-kesehatan-warga/fasilitas-kesehatan-page/${item.id}`
style={{ display: 'inline-flex', alignItems: 'center', gap: '4px' }} )
> }
Lihat Detail >
<IconArrowRight size={16} stroke={1.5} /> Lihat Detail
</Anchor> </Button>
</Stack> </Group>
</Card> </Stack>
)) </Card>
)} ))
)}
</Stack> </Stack>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -4,14 +4,16 @@ import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { import {
Box, Box,
Button,
Divider, Divider,
Group, Group,
Modal,
Paper, Paper,
Skeleton, Skeleton,
Stack, Stack,
Text Text
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDisclosure, useShallowEffect } from '@mantine/hooks';
import { IconMail, IconPhone, IconUser } from '@tabler/icons-react'; import { IconMail, IconPhone, IconUser } from '@tabler/icons-react';
import { useParams } from 'next/navigation'; import { useParams } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
@@ -21,6 +23,7 @@ import CreatePendaftaran from '../create/page';
function Page() { function Page() {
const params = useParams(); const params = useParams();
const state = useProxy(jadwalkegiatanState); const state = useProxy(jadwalkegiatanState);
const [opened, { open, close }] = useDisclosure(false);
useShallowEffect(() => { useShallowEffect(() => {
state.findUnique.load(params?.id as string); state.findUnique.load(params?.id as string);
@@ -66,28 +69,38 @@ function Page() {
<Stack gap="sm"> <Stack gap="sm">
<Text fz="lg" fw="bold">Deskripsi Kegiatan</Text> <Text fz="lg" fw="bold">Deskripsi Kegiatan</Text>
<Divider /> <Divider />
<Text ta="justify" fz="md" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: state.findUnique.data.deskripsijadwalkegiatan.deskripsi }} /> <Text ta="justify" fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.deskripsijadwalkegiatan.deskripsi }} />
</Stack> </Stack>
<Stack gap="sm"> <Stack gap="sm">
<Text fz="lg" fw="bold">Layanan yang Tersedia</Text> <Text fz="lg" fw="bold">Layanan yang Tersedia</Text>
<Divider /> <Divider />
<Text ta="justify" fz="md" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: state.findUnique.data.layananjadwalkegiatan.content }} /> <Text ta="justify" fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.layananjadwalkegiatan.content }} />
</Stack> </Stack>
<Stack gap="sm"> <Stack gap="sm">
<Text fz="lg" fw="bold">Syarat & Ketentuan</Text> <Text fz="lg" fw="bold">Syarat & Ketentuan</Text>
<Divider /> <Divider />
<Text ta="justify" fz="md" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: state.findUnique.data.syaratketentuanjadwalkegiatan.content }} /> <Text ta="justify" fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.syaratketentuanjadwalkegiatan.content }} />
</Stack> </Stack>
<Stack gap="sm"> <Stack gap="sm">
<Text fz="lg" fw="bold">Dokumen yang Perlu Dibawa</Text> <Text fz="lg" fw="bold">Dokumen yang Perlu Dibawa</Text>
<Divider /> <Divider />
<Text ta="justify" fz="md" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: state.findUnique.data.dokumenjadwalkegiatan.content }} /> <Text ta="justify" fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.dokumenjadwalkegiatan.content }} />
</Stack> </Stack>
<CreatePendaftaran /> <Stack gap="sm">
<Text fz="lg" fw="bold">Pendaftaran Kegiatan</Text>
<Divider />
<Group>
<Button onClick={open}>Buat Pendaftaran</Button>
</Group>
</Stack>
<Modal opened={opened} onClose={close}>
<CreatePendaftaran />
</Modal>
<Paper p="lg" radius="md" bg={colors['blue-button-trans']} shadow="sm"> <Paper p="lg" radius="md" bg={colors['blue-button-trans']} shadow="sm">
<Stack gap="xs"> <Stack gap="xs">

View File

@@ -49,7 +49,7 @@ function JadwalKegiatanPage() {
> >
<Stack gap="sm"> <Stack gap="sm">
<Group justify="space-between"> <Group justify="space-between">
<Text fw={700} fz="xl"> <Text fw={700} fz="xl" c={colors['blue-button']}>
{item.informasijadwalkegiatan.name} {item.informasijadwalkegiatan.name}
</Text> </Text>
<Text fw={600} fz="sm" c={colors['blue-button']}> <Text fw={600} fz="sm" c={colors['blue-button']}>
@@ -62,20 +62,20 @@ function JadwalKegiatanPage() {
</Group> </Group>
<Group gap="xs"> <Group gap="xs">
<IconClockHour4 size={18} color={colors['blue-button']} /> <IconClockHour4 size={18} />
<Text fz="sm">{item.informasijadwalkegiatan.waktu}</Text> <Text fz="sm">{item.informasijadwalkegiatan.waktu}</Text>
</Group> </Group>
<Group gap="xs"> <Group gap="xs">
<IconMapPin size={18} color={colors['blue-button']} /> <IconMapPin size={18} />
<Text fz="sm">{item.informasijadwalkegiatan.lokasi}</Text> <Text fz="sm">{item.informasijadwalkegiatan.lokasi}</Text>
</Group> </Group>
<Divider my="sm" /> <Divider my="sm" />
<Group justify="flex-end"> <Group justify="flex-start">
<Button <Button
variant="light" bg={colors['blue-button']}
radius="lg" radius="lg"
size="sm" size="sm"
rightSection={<IconChevronRight size={18} />} rightSection={<IconChevronRight size={18} />}
@@ -84,14 +84,6 @@ function JadwalKegiatanPage() {
`/darmasaba/kesehatan/data-kesehatan-warga/jadwal-kegiatan-page/${item.id}` `/darmasaba/kesehatan/data-kesehatan-warga/jadwal-kegiatan-page/${item.id}`
) )
} }
styles={{
root: {
background: colors['blue-button'],
color: 'white',
boxShadow: '0 0 12px rgba(0, 123, 255, 0.4)',
transition: 'all 0.2s ease',
},
}}
> >
Lihat Detail & Daftar Lihat Detail & Daftar
</Button> </Button>

View File

@@ -28,16 +28,18 @@ function DetailInfoWabahPenyakitUser() {
const data = state.findUnique.data; const data = state.findUnique.data;
return ( return (
<Box py={10}> <Box py={10} px={{ base: 'md', md: 100 }}>
{/* Tombol Back */} {/* Tombol Back */}
<Button <Box>
variant="subtle" <Button
onClick={() => router.back()} variant="subtle"
leftSection={<IconArrowBack size={22} color={colors['blue-button']} />} onClick={() => router.back()}
mb={15} leftSection={<IconArrowBack size={22} color={colors['blue-button']} />}
> mb={15}
Kembali >
</Button> Kembali
</Button>
</Box>
{/* Wrapper Detail */} {/* Wrapper Detail */}
<Paper <Paper
@@ -71,7 +73,6 @@ function DetailInfoWabahPenyakitUser() {
<Text fz="lg" fw="bold">Deskripsi</Text> <Text fz="lg" fw="bold">Deskripsi</Text>
<Text <Text
fz="md" fz="md"
c="dimmed"
dangerouslySetInnerHTML={{ __html: data.deskripsiLengkap || '-' }} dangerouslySetInnerHTML={{ __html: data.deskripsiLengkap || '-' }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }} style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/> />

View File

@@ -61,7 +61,7 @@ function Page() {
> >
Informasi Wabah & Penyakit Informasi Wabah & Penyakit
</Text> </Text>
<Text fz="md" c="dimmed" mt={4}> <Text fz="md" mt={4}>
Dapatkan informasi terbaru mengenai wabah dan penyakit yang sedang Dapatkan informasi terbaru mengenai wabah dan penyakit yang sedang
diawasi. diawasi.
</Text> </Text>
@@ -84,7 +84,7 @@ function Page() {
<Center py="6xl"> <Center py="6xl">
<Stack align="center" gap="sm"> <Stack align="center" gap="sm">
<IconInfoCircle size={50} color={colors['blue-button']} /> <IconInfoCircle size={50} color={colors['blue-button']} />
<Text fz="lg" fw={500} c="dimmed"> <Text fz="lg" fw={500} >
Tidak ada data yang cocok dengan pencarian Anda. Tidak ada data yang cocok dengan pencarian Anda.
</Text> </Text>
</Stack> </Stack>
@@ -101,17 +101,35 @@ function Page() {
bg={colors['white-trans-1']} bg={colors['white-trans-1']}
style={{ style={{
transition: 'transform 200ms ease, box-shadow 200ms ease', transition: 'transform 200ms ease, box-shadow 200ms ease',
display: 'flex',
flexDirection: 'column',
}} }}
> >
<Stack gap="sm"> <Stack gap="sm" style={{ flex: 1 }}>
<Image {/* Gambar */}
radius="md" <Box
h={180} h={180}
src={v.image.link} w="100%"
alt={v.name} style={{
fit="cover" overflow: 'hidden',
loading="lazy" borderRadius: '8px',
/> }}
>
<Image
src={v.image?.link}
alt={v.name}
fit="cover"
w="100%"
h="100%"
style={{
objectFit: 'cover',
objectPosition: 'center',
}}
loading="lazy"
/>
</Box>
{/* Judul dan badge */}
<Group justify="space-between" mt="sm"> <Group justify="space-between" mt="sm">
<Text fw={700} fz="lg" c={colors['blue-button']}> <Text fw={700} fz="lg" c={colors['blue-button']}>
{v.name} {v.name}
@@ -120,20 +138,46 @@ function Page() {
Wabah Wabah
</Badge> </Badge>
</Group> </Group>
<Text fz="sm" c="dimmed"> <Text fz="sm" c="dimmed">
Diposting: {v.createdAt.toLocaleDateString()} Diposting:{' '}
{new Date(v.createdAt).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric',
})}
</Text> </Text>
<Divider /> <Divider />
<Text fz="sm" lh={1.5} lineClamp={3} truncate="end">
{v.deskripsiSingkat} {/* Bagian deskripsi dan tombol */}
</Text> <Box style={{ display: 'flex', flexDirection: 'column', flexGrow: 1 }}>
<Button variant="light" radius="md" size="md" onClick={() => router.push(`/darmasaba/kesehatan/info-wabah-penyakit/${v.id}`)}> <Text
Selengkapnya fz="sm"
</Button> lh={1.5}
lineClamp={3}
dangerouslySetInnerHTML={{ __html: v.deskripsiSingkat }}
style={{ flexGrow: 1 }}
/>
<Button
variant="light"
radius="md"
size="md"
mt="md"
onClick={() =>
router.push(`/darmasaba/kesehatan/info-wabah-penyakit/${v.id}`)
}
>
Selengkapnya
</Button>
</Box>
</Stack> </Stack>
</Paper> </Paper>
))} ))}
</SimpleGrid> </SimpleGrid>
)} )}
</Box> </Box>

View File

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

View File

@@ -0,0 +1,78 @@
'use client';
import penangananDarurat from '@/app/admin/(dashboard)/_state/kesehatan/penanganan-darurat/penangananDarurat';
import colors from '@/con/colors';
import { Box, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useParams } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import BackButton from '../../../desa/layanan/_com/BackButto';
function DetailPenangananDaruratUser() {
const state = useProxy(penangananDarurat);
const params = useParams();
useShallowEffect(() => {
state.findUnique.load(params?.id as string);
}, []);
if (!state.findUnique.data) {
return (
<Stack py={40}>
<Skeleton height={400} radius="md" />
<Skeleton height={20} width="80%" radius="md" />
<Skeleton height={20} width="60%" radius="md" />
</Stack>
);
}
const data = state.findUnique.data;
return (
<Box py={20}>
{/* Tombol Back */}
<Box px={{ base: 'md', md: 100 }}>
<BackButton/>
</Box>
{/* Wrapper Detail */}
<Paper
withBorder
w={{ base: '100%', md: '70%', lg: '60%' }}
mx="auto"
bg={colors['white-1']}
p="xl"
radius="lg"
shadow="sm"
>
<Stack gap="md" align="center" ta="center">
<Text fz="xl" fw={700} c={colors['blue-button']}>
{data.name || 'Penanganan Darurat'}
</Text>
{data.image?.link && (
<Image
src={data.image.link}
alt={data.name}
radius="md"
mah={300}
fit="contain"
loading="lazy"
mb="md"
/>
)}
<Box>
<Text
fz="md"
ta="justify"
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
/>
</Box>
</Stack>
</Paper>
</Box>
);
}
export default DetailPenangananDaruratUser;

View File

@@ -3,6 +3,7 @@ import penangananDarurat from '@/app/admin/(dashboard)/_state/kesehatan/penangan
import colors from '@/con/colors' import colors from '@/con/colors'
import { import {
Box, Box,
Button,
Center, Center,
Grid, Grid,
GridCol, GridCol,
@@ -51,7 +52,7 @@ function Page() {
<Text fz={{ base: 30, md: 40 }} c={colors['blue-button']} fw={800} lh={1.2}> <Text fz={{ base: 30, md: 40 }} c={colors['blue-button']} fw={800} lh={1.2}>
Penanganan Darurat Penanganan Darurat
</Text> </Text>
<Text fz="md" c="dimmed" mt={4}> <Text fz="md" mt={4}>
Informasi cepat dan jelas untuk situasi darurat kesehatan Informasi cepat dan jelas untuk situasi darurat kesehatan
</Text> </Text>
</GridCol> </GridCol>
@@ -104,32 +105,28 @@ function Page() {
onMouseLeave={(e) => (e.currentTarget.style.transform = 'translateY(0)')} onMouseLeave={(e) => (e.currentTarget.style.transform = 'translateY(0)')}
> >
<Stack align="center" gap="md"> <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={{ style={{
width: '100%', width: '100%',
aspectRatio: '16/9', height: '100%',
borderRadius: '12px', transition: 'transform 0.4s ease',
overflow: 'hidden',
position: 'relative',
}} }}
> />
<Image </Box>
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>
</Center>
<Stack gap={4} w="100%"> <Stack gap={4} w="100%">
<Text <Text
fz="lg" fz="lg"
@@ -142,13 +139,22 @@ function Page() {
</Text> </Text>
<Box> <Box>
<Text <Text
fz="sm" fz="md"
c="dimmed" lineClamp={3}
lineClamp={4}
dangerouslySetInnerHTML={{ __html: v.deskripsi }} dangerouslySetInnerHTML={{ __html: v.deskripsi }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }} style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/> />
</Box> </Box>
<Button
radius="xl"
size="md"
component="a"
href={`/darmasaba/kesehatan/penanganan-darurat/${v.id}`}
bg={colors['blue-button']}
c="white"
>
Lihat Detail
</Button>
</Stack> </Stack>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -0,0 +1,121 @@
'use client';
import colors from '@/con/colors';
import { Button, Center, Flex, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconCalendar, IconInfoCircle, IconPhone } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import posyanduState from '@/app/admin/(dashboard)/_state/kesehatan/posyandu/posyandu';
export default function DetailPosyanduUser() {
const statePosyandu = useProxy(posyanduState);
const params = useParams();
const router = useRouter();
useShallowEffect(() => {
statePosyandu.findUnique.load(params?.id as string);
}, []);
if (!statePosyandu.findUnique.data) {
return (
<Stack py="xl" px={{ base: 'md', md: 100 }}>
<Skeleton height={500} radius="md" />
</Stack>
);
}
const data = statePosyandu.findUnique.data;
return (
<Stack pos="relative" bg={colors.Bg} py="xl" px={{ base: 'md', md: 100 }} gap="xl">
{/* Tombol Kembali */}
<Group>
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={22} color={colors['blue-button']} />}
mb="sm"
c={colors['blue-button']}
>
Kembali
</Button>
</Group>
{/* Konten utama */}
<Paper
withBorder
p="xl"
radius="lg"
shadow="md"
bg={colors['white-trans-1']}
maw={800}
mx="auto"
>
<Stack gap="md">
{/* Header */}
<Text
ta="center"
fz={{ base: '1.8rem', md: '2.2rem' }}
fw={700}
c={colors['blue-button']}
>
{data.name || 'Posyandu Desa'}
</Text>
{/* Gambar */}
{data.image?.link ? (
<Center>
<Image
src={data.image.link}
alt={`Gambar ${data.name}`}
w="100%"
h={300}
radius="md"
fit="cover"
loading="lazy"
/>
</Center>
) : (
<Center>
<Text fz="sm" c="dimmed">
Tidak ada gambar
</Text>
</Center>
)}
{/* Info utama */}
<Stack gap="sm" mt="md">
<Flex align="center" gap="xs">
<IconPhone size={18} stroke={1.5} />
<Text fz="sm" c="dimmed">
{data.nomor || 'Nomor tidak tersedia'}
</Text>
</Flex>
<Flex align="center" gap="xs">
<IconCalendar size={18} stroke={1.5} />
<Text
fz="sm"
c="dimmed"
dangerouslySetInnerHTML={{ __html: data.jadwalPelayanan || '-' }}
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
/>
</Flex>
<Flex align="center" gap="xs">
<IconInfoCircle size={18} stroke={1.5} />
<Text
fz="sm"
c="dimmed"
lh={1.6}
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
/>
</Flex>
</Stack>
</Stack>
</Paper>
</Stack>
);
}

View File

@@ -1,18 +1,19 @@
'use client' 'use client'
import posyandustate from "@/app/admin/(dashboard)/_state/kesehatan/posyandu/posyandu"; import posyandustate from "@/app/admin/(dashboard)/_state/kesehatan/posyandu/posyandu";
import colors from "@/con/colors"; import colors from "@/con/colors";
import { Badge, Box, Center, Flex, Group, Image, List, ListItem, Pagination, Paper, SimpleGrid, Skeleton, Spoiler, Stack, Text, TextInput } from "@mantine/core"; import { Badge, Box, Button, Center, Flex, Group, Image, List, ListItem, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks"; import { useDebouncedValue, useShallowEffect } from "@mantine/hooks";
import { IconCalendar, IconInfoCircle, IconPhone, IconSearch } from "@tabler/icons-react"; import { IconCalendar, IconInfoCircle, IconPhone, IconSearch } from "@tabler/icons-react";
import { useState } from "react"; import { useState } from "react";
import { useProxy } from "valtio/utils"; import { useProxy } from "valtio/utils";
import BackButton from "../../desa/layanan/_com/BackButto"; import BackButton from "../../desa/layanan/_com/BackButto";
import { useDebouncedValue } from "@mantine/hooks"; import { useTransitionRouter } from "next-view-transitions";
export default function Page() { export default function Page() {
const state = useProxy(posyandustate); const state = useProxy(posyandustate);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const router = useTransitionRouter()
const { data, page, totalPages, loading, load } = state.findMany; const { data, page, totalPages, loading, load } = state.findMany;
@@ -133,33 +134,41 @@ export default function Page() {
loading="lazy" loading="lazy"
/> />
</Center> </Center>
<Flex align="center" gap="xs"> <Flex align="flex-start" gap="xs">
<IconPhone size={18} stroke={1.5} /> <IconPhone size={18} stroke={1.5} style={{ marginTop: 3 }} />
<Text fz="sm" c="dimmed"> <Box>
{v.nomor || "Tidak tersedia"} <Text fz="sm" c="dimmed" lh={1.4}>
</Text> {v.nomor || "Tidak tersedia"}
</Text>
</Box>
</Flex> </Flex>
<Flex align="center" gap="xs">
<IconCalendar size={18} stroke={1.5} /> <Flex align="flex-start" gap="xs">
<Text fz="sm" c="dimmed"> <IconCalendar size={18} stroke={1.5} style={{ marginTop: 3 }} />
Jadwal:{" "} <Box>
<span style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: v.jadwalPelayanan }} /> <Text fz="sm" c="dimmed" lh={1.4}>
</Text> <strong>Jadwal:</strong>{" "}
<span
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
dangerouslySetInnerHTML={{ __html: v.jadwalPelayanan }}
/>
</Text>
</Box>
</Flex> </Flex>
<Spoiler
key={`spoiler-${v.id}`} <Flex align="flex-start" gap="xs">
maxHeight={70} <IconInfoCircle size={18} stroke={1.5} style={{ marginTop: 3 }} />
showLabel="Lihat selengkapnya"
hideLabel="Sembunyikan"
transitionDuration={300}
>
<Text <Text
fz="sm" fz="sm"
lh={1.5} lh={1.5}
c="dimmed"
dangerouslySetInnerHTML={{ __html: v.deskripsi }} dangerouslySetInnerHTML={{ __html: v.deskripsi }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }} style={{ wordBreak: "break-word", whiteSpace: "normal" }}
lineClamp={3}
truncate="end"
/> />
</Spoiler> </Flex>
<Button radius="lg" size="md" variant="outline" onClick={() => router.push(`/darmasaba/kesehatan/posyandu/${v.id}`)}>Detail</Button>
</Stack> </Stack>
</Paper> </Paper>
))} ))}

View File

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

View File

@@ -1,4 +1,5 @@
'use client' 'use client'
import programKesehatan from "@/app/admin/(dashboard)/_state/kesehatan/program-kesehatan/programKesehatan";
import colors from "@/con/colors"; import colors from "@/con/colors";
import { import {
Box, Box,
@@ -15,9 +16,9 @@ import {
Stack, Stack,
Text, Text,
TextInput, TextInput,
Tooltip, Transition
Transition,
} from "@mantine/core"; } from "@mantine/core";
import { useDebouncedValue, useShallowEffect } from "@mantine/hooks";
import { import {
IconBarbell, IconBarbell,
IconCalendar, IconCalendar,
@@ -26,12 +27,10 @@ import {
IconUser, IconUser,
IconUsersGroup, IconUsersGroup,
} from "@tabler/icons-react"; } 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 { useRouter } from "next/navigation";
import { useState } from "react";
import { useProxy } from "valtio/utils";
import BackButton from "../../desa/layanan/_com/BackButto";
const manfaatProgram = [ const manfaatProgram = [
{ {
@@ -88,7 +87,7 @@ export default function Page() {
> >
Program Kesehatan Desa Program Kesehatan Desa
</Text> </Text>
<Text fz="lg" c="dimmed" mt="xs"> <Text fz="lg" mt="xs">
Temukan berbagai program kesehatan untuk mendukung kualitas hidup Temukan berbagai program kesehatan untuk mendukung kualitas hidup
masyarakat Darmasaba. masyarakat Darmasaba.
</Text> </Text>
@@ -126,17 +125,36 @@ export default function Page() {
className="hover-scale" className="hover-scale"
> >
<Stack gap="md"> <Stack gap="md">
<Box h={180} w="100%"> <Center>
<Image <Box
src={v.image?.link} style={{
alt={v.name} width: '100%',
radius="xl" height: 180, // 🔥 tinggi fix biar semua seragam
w="100%" borderRadius: 12,
h="100%" overflow: 'hidden',
fit="cover" position: 'relative',
loading="lazy" backgroundColor: '#f0f2f5', // fallback kalau gambar loading
/> }}
</Box> >
<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>
</Center>
<Box px="lg" pb="lg"> <Box px="lg" pb="lg">
<Text <Text
@@ -149,7 +167,7 @@ export default function Page() {
</Text> </Text>
<Text <Text
fz="sm" fz="sm"
c="dimmed" ta={"justify"}
lineClamp={3} lineClamp={3}
dangerouslySetInnerHTML={{ __html: v.deskripsi }} dangerouslySetInnerHTML={{ __html: v.deskripsi }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }} style={{ wordBreak: "break-word", whiteSpace: "normal" }}
@@ -175,7 +193,6 @@ export default function Page() {
<Text size="sm">Admin Desa</Text> <Text size="sm">Admin Desa</Text>
</Group> </Group>
</Group> </Group>
<Tooltip label="Lihat detail program" withArrow>
<Button <Button
mt="lg" mt="lg"
fullWidth fullWidth
@@ -192,7 +209,6 @@ export default function Page() {
> >
Lihat Detail Lihat Detail
</Button> </Button>
</Tooltip>
</Box> </Box>
</Stack> </Stack>
</Paper> </Paper>
@@ -230,7 +246,7 @@ export default function Page() {
> >
Manfaat Program Kesehatan Manfaat Program Kesehatan
</Text> </Text>
<Text fz="lg" c="dimmed" maw={700}> <Text fz="lg" maw={700}>
Program kesehatan Desa Darmasaba berperan penting dalam meningkatkan Program kesehatan Desa Darmasaba berperan penting dalam meningkatkan
kesejahteraan dan kualitas hidup warganya. kesejahteraan dan kualitas hidup warganya.
</Text> </Text>
@@ -260,7 +276,7 @@ export default function Page() {
<Text ta="center" fw="bold" fz="xl" c={colors["blue-button"]}> <Text ta="center" fw="bold" fz="xl" c={colors["blue-button"]}>
{v.title} {v.title}
</Text> </Text>
<Text ta="center" fz="sm" c="dimmed"> <Text ta="center" fz="sm">
{v.desc} {v.desc}
</Text> </Text>
</Stack> </Stack>

View File

@@ -43,7 +43,7 @@ function Page() {
<Text fz={{ base: "2rem", md: "2.5rem" }} c={colors["blue-button"]} fw="bold"> <Text fz={{ base: "2rem", md: "2.5rem" }} c={colors["blue-button"]} fw="bold">
Daftar Puskesmas Daftar Puskesmas
</Text> </Text>
<Text fz="sm" c="dimmed"> <Text fz="md">
Temukan informasi lengkap mengenai layanan, kontak, dan lokasi Puskesmas Darmasaba Temukan informasi lengkap mengenai layanan, kontak, dan lokasi Puskesmas Darmasaba
</Text> </Text>
</GridCol> </GridCol>
@@ -93,20 +93,23 @@ function Page() {
<Text fw={600} fz="lg" lineClamp={1}>{v.name}</Text> <Text fw={600} fz="lg" lineClamp={1}>{v.name}</Text>
<Badge color="blue" variant="light" radius="sm" fz="xs">Aktif</Badge> <Badge color="blue" variant="light" radius="sm" fz="xs">Aktif</Badge>
</Group> </Group>
<Stack gap={4}> <Stack gap={6}>
<Group gap="xs"> <Group gap="xs" align="flex-start" wrap="nowrap">
<IconMapPin size={16} /> <Box pt={2}><IconMapPin size={16} /></Box>
<Text fz="sm" c="dimmed" lineClamp={2}>{v.alamat}</Text> <Text fz="sm" c="dimmed">{v.alamat}</Text>
</Group> </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> <Text fz="sm" c="dimmed">{v.kontak.kontakPuskesmas}</Text>
</Group> </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> <Text fz="sm" c="dimmed">{v.kontak.email}</Text>
</Group> </Group>
</Stack> </Stack>
<Anchor <Anchor
href={`/darmasaba/kesehatan/puskesmas/${v.id}`} href={`/darmasaba/kesehatan/puskesmas/${v.id}`}
fz="sm" fz="sm"

View File

@@ -71,8 +71,11 @@ function Page() {
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
/> />
</Group> </Group>
<Text fz="lg" c={'black'}> <Text fz="md" >
Desa Darmasaba menjaga dan mengembangkan lingkungan demi kesejahteraan warganya. Fokus utama meliputi penghijauan, pengelolaan sampah, dan perlindungan kawasan hijau. Desa Darmasaba menjaga dan mengembangkan lingkungan demi kesejahteraan warganya.
</Text>
<Text fz="md">
Fokus utama meliputi penghijauan, pengelolaan sampah, dan perlindungan kawasan hijau.
</Text> </Text>
</Box> </Box>
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>

View File

@@ -0,0 +1,63 @@
// Create a new component: components/EdukasiCard.tsx
'use client';
import { Box, Paper, Stack, Text } from '@mantine/core';
import { ReactNode } from 'react';
interface EdukasiCardProps {
icon: ReactNode;
title: string;
description: string;
color?: string;
}
export function EdukasiCard({ icon, title, description, color = '#1e88e5' }: EdukasiCardProps) {
return (
<Paper
p={{ base: 'md', md: 'lg' }}
radius="md"
shadow="sm"
withBorder
style={{
height: '100%',
transition: 'transform 0.2s, box-shadow 0.2s',
'&:hover': {
transform: 'translateY(-4px)',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)'
}
}}
>
<Stack h="100%" justify="space-between" gap="md">
<Box>
<Stack align="center" gap="xs" mb="md">
<Box style={{ color }}>{icon}</Box>
<Text
fz={{ base: 'h5', md: 'h4' }}
fw={700}
c={color}
ta="center"
lineClamp={2}
style={{
wordBreak: 'break-word',
minHeight: '3.5rem',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
dangerouslySetInnerHTML={{ __html: title }}
/>
</Stack>
<Text
size="sm"
style={{
wordBreak: 'break-word',
lineHeight: 1.6,
color: 'var(--mantine-color-gray-7)'
}}
dangerouslySetInnerHTML={{ __html: description }}
/>
</Box>
</Stack>
</Paper>
);
}

View File

@@ -1,128 +1,103 @@
'use client' 'use client';
import stateEdukasiLingkungan from '@/app/admin/(dashboard)/_state/lingkungan/edukasi-lingkungan';
import colors from '@/con/colors'; import { Box, Container, SimpleGrid, Skeleton, Stack, Text } from '@mantine/core';
import { Box, Paper, SimpleGrid, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconLeaf, IconPlant2, IconRecycle } from '@tabler/icons-react'; import { IconLeaf, IconPlant2, IconRecycle } from '@tabler/icons-react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto'; import BackButton from '../../desa/layanan/_com/BackButto';
function Page() { import stateEdukasiLingkungan from '@/app/admin/(dashboard)/_state/lingkungan/edukasi-lingkungan';
const tujuan = useProxy(stateEdukasiLingkungan.stateTujuanEdukasi.findById) import colors from '@/con/colors';
const materi = useProxy(stateEdukasiLingkungan.stateMateriEdukasiLingkungan.findById) import { EdukasiCard } from './component/edukasiCard';
const contoh = useProxy(stateEdukasiLingkungan.stateContohEdukasiLingkungan.findById)
function LoadingSkeleton() {
return (
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="lg">
{[1, 2, 3].map((item) => (
<Skeleton key={item} height={300} radius="md" />
))}
</SimpleGrid>
);
}
export default function EdukasiLingkunganPage() {
const tujuan = useProxy(stateEdukasiLingkungan.stateTujuanEdukasi.findById);
const materi = useProxy(stateEdukasiLingkungan.stateMateriEdukasiLingkungan.findById);
const contoh = useProxy(stateEdukasiLingkungan.stateContohEdukasiLingkungan.findById);
useShallowEffect(() => { useShallowEffect(() => {
tujuan.load('edit') tujuan.load('edit');
materi.load('edit') materi.load('edit');
contoh.load('edit') contoh.load('edit');
}, []) }, []);
if (tujuan.loading || !tujuan.data || materi.loading || !materi.data || contoh.loading || !contoh.data) { const isLoading = tujuan.loading || !tujuan.data ||
materi.loading || !materi.data ||
contoh.loading || !contoh.data;
if (isLoading) {
return ( return (
<Stack py={20}> <Stack py="xl" px={{ base: 'md', md: 'xl' }}>
<Skeleton radius="md" height={600} /> <BackButton />
<LoadingSkeleton />
</Stack> </Stack>
); );
} }
return ( return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22"> <Stack bg={colors.Bg} py="xl" gap="xl" px={{ base: 'md', md: 'xl' }}>
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
<BackButton /> <BackButton />
</Box> </Box>
<Box px={{ base: 'md', md: 100 }} pb={20}> <Container size="lg" ta="center">
<Text ta={'center'} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw="bold"> <Text
component="h1"
fz={{ base: 'h2', md: '2.5rem' }}
c={colors['blue-button']}
fw={700}
mb="md"
>
Edukasi Lingkungan Edukasi Lingkungan
</Text> </Text>
<Text ta={'center'} fz="h4" c="black"> <Text
fz={{ base: 'md', md: 'lg' }}
maw={800}
mx="auto"
>
Program edukasi ini membimbing masyarakat untuk peduli dan bertanggung jawab terhadap alam, Program edukasi ini membimbing masyarakat untuk peduli dan bertanggung jawab terhadap alam,
meningkatkan kesehatan, kenyamanan, dan keberlanjutan hidup bersama. meningkatkan kesehatan, kenyamanan, dan keberlanjutan hidup bersama.
</Text> </Text>
</Box> </Container>
<Box px={{ base: 'md', md: 100 }}> <Container size="xl">
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg" style={{ alignItems: 'stretch' }}> <SimpleGrid
{/* Tujuan Edukasi Lingkungan */} cols={{ base: 1, sm: 2, lg: 3 }}
<Box style={{ display: 'flex', height: '100%' }}> spacing="xl"
<Paper p={20} bg={colors['white-trans-1']} shadow="md" radius="md" style={{ width: '100%', display: 'flex', flexDirection: 'column' }}> verticalSpacing={{ base: 'md', md: 'xl' }}
<Stack gap="md"> >
<Box> <EdukasiCard
<Tooltip label={tujuan.data?.judul} position="top" withArrow> icon={<IconLeaf size={45} />}
<Stack gap={4} align="center"> title={tujuan.data?.judul || ''}
<IconLeaf size={28} color={colors['blue-button']} /> description={tujuan.data?.deskripsi || ''}
<Text fz="h3" fw="bold" c={colors['blue-button']} ta="center"> color={colors['blue-button']}
{tujuan.data?.judul} />
</Text>
</Stack> <EdukasiCard
</Tooltip> icon={<IconRecycle size={45} />}
</Box> title={materi.data?.judul || ''}
<Text description={materi.data?.deskripsi || ''}
style={{ color={colors['blue-button']}
wordBreak: "break-word", />
whiteSpace: "normal",
flexGrow: 1 <EdukasiCard
}} icon={<IconPlant2 size={45} />}
dangerouslySetInnerHTML={{ __html: tujuan.data?.deskripsi || '' }} title={contoh.data?.judul || ''}
/> description={contoh.data?.deskripsi || ''}
<Box style={{ flexGrow: 1 }} /> color={colors['blue-button']}
</Stack> />
</Paper>
</Box>
{/* Materi Edukasi Lingkungan */}
<Box style={{ display: 'flex', height: '100%' }}>
<Paper p={20} bg={colors['white-trans-1']} shadow="md" radius="md">
<Stack gap="md">
<Box>
<Tooltip label={materi.data?.judul} position="top" withArrow>
<Stack gap={4} align="center">
<IconRecycle size={28} color={colors['blue-button']} />
<Text fz="h3" fw="bold" c={colors['blue-button']} ta="center" dangerouslySetInnerHTML={{ __html: materi.data?.judul || '' }} />
</Stack>
</Tooltip>
</Box>
<Text
style={{
wordBreak: "break-word",
whiteSpace: "normal",
flexGrow: 1
}}
dangerouslySetInnerHTML={{ __html: materi.data?.deskripsi || '' }}
/>
<Box style={{ flexGrow: 1 }} />
</Stack>
</Paper>
</Box>
{/* Contoh Edukasi Lingkungan */}
<Box style={{ display: 'flex', height: '100%' }}>
<Paper p={20} bg={colors['white-trans-1']} shadow="md" radius="md">
<Stack gap="md">
<Box>
<Tooltip label={contoh.data?.judul} position="top" withArrow>
<Stack gap={4} align="center">
<IconPlant2 size={28} color={colors['blue-button']} />
<Text fz="h3" fw="bold" c={colors['blue-button']} ta="center">
{contoh.data?.judul}
</Text>
</Stack>
</Tooltip>
</Box>
<Text
style={{
wordBreak: "break-word",
whiteSpace: "normal",
flexGrow: 1
}}
dangerouslySetInnerHTML={{ __html: contoh.data?.deskripsi || '' }}
/>
</Stack>
</Paper>
</Box>
</SimpleGrid> </SimpleGrid>
</Box> </Container>
</Stack> </Stack>
); );
} }
export default Page;

View File

@@ -1,80 +1,132 @@
/* eslint-disable react-hooks/exhaustive-deps */ 'use client';
'use client'
import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong';
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'; import { useProxy } from 'valtio/utils';
import { Box, Image, Paper, Skeleton, Stack, Text, Title, Group } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconCalendar, IconMapPin, IconUsers } from '@tabler/icons-react';
import { useParams } from 'next/navigation';
import colors from '@/con/colors';
import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong';
function DetailKegiatanDesaUser() {
const kegiatanDesaState = useProxy(gotongRoyongState.kegiatanDesa);
const params = useParams();
useShallowEffect(() => {
kegiatanDesaState.findUnique.load(params?.id as string);
}, []);
const data = kegiatanDesaState.findUnique.data;
function Page() { if (!data) {
const params = useParams<{ id: string }>();
const id = Array.isArray(params.id) ? params.id[0] : params.id;
const state = useProxy(gotongRoyongState.kegiatanDesa)
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 ( return (
<Center> <Stack py={20}>
<Skeleton height={500} /> <Skeleton height={400} radius="md" />
</Center> <Skeleton height={20} width="70%" />
<Skeleton height={15} width="50%" />
<Skeleton height={300} radius="md" />
</Stack>
); );
} }
if (!state.findUnique.data) {
return (
<Center>
<Text>Data tidak ditemukan</Text>
</Center>
);
}
return ( return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"} px={{ base: "md", md: 0 }}> <Box py={30}>
<Box px={{ base: "md", md: 100 }}><BackButton /></Box> {/* Header Gambar */}
<Container w={{ base: "100%", md: "50%" }} >
<Box pb={20}> {/* Konten */}
<Text ta={"center"} fz={"2.4rem"} c={colors["blue-button"]} fw={"bold"}> <Paper
{state.findUnique.data?.judul} bg={colors['white-1']}
</Text> p="lg"
<Text radius="md"
ta={"center"} shadow="sm"
fw={"bold"} maw={900}
fz={"1.5rem"} mx="auto"
> >
Informasi Kegiatan Gotong Royong <Stack gap="md">
</Text> {data.image?.link && (
</Box> <Image
<Image src={state.findUnique.data?.image?.link || ''} alt='' w={"100%"} loading="lazy"/> src={data.image.link}
</Container> alt={data.judul || 'Kegiatan Desa'}
<Box px={{ base: "md", md: 100 }}> radius="md"
<Stack gap={"xs"}> fit="cover"
<Text py={20} style={{wordBreak: "break-word", whiteSpace: "normal"}} fz={{ base: "sm", md: "lg" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: state.findUnique.data?.deskripsiLengkap || '' }} /> h={300}
mb="xl"
style={{ objectPosition: 'center', width: '100%' }}
/>
)}
{/* Judul */}
<Title order={2} c={colors['blue-button']}>
{data.judul || 'Kegiatan Desa'}
</Title>
{/* Meta Info */}
<Group gap="xl" c="dimmed">
<Group gap={6}>
<IconCalendar size={18} />
<Text fz="sm">
{data.tanggal ? new Date(data.tanggal).toLocaleDateString('id-ID', { day: 'numeric', month: 'long', year: 'numeric' }) : '-'}
</Text>
</Group>
{data.lokasi && (
<Group gap={6}>
<IconMapPin size={18} />
<Text fz="sm">{data.lokasi}</Text>
</Group>
)}
{data.partisipan && (
<Group gap={6}>
<IconUsers size={18} />
<Text fz="sm">{data.partisipan}</Text>
</Group>
)}
</Group>
{/* Deskripsi Singkat */}
{data.deskripsiSingkat && (
<Box>
<Text fw={600} fz="lg" mb={4}>
Ringkasan
</Text>
<Text
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{ __html: data.deskripsiSingkat }}
/>
</Box>
)}
{/* Deskripsi Lengkap */}
{data.deskripsiLengkap && (
<Box>
<Text fw={600} fz="lg" mb={4}>
Detail Kegiatan
</Text>
<Text
fz="md"
c="dimmed"
style={{ lineHeight: 1.6 }}
dangerouslySetInnerHTML={{ __html: data.deskripsiLengkap }}
/>
</Box>
)}
{/* Kategori */}
{data.kategoriKegiatan?.nama && (
<Box mt="md">
<Text fw={600} fz="lg">
Kategori
</Text>
<Text fz="md" c="dimmed">
{data.kategoriKegiatan.nama}
</Text>
</Box>
)}
</Stack> </Stack>
</Box> </Paper>
</Stack> </Box>
); );
} }
export default Page; export default DetailKegiatanDesaUser;

View File

@@ -1,323 +1,5 @@
// 'use client'
// import colors from '@/con/colors';
// import { Box, Container, Grid, GridCol, Stack, Tabs, TabsList, TabsTab, Text, TextInput } from '@mantine/core';
// import { IconSearch } from '@tabler/icons-react';
// import { usePathname, useRouter, useSearchParams } from 'next/navigation';
// import React, { useEffect, useState } from 'react';
// import BackButton from '../../../desa/layanan/_com/BackButto';
// type HeaderSearchProps = {
// placeholder?: string;
// searchIcon?: React.ReactNode;
// value?: string;
// onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
// children?: React.ReactNode;
// };
// function LayoutTabsGotongRoyong({
// children,
// placeholder = "pencarian",
// searchIcon = <IconSearch size={20} />
// }: HeaderSearchProps) {
// const router = useRouter();
// const pathname = usePathname();
// const searchParams = useSearchParams();
// // Get active tab from URL path
// const activeTab = pathname.split('/').pop() || 'semua';
// // Get initial search value from URL
// const initialSearch = searchParams.get('search') || '';
// const [searchValue, setSearchValue] = useState(initialSearch);
// const [searchTimeout, setSearchTimeout] = useState<number | null>(null);
// // Update active tab state when pathname changes
// const [activeTabState, setActiveTabState] = useState(activeTab);
// useEffect(() => {
// setActiveTabState(activeTab);
// }, [activeTab]);
// // Clean up timeouts on unmount
// useEffect(() => {
// return () => {
// if (searchTimeout !== null) {
// clearTimeout(searchTimeout);
// }
// };
// }, [searchTimeout]);
// // Handle search input change with debounce
// const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
// const value = event.target.value;
// setSearchValue(value);
// // Clear previous timeout
// if (searchTimeout !== null) {
// clearTimeout(searchTimeout);
// }
// // Set new timeout
// const newTimeout = window.setTimeout(() => {
// const params = new URLSearchParams(searchParams.toString());
// if (value) {
// params.set('search', value);
// } else {
// params.delete('search');
// }
// // Only update URL if the search value has actually changed
// if (params.toString() !== searchParams.toString()) {
// router.push(`/darmasaba/lingkungan/gotong-royong/${activeTab}?${params.toString()}`);
// }
// }, 500); // 500ms debounce delay
// setSearchTimeout(newTimeout);
// };
// const tabs = [
// {
// label: "Semua",
// value: "semua",
// href: "/darmasaba/lingkungan/gotong-royong/semua"
// },
// {
// label: "Kebersihan",
// value: "kebersihan",
// href: "/darmasaba/lingkungan/gotong-royong/kebersihan"
// },
// {
// label: "Infrastruktur",
// value: "infrastruktur",
// href: "/darmasaba/lingkungan/gotong-royong/infrastruktur"
// },
// {
// label: "Sosial",
// value: "sosial",
// href: "/darmasaba/lingkungan/gotong-royong/sosial"
// },
// {
// label: "Lingkungan",
// value: "lingkungan",
// href: "/darmasaba/lingkungan/gotong-royong/lingkungan"
// }
// ];
// const handleTabChange = (value: string | null) => {
// if (!value) return;
// const tab = tabs.find(t => t.value === value);
// if (tab) {
// const params = new URLSearchParams(searchParams.toString());
// router.push(`/darmasaba/lingkungan/gotong-royong/${value}${params.toString() ? `?${params.toString()}` : ''}`);
// }
// };
// return (
// <Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
// {/* Header */}
// <Box px={{ base: "md", md: 100 }}>
// <BackButton />
// </Box>
// <Container size="lg" px="md">
// <Stack align="center" gap="0" >
// <Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" ta="center">
// Gotong Royong Desa Darmasaba
// </Text>
// <Text ta="center" px="md">
// Gotong royong rutin dilakukan oleh warga desa untuk meningkatkan kualitas hidup dan kesejahteraan masyarakat Desa Darmasaba
// </Text>
// </Stack>
// </Container>
// <Tabs
// color={colors['blue-button']}
// variant="pills"
// value={activeTabState}
// onChange={handleTabChange}
// >
// <Box px={{ base: "md", md: 100 }} py="md" bg={colors['BG-trans']}>
// <Grid>
// <GridCol span={{ base: 12, md: 9, lg: 8, xl: 9 }}>
// <TabsList>
// {tabs.map((tab, index) => (
// <TabsTab
// key={index}
// value={tab.value}
// onClick={() => router.push(tab.href)}
// >
// {tab.label}
// </TabsTab>
// ))}
// </TabsList>
// </GridCol>
// <GridCol span={{ base: 12, md: 3, lg: 4, xl: 3 }}>
// <TextInput
// radius="lg"
// placeholder={placeholder}
// leftSection={searchIcon}
// w="100%"
// value={searchValue}
// onChange={handleSearchChange}
// />
// </GridCol>
// </Grid>
// </Box>
// {children}
// </Tabs>
// </Stack>
// );
// }
// export default LayoutTabsGotongRoyong;
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
// 'use client'
// import colors from '@/con/colors';
// import { Box, Group, Stack, Tabs, TabsList, TabsTab, Text, TextInput } from '@mantine/core';
// import { IconSearch } from '@tabler/icons-react';
// import { usePathname, useRouter, useSearchParams } from 'next/navigation';
// import React, { useEffect, useState } from 'react';
// import BackButton from '../../layanan/_com/BackButto';
// type HeaderSearchProps = {
// placeholder?: string;
// searchIcon?: React.ReactNode;
// value?: string;
// onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
// children?: React.ReactNode;
// };
// function LayoutTabsBerita({
// children,
// placeholder = "pencarian",
// searchIcon = <IconSearch size={20} />
// }: HeaderSearchProps) {
// const router = useRouter();
// const pathname = usePathname();
// const searchParams = useSearchParams();
// const activeTab = pathname.split('/').pop() || 'semua';
// const initialSearch = searchParams.get('search') || '';
// const [searchValue, setSearchValue] = useState(initialSearch);
// const [searchTimeout, setSearchTimeout] = useState<number | null>(null);
// const [activeTabState, setActiveTabState] = useState(activeTab);
// useEffect(() => {
// setActiveTabState(activeTab);
// }, [activeTab]);
// useEffect(() => {
// return () => {
// if (searchTimeout !== null) clearTimeout(searchTimeout);
// };
// }, [searchTimeout]);
// const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
// const value = event.target.value;
// setSearchValue(value);
// if (searchTimeout !== null) clearTimeout(searchTimeout);
// const newTimeout = window.setTimeout(() => {
// const params = new URLSearchParams(searchParams.toString());
// if (value) params.set('search', value);
// else params.delete('search');
// if (params.toString() !== searchParams.toString()) {
// router.push(`/darmasaba/desa/berita/${activeTab}?${params.toString()}`);
// }
// }, 500);
// setSearchTimeout(newTimeout);
// };
// const tabs = [
// { label: "Semua", value: "semua", href: "/darmasaba/desa/berita/semua" },
// { label: "Budaya", value: "budaya", href: "/darmasaba/desa/berita/budaya" },
// { label: "Pemerintahan", value: "pemerintahan", href: "/darmasaba/desa/berita/pemerintahan" },
// { label: "Ekonomi", value: "ekonomi", href: "/darmasaba/desa/berita/ekonomi" },
// { label: "Pembangunan", value: "pembangunan", href: "/darmasaba/desa/berita/pembangunan" },
// { label: "Sosial", value: "sosial", href: "/darmasaba/desa/berita/sosial" },
// { label: "Teknologi", value: "teknologi", href: "/darmasaba/desa/berita/teknologi" },
// ];
// const handleTabChange = (value: string | null) => {
// if (!value) return;
// const tab = tabs.find(t => t.value === value);
// if (tab) {
// const params = new URLSearchParams(searchParams.toString());
// router.push(`/darmasaba/desa/berita/${value}${params.toString() ? `?${params.toString()}` : ''}`);
// }
// };
// return (
// <Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
// {/* Header */}
// <Box px={{ base: "md", md: 100 }}>
// <BackButton />
// </Box>
// <Box px={{ base: 'md', md: 100 }}>
// <Group justify='space-between' align="center">
// <Stack gap="0">
// <Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" >
// Portal Berita Darmasaba
// </Text>
// <Text>
// Temukan berbagai potensi dan keunggulan yang dimiliki Desa Darmasaba
// </Text>
// </Stack>
// <Box>
// <TextInput
// radius="lg"
// placeholder={placeholder}
// leftSection={searchIcon}
// w="100%"
// value={searchValue}
// onChange={handleSearchChange}
// />
// </Box>
// </Group>
// </Box>
// <Tabs
// color={colors['blue-button']}
// variant="pills"
// value={activeTabState}
// onChange={handleTabChange}
// >
// <Box px={{ base: "md", md: 100 }} py="md" bg={colors['BG-trans']}>
// {/* SCROLLABLE TABS */}
// <Box style={{ overflowX: 'auto', whiteSpace: 'nowrap' }}>
// <TabsList style={{ display: 'flex', flexWrap: 'nowrap', gap: '0.5rem' }}>
// {tabs.map((tab, index) => (
// <TabsTab
// key={index}
// value={tab.value}
// onClick={() => router.push(tab.href)}
// style={{
// flex: '0 0 auto', // Prevent shrinking
// minWidth: 100, // optional: makes them touch-friendly
// textAlign: 'center'
// }}
// >
// {tab.label}
// </TabsTab>
// ))}
// </TabsList>
// </Box>
// </Box>
// {children}
// </Tabs>
// </Stack>
// );
// }
// export default LayoutTabsBerita;
'use client' 'use client'
import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong'; import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong';
@@ -402,7 +84,7 @@ function LayoutTabsGotongRoyong({ children }: { children: React.ReactNode }) {
<Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold"> <Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold">
Portal Gotong royong Darmasaba Portal Gotong royong Darmasaba
</Text> </Text>
<Text>Temukan berbagai kegiatan lingkungan yang dimiliki Desa Darmasaba</Text> <Text fz="md">Temukan berbagai kegiatan lingkungan yang dimiliki Desa Darmasaba</Text>
</Stack> </Stack>
<Box> <Box>
<TextInput <TextInput

View File

@@ -43,15 +43,48 @@ export default function Page() {
const loadingGrid = state.findMany.loading; const loadingGrid = state.findMany.loading;
const loadingFeatured = featured.loading; const loadingFeatured = featured.loading;
// Load featured data once on component mount
useEffect(() => { useEffect(() => {
if (!featured.data && !loadingFeatured) { let mounted = true;
gotongRoyongState.kegiatanDesa.findFirst.load();
const loadFeatured = async () => {
try {
if (!featured.data && !loadingFeatured) {
await gotongRoyongState.kegiatanDesa.findFirst.load();
}
} catch (error) {
console.error('Error loading featured data:', error);
}
};
if (mounted) {
loadFeatured();
} }
}, [featured.data, loadingFeatured]);
return () => {
mounted = false;
};
}, []); // Empty dependency array to run only once on mount
useEffect(() => { useEffect(() => {
const limit = 3; let mounted = true;
state.findMany.load(page, limit, search);
const loadData = async () => {
try {
const limit = 3;
await state.findMany.load(page, limit, search);
} catch (error) {
console.error('Error loading data:', error);
}
};
if (mounted) {
loadData();
}
return () => {
mounted = false;
};
}, [page, search]); }, [page, search]);
const handlePageChange = (newPage: number) => { const handlePageChange = (newPage: number) => {
@@ -59,7 +92,9 @@ export default function Page() {
if (search) url.set('search', search); if (search) url.set('search', search);
if (newPage > 1) url.set('page', newPage.toString()); if (newPage > 1) url.set('page', newPage.toString());
else url.delete('page'); else url.delete('page');
router.replace(`?${url.toString()}`);
// Use push instead of replace to keep browser history
router.push(`?${url.toString()}`, { scroll: false });
}; };
const featuredData = featured.data; const featuredData = featured.data;

View File

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

View File

@@ -1,9 +1,9 @@
'use client' 'use client'
import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah'; import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Center, Flex, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core'; import { Box, Center, Flex, Group, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { Icon, IconChartLine, IconClipboardTextFilled, IconLeaf, IconRecycle, IconScale, IconSearch, IconTent, IconTrashFilled, IconTrophy, IconTruckFilled } from '@tabler/icons-react'; import { Icon, IconChartLine, IconClipboardTextFilled, IconLeaf, IconRecycle, IconRoute, IconScale, IconSearch, IconTent, IconTrashFilled, IconTrophy, IconTruckFilled } from '@tabler/icons-react';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto'; import BackButton from '../../desa/layanan/_com/BackButto';
@@ -91,7 +91,7 @@ function Page() {
<Box style={{ alignContent: 'center', alignItems: 'center' }}> <Box style={{ alignContent: 'center', alignItems: 'center' }}>
{iconMap[v.icon] ? React.createElement(iconMap[v.icon]) : null} {iconMap[v.icon] ? React.createElement(iconMap[v.icon]) : null}
</Box> </Box>
<Text fw={'bold'} fz={{ base: "lg", md: "xl" }} c={'black'}>{v.name}</Text> <Text fz={{ base: "lg", md: "xl" }} c={'black'}>{v.name}</Text>
</Flex> </Flex>
</Paper> </Paper>
</Box> </Box>
@@ -122,20 +122,28 @@ function Page() {
<Stack gap="md"> <Stack gap="md">
{data2?.map((v, k) => ( {data2?.map((v, k) => (
<Paper key={k} p="md" withBorder radius="md"> <Paper key={k} p="md" withBorder radius="md">
<Text fw="bold" fz="lg">{v.namaTempatMaps}</Text> <Group justify='space-between'>
<Text c="dimmed" fz="sm" mb="sm">{v.alamat}</Text> <Box>
{v.lat && v.lng ? ( <Text fw="bold" fz="lg">{v.namaTempatMaps}</Text>
<a <Text c="dimmed" fz="sm" mb="sm">{v.alamat}</Text>
href={`https://www.google.com/maps/dir/?api=1&destination=${v.lat},${v.lng}`} </Box>
target="_blank" <Box>
rel="noopener noreferrer" <IconRoute color={colors['blue-button']} size={30} />
style={{ color: colors['blue-button'], textDecoration: 'none' }} <Text fw={"bold"} fz="sm" c={colors['blue-button']}>Rute</Text>
> </Box>
<Text fz="sm">📌 Buka di Google Maps</Text> </Group>
</a> {v.lat && v.lng ? (
) : ( <a
<Text c="dimmed" fz="sm">Koordinat belum tersedia</Text> href={`https://www.google.com/maps/dir/?api=1&destination=${v.lat},${v.lng}`}
)} target="_blank"
rel="noopener noreferrer"
style={{ color: colors['blue-button'], textDecoration: 'none' }}
>
<Text fz="sm">📌 Lihat Peta Lebih Besar</Text>
</a>
) : (
<Text c="dimmed" fz="sm">Koordinat belum tersedia</Text>
)}
</Paper> </Paper>
))} ))}
</Stack> </Stack>

View File

@@ -66,8 +66,11 @@ function Page() {
/> />
</Group> </Group>
</Box> </Box>
<Text c="dimmed" fz={{ base: 'sm', md: 'lg' }} mt="sm"> <Text fz="md" mt="sm">
Mari berpartisipasi menanam dan merawat pohon untuk menciptakan lingkungan hijau, sehat, dan seimbang bagi seluruh warga desa. Mari berpartisipasi menanam dan merawat pohon untuk menciptakan lingkungan hijau,
</Text>
<Text fz="md">
sehat, dan seimbang bagi seluruh warga desa.
</Text> </Text>
</Box> </Box>
<Box px={{ base: 'md', md: 100 }} pb={60}> <Box px={{ base: 'md', md: 100 }} pb={60}>

View File

@@ -1,13 +1,13 @@
'use client' 'use client'
import beasiswaDesaState from '@/app/admin/(dashboard)/_state/pendidikan/beasiswa-desa'; import beasiswaDesaState from '@/app/admin/(dashboard)/_state/pendidikan/beasiswa-desa';
import colors from '@/con/colors'; 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 { useDisclosure, useShallowEffect } from '@mantine/hooks';
import { IconArrowRight, IconCoin, IconInfoCircle, IconSchool, IconUsers } from '@tabler/icons-react'; import { IconArrowRight, IconCoin, IconInfoCircle, IconSchool, IconUsers } from '@tabler/icons-react';
import { useTransitionRouter } from 'next-view-transitions';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto'; import BackButton from '../../desa/layanan/_com/BackButto';
import { useTransitionRouter } from 'next-view-transitions';
const dataBeasiswa = [ const dataBeasiswa = [
{ id: 1, nama: 'Penerima Beasiswa', jumlah: '250+', icon: IconUsers }, { id: 1, nama: 'Penerima Beasiswa', jumlah: '250+', icon: IconUsers },
@@ -27,7 +27,7 @@ function Page() {
tempatLahir: "", tempatLahir: "",
tanggalLahir: "", tanggalLahir: "",
jenisKelamin: "", jenisKelamin: "",
kewarganegaraan: "", kewarganegaraan: "WNI",
agama: "", agama: "",
alamatKTP: "", alamatKTP: "",
alamatDomisili: "", alamatDomisili: "",
@@ -50,9 +50,21 @@ function Page() {
close(); 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 [active, setActive] = useState(1);
const nextStep = () => setActive((current) => (current < 5 ? current + 1 : current)); useShallowEffect(() => {
const prevStep = () => setActive((current) => (current > 0 ? current - 1 : current)); 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) { if (loading || !data) {
return ( return (
@@ -74,7 +86,7 @@ function Page() {
<Title fz={55} fw={900} c={colors['blue-button']}> <Title fz={55} fw={900} c={colors['blue-button']}>
Wujudkan Mimpi Pendidikanmu di Desa Darmasaba Wujudkan Mimpi Pendidikanmu di Desa Darmasaba
</Title> </Title>
<Text fz="lg" mt="md" c="dimmed"> <Text fz="lg" mt="md" fw={"bold"}>
Program beasiswa untuk mendukung pendidikan berkualitas bagi generasi muda Desa Darmasaba. Program beasiswa untuk mendukung pendidikan berkualitas bagi generasi muda Desa Darmasaba.
</Text> </Text>
<Group mt="xl"> <Group mt="xl">
@@ -115,7 +127,7 @@ function Page() {
{data.map((v, k) => ( {data.map((v, k) => (
<Paper key={k} p="xl" radius="xl" shadow="sm" bg={colors['white-trans-1']}> <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> <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> </Paper>
))} ))}
</SimpleGrid> </SimpleGrid>
@@ -139,19 +151,22 @@ function Page() {
Timeline Pendaftaran Timeline Pendaftaran
</Title> </Title>
<Center> <Center>
<Stepper mt={20} active={active} onStepClick={setActive} orientation="vertical" allowNextStepsSelect={false}> <Stepper
<StepperStep label="1 Maret 2025" description="Pembukaan Pendaftaran" /> mt={20}
<StepperStep label="15 Maret 2025" description="Seleksi Administrasi" /> active={active}
<StepperStep label="1 April 2025" description="Tes Potensi Akademik" /> onStepClick={setActive}
<StepperStep label="15 April 2025" description="Wawancara" /> orientation="vertical"
<StepperStep label="1 Mei 2025" description="Pengumuman Hasil" /> allowNextStepsSelect={false}
>
{timeline.map((item, index) => (
<Stepper.Step
key={index}
label={item.label}
description={item.desc}
/>
))}
</Stepper> </Stepper>
</Center> </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> </Box>
<Modal <Modal
@@ -194,7 +209,11 @@ function Page() {
<TextInput <TextInput
label="Kewarganegaraan" label="Kewarganegaraan"
placeholder="Masukkan 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 <Select
label="Agama" label="Agama"
placeholder="Pilih agama" placeholder="Pilih agama"

View File

@@ -15,7 +15,7 @@ import {
Timeline, Timeline,
Title Title
} from '@mantine/core'; } from '@mantine/core';
import { IconArrowLeft } from '@tabler/icons-react'; import { IconArrowLeft, IconChecklist, IconInfoCircle, IconQuote, IconSchool, IconTimeline, IconUserPlus } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useDisclosure } from '@mantine/hooks'; import { useDisclosure } from '@mantine/hooks';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
@@ -34,7 +34,7 @@ export default function BeasiswaPage() {
tempatLahir: "", tempatLahir: "",
tanggalLahir: "", tanggalLahir: "",
jenisKelamin: "", jenisKelamin: "",
kewarganegaraan: "", kewarganegaraan: "WNI",
agama: "", agama: "",
alamatKTP: "", alamatKTP: "",
alamatDomisili: "", alamatDomisili: "",
@@ -69,10 +69,11 @@ export default function BeasiswaPage() {
{/* Hero Section */} {/* Hero Section */}
<Container size="lg" py="xl"> <Container size="lg" py="xl">
<Stack gap="md" maw={600}> <Stack gap="md" maw={600}>
<Title order={2} c="blue.9"> <Group>
Program Beasiswa Pendidikan Desa Darmasaba <IconSchool size={30} color={colors["blue-button"]} />
</Title> <Title order={2} c={colors["blue-button"]}>Program Beasiswa Pendidikan Desa Darmasaba</Title>
<Text c="dimmed"> </Group>
<Text>
Program ini bertujuan untuk mendukung pendidikan generasi muda di Desa Darmasaba Program ini bertujuan untuk mendukung pendidikan generasi muda di Desa Darmasaba
agar dapat melanjutkan studi ke jenjang lebih tinggi dengan dukungan finansial dan pendampingan. agar dapat melanjutkan studi ke jenjang lebih tinggi dengan dukungan finansial dan pendampingan.
</Text> </Text>
@@ -81,21 +82,34 @@ export default function BeasiswaPage() {
{/* Tentang Program */} {/* Tentang Program */}
<Container size="lg" py="xl"> <Container size="lg" py="xl">
<Title order={3} mb="sm"> <Group mb="sm">
Tentang Program <IconInfoCircle size={24} color={colors["blue-button"]} />
</Title> <Title order={3} c={colors["blue-button"]}>Tentang Program</Title>
</Group>
<Text> <Text>
Program Beasiswa Desa Darmasaba adalah inisiatif pemerintah desa untuk meningkatkan akses Program Beasiswa Desa Darmasaba adalah inisiatif pemerintah desa untuk meningkatkan akses
pendidikan bagi siswa berprestasi dan kurang mampu. Melalui program ini, desa memberikan bantuan pendidikan bagi siswa berprestasi dan kurang mampu. Melalui program ini, desa memberikan bantuan
biaya sekolah, bimbingan akademik, serta pelatihan soft skill bagi peserta terpilih. biaya sekolah, bimbingan akademik, serta pelatihan soft skill bagi peserta terpilih.
</Text> </Text>
{/* Tambahkan info tahun berjalan di sini */}
<Paper mt="md" p="md" radius="lg" shadow="xs" bg="#f8fbff" withBorder>
<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>.
Pengumuman hasil seleksi akan diumumkan pada pertengahan Juni 2025 melalui website resmi Desa Darmasaba.
</Text>
</Paper>
</Container> </Container>
{/* Syarat dan Ketentuan */} {/* Syarat dan Ketentuan */}
<Container size="lg" py="xl"> <Container size="lg" py="xl">
<Title order={3} mb="sm"> <Group mb="sm">
Syarat Pendaftaran <IconChecklist size={24} color={colors["blue-button"]} />
</Title> <Title order={3} c={colors["blue-button"]}>Syarat Pendaftaran</Title>
</Group>
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="lg"> <SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="lg">
<Paper shadow="sm" p="md" radius="lg" withBorder> <Paper shadow="sm" p="md" radius="lg" withBorder>
@@ -123,42 +137,61 @@ export default function BeasiswaPage() {
{/* Proses Seleksi */} {/* Proses Seleksi */}
<Container size="lg" py="xl"> <Container size="lg" py="xl">
<Title order={3} mb="sm"> <Group mb="sm">
Proses Seleksi <IconTimeline size={24} color={colors["blue-button"]} />
</Title> <Title order={3} c={colors["blue-button"]}>Proses Seleksi</Title>
</Group>
<Timeline active={4} bulletSize={24} lineWidth={2}> <Timeline active={4} bulletSize={24} lineWidth={2}>
<Timeline.Item title="Pendaftaran Online"> <Timeline.Item title="Pendaftaran Online">
<Text c="dimmed" size="sm"> <Text c="dimmed" size="sm">
Calon peserta mengisi formulir pendaftaran dan mengunggah dokumen pendukung. Calon peserta mengisi formulir pendaftaran dan mengunggah dokumen pendukung.
</Text> </Text>
<Text size="sm" fw={500} mt={4}>
Estimasi waktu: 1 Februari 31 Mei 2025
</Text>
</Timeline.Item> </Timeline.Item>
<Timeline.Item title="Seleksi Administrasi"> <Timeline.Item title="Seleksi Administrasi">
<Text c="dimmed" size="sm"> <Text c="dimmed" size="sm">
Panitia memverifikasi kelengkapan dan validitas berkas. Panitia memverifikasi kelengkapan dan validitas berkas.
</Text> </Text>
<Text size="sm" fw={500} mt={4}>
Estimasi waktu: 57 hari kerja setelah penutupan pendaftaran
</Text>
</Timeline.Item> </Timeline.Item>
<Timeline.Item title="Wawancara dan Penilaian"> <Timeline.Item title="Wawancara dan Penilaian">
<Text c="dimmed" size="sm"> <Text c="dimmed" size="sm">
Peserta yang lolos administrasi akan diundang untuk wawancara langsung dengan tim seleksi. Peserta yang lolos administrasi akan diundang untuk wawancara langsung dengan tim seleksi.
</Text> </Text>
<Text size="sm" fw={500} mt={4}>
Estimasi waktu: 710 hari kerja setelah pengumuman seleksi administrasi
</Text>
</Timeline.Item> </Timeline.Item>
<Timeline.Item title="Pengumuman Penerima"> <Timeline.Item title="Pengumuman Penerima">
<Text c="dimmed" size="sm"> <Text c="dimmed" size="sm">
Daftar penerima beasiswa diumumkan melalui website resmi Desa Darmasaba. Daftar penerima beasiswa diumumkan melalui website resmi Desa Darmasaba.
</Text> </Text>
<Text size="sm" fw={500} mt={4}>
Estimasi waktu: 5 hari kerja setelah tahap wawancara selesai
</Text>
</Timeline.Item> </Timeline.Item>
</Timeline> </Timeline>
<Text c="dimmed" size="sm" mt="lg" ta="center">
Total estimasi keseluruhan proses: sekitar 34 minggu setelah penutupan pendaftaran
</Text>
</Container> </Container>
{/* Testimoni */} {/* Testimoni */}
<Container size="lg" py="xl"> <Container size="lg" py="xl">
<Title order={3} mb="sm"> <Group mb="sm">
Cerita Sukses Penerima Beasiswa <IconQuote size={24} color={colors["blue-button"]} />
</Title> <Title order={3} c={colors["blue-button"]}>Cerita Sukses Penerima Beasiswa</Title>
</Group>
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="lg"> <SimpleGrid cols={{ base: 1, sm: 2 }} spacing="lg">
<Paper shadow="md" p="lg" radius="lg"> <Paper shadow="md" p="lg" radius="lg">
@@ -183,11 +216,14 @@ export default function BeasiswaPage() {
{/* CTA Akhir */} {/* CTA Akhir */}
<Container size="lg" py="xl" ta="center"> <Container size="lg" py="xl" ta="center">
<Title order={3}>Siap Bergabung dengan Program Ini?</Title> <Group justify="center" mb="sm">
<IconUserPlus size={28} color={colors["blue-button"]} />
<Title order={3} c={colors["blue-button"]}>Siap Bergabung dengan Program Ini?</Title>
</Group>
<Text c="dimmed" mb="md"> <Text c="dimmed" mb="md">
Segera daftar dan wujudkan mimpimu bersama Desa Darmasaba. Segera daftar dan wujudkan mimpimu bersama Desa Darmasaba.
</Text> </Text>
<Button onClick={open} size="lg" radius="xl" color="blue"> <Button onClick={open} size="lg" radius="xl" bg={colors["blue-button"]}>
Daftar Sekarang Daftar Sekarang
</Button> </Button>
</Container> </Container>
@@ -232,7 +268,11 @@ export default function BeasiswaPage() {
<TextInput <TextInput
label="Kewarganegaraan" label="Kewarganegaraan"
placeholder="Masukkan 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 <Select
label="Agama" label="Agama"
placeholder="Pilih agama" placeholder="Pilih agama"

View File

@@ -1,10 +1,10 @@
'use client' 'use client'
import stateBimbinganBelajarDesa from '@/app/admin/(dashboard)/_state/pendidikan/bimbingan-belajar-desa'; import stateBimbinganBelajarDesa from '@/app/admin/(dashboard)/_state/pendidikan/bimbingan-belajar-desa';
import colors from '@/con/colors'; 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 { useShallowEffect } from '@mantine/hooks';
import { IconBook2, IconCalendarTime, IconMapPin } from '@tabler/icons-react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { IconMapPin, IconCalendarTime, IconBook2 } from '@tabler/icons-react';
import BackButton from '../../desa/layanan/_com/BackButto'; import BackButton from '../../desa/layanan/_com/BackButto';
function Page() { function Page() {
@@ -55,9 +55,9 @@ function Page() {
<IconBook2 size={36} stroke={1.5} color={colors['blue-button']} /> <IconBook2 size={36} stroke={1.5} color={colors['blue-button']} />
</Box> </Box>
</Tooltip> </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} {stateTujuanProgram.findById.data?.judul}
</Badge> </Title>
</Group> </Group>
<Text fz="md" style={{wordBreak: "break-word", whiteSpace: "normal"}} lh={1.6} dangerouslySetInnerHTML={{ __html: stateTujuanProgram.findById.data?.deskripsi }} /> <Text fz="md" style={{wordBreak: "break-word", whiteSpace: "normal"}} lh={1.6} dangerouslySetInnerHTML={{ __html: stateTujuanProgram.findById.data?.deskripsi }} />
</Stack> </Stack>
@@ -70,9 +70,9 @@ function Page() {
<IconMapPin size={36} stroke={1.5} color={colors['blue-button']} /> <IconMapPin size={36} stroke={1.5} color={colors['blue-button']} />
</Box> </Box>
</Tooltip> </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} {stateLokasiDanJadwal.findById.data?.judul}
</Badge> </Title>
</Group> </Group>
<Text fz="md" lh={1.6} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: stateLokasiDanJadwal.findById.data?.deskripsi }} /> <Text fz="md" lh={1.6} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: stateLokasiDanJadwal.findById.data?.deskripsi }} />
</Stack> </Stack>
@@ -85,9 +85,9 @@ function Page() {
<IconCalendarTime size={36} stroke={1.5} color={colors['blue-button']} /> <IconCalendarTime size={36} stroke={1.5} color={colors['blue-button']} />
</Box> </Box>
</Tooltip> </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} {stateFasilitas.findById.data?.judul}
</Badge> </Title>
</Group> </Group>
<Text fz="md" lh={1.6} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: stateFasilitas.findById.data?.deskripsi }} /> <Text fz="md" lh={1.6} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: stateFasilitas.findById.data?.deskripsi }} />
</Stack> </Stack>

View File

@@ -57,7 +57,7 @@ function Page() {
<Title order={1} fw={700} ta="center" c={colors['blue-button']}> <Title order={1} fw={700} ta="center" c={colors['blue-button']}>
Statistik Data Pendidikan Statistik Data Pendidikan
</Title> </Title>
<Text c="dimmed" size="sm" ta="center"> <Text fz="md" ta="center">
Visualisasi jumlah pendidikan berdasarkan kategori yang tersedia Visualisasi jumlah pendidikan berdasarkan kategori yang tersedia
</Text> </Text>
</Stack> </Stack>
@@ -92,7 +92,7 @@ function Page() {
cursor={{ fill: 'var(--mantine-color-gray-1)' }} cursor={{ fill: 'var(--mantine-color-gray-1)' }}
/> />
<Legend /> <Legend />
<Bar dataKey="jumlah" fill={colors['blue-button']} name="Jumlah Pendidikan" radius={[8, 8, 0, 0]} /> <Bar dataKey="jumlah" fill={colors['blue-button']} name="Jumlah Penduduk dengan Pendidikan" radius={[8, 8, 0, 0]} />
</BarChart> </BarChart>
</ResponsiveContainer> </ResponsiveContainer>
</Paper> </Paper>

View File

@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
'use client'; 'use client';
import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud'; 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 { 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 { IconChalkboard, IconMicroscope, IconProps, IconRefresh, IconSchool, IconInfoCircle } from '@tabler/icons-react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
@@ -23,19 +24,19 @@ export default function KategoriPage({ jenjangPendidikan }: { jenjangPendidikan:
const router = useTransitionRouter(); const router = useTransitionRouter();
const [stats, setStats] = useState<Stat[]>([]); const [stats, setStats] = useState<Stat[]>([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
// Decode the URL parameter // Decode the URL parameter
const decodedJenjangPendidikan = decodeURIComponent(jenjangPendidikan); const decodedJenjangPendidikan = decodeURIComponent(jenjangPendidikan);
const jenjangFilter = decodedJenjangPendidikan.toLowerCase() === 'semua' const jenjangFilter = decodedJenjangPendidikan.toLowerCase() === 'semua'
? undefined ? undefined
: decodedJenjangPendidikan; : decodedJenjangPendidikan;
const loadData = useCallback(async () => { const loadData = useCallback(async () => {
if (!decodedJenjangPendidikan) return; if (!decodedJenjangPendidikan) return;
try { try {
setIsLoading(true); setIsLoading(true);
// Load all data in parallel with the jenjang filter // Load all data in parallel with the jenjang filter
await Promise.all([ await Promise.all([
infoSekolahPaud.lembagaPendidikan.findMany.load(1, 100, '', jenjangFilter), infoSekolahPaud.lembagaPendidikan.findMany.load(1, 100, '', jenjangFilter),
@@ -50,7 +51,7 @@ export default function KategoriPage({ jenjangPendidikan }: { jenjangPendidikan:
setStats([ setStats([
{ {
icon: IconChalkboard, icon: IconChalkboard,
jumlah: totalLembaga, jumlah: totalLembaga,
nama: 'Lembaga Pendidikan', nama: 'Lembaga Pendidikan',
@@ -119,11 +120,15 @@ export default function KategoriPage({ jenjangPendidikan }: { jenjangPendidikan:
</Text> </Text>
</Box> </Box>
<Button <Button
leftSection={<IconRefresh size={16} />} leftSection={<IconRefresh color={colors['blue-button']} size={16} />}
variant="outline" variant="outline"
size="xs" size="xs"
onClick={handleRefresh} onClick={handleRefresh}
loading={stats.some(stat => stat.loading)} loading={stats.some(stat => stat.loading)}
c={colors['blue-button']}
style={{
borderColor: colors['blue-button'],
}}
> >
Segarkan Data Segarkan Data
</Button> </Button>
@@ -143,7 +148,7 @@ export default function KategoriPage({ jenjangPendidikan }: { jenjangPendidikan:
aria-label="Tidak ada hasil" aria-label="Tidak ada hasil"
> >
<Center style={{ minHeight: 180, flexDirection: 'column' }}> <Center style={{ minHeight: 180, flexDirection: 'column' }}>
<Text fz="lg" fw={800} c="#2563eb"> <Text fz="lg" fw={800} c={colors['blue-button']}>
Tidak ditemukan Tidak ditemukan
</Text> </Text>
<Text c="dimmed" mt="6px"> <Text c="dimmed" mt="6px">
@@ -173,81 +178,81 @@ export default function KategoriPage({ jenjangPendidikan }: { jenjangPendidikan:
style={{ width: '100%' }} style={{ width: '100%' }}
> >
<Skeleton visible={v.loading}> <Skeleton visible={v.loading}>
<Paper <Paper
p="lg" p="lg"
radius="lg" radius="lg"
style={{ style={{
background: 'white', background: 'white',
border: '1px solid #e2e8f0', border: '1px solid #e2e8f0',
boxShadow: '0 8px 28px rgba(0,0,0,0.06)', boxShadow: '0 8px 28px rgba(0,0,0,0.06)',
minHeight: 260, minHeight: 260,
}} }}
role="article" role="article"
aria-label={`${v.nama} kartu statistik`} aria-label={`${v.nama} kartu statistik`}
> >
<Stack gap="sm" mb="md"> <Stack gap="sm" mb="md">
<Center> <Center>
<Box <Box
style={{ style={{
width: 80, width: 80,
height: 80, height: 80,
borderRadius: 16, borderRadius: 16,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
background: '#eff6ff', background: '#eff6ff',
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.6)', boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.6)',
}} }}
aria-hidden aria-hidden
> >
{React.createElement(v.icon, { {React.createElement(v.icon, {
color: '#2563eb', color: colors['blue-button'],
size: 34, size: 34,
stroke: 1.6, stroke: 1.6,
})} })}
</Box> </Box>
</Center> </Center>
<Group justify="center" align="center" gap="xs"> <Group justify="center" align="center" gap="xs">
<Stack gap={0}> <Stack gap={0}>
<Text ta={"center"} fz={{ base: 18, md: 22 }} fw={800} c="#0f172a"> <Text ta={"center"} fz={{ base: 18, md: 22 }} fw={800} c="#0f172a">
{v.jumlah.toLocaleString()} {v.jumlah.toLocaleString()}
</Text>
<Group gap={6} align="center">
<Text ta={"center"} fz="sm" fw={700} c="#2563eb">
{v.nama}
</Text> </Text>
<Tooltip label={v.helper ?? ''} position="right" withArrow> <Group gap={6} align="center">
<ActionIcon aria-label={`Info ${v.nama}`} variant="transparent" size="xs"> <Text ta={"center"} fz="sm" fw={700} c={colors['blue-button']}>
<IconInfoCircle size={16} style={{ color: '#2563eb' }} /> {v.nama}
</ActionIcon> </Text>
</Tooltip> <Tooltip label={v.helper ?? ''} position="right" withArrow>
</Group> <ActionIcon aria-label={`Info ${v.nama}`} variant="transparent" size="xs">
</Stack> <IconInfoCircle size={16} style={{ color: colors['blue-button'] }} />
</Group> </ActionIcon>
</Stack> </Tooltip>
</Group>
</Stack>
</Group>
</Stack>
<Group justify="center" mt="8px"> <Group justify="center" mt="8px">
<Button <Button
radius="xl" radius="xl"
variant="outline" variant="outline"
aria-label={`Lihat detail ${v.nama}`} aria-label={`Lihat detail ${v.nama}`}
style={{ style={{
borderColor: '#e2e8f0', borderColor: colors['blue-button'],
color: '#2563eb', color: colors['blue-button'],
paddingLeft: 20, paddingLeft: 20,
paddingRight: 20, paddingRight: 20,
}} }}
onClick={() => { onClick={() => {
if (v.nama === "Lembaga Pendidikan") router.push(`/darmasaba/pendidikan/info-sekolah/${jenjangPendidikan}/lembaga`); 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 === "Siswa Terdaftar") router.push(`/darmasaba/pendidikan/info-sekolah/${jenjangPendidikan}/siswa`);
if (v.nama === "Tenaga Pengajar") router.push(`/darmasaba/pendidikan/info-sekolah/${jenjangPendidikan}/pengajar`); if (v.nama === "Tenaga Pengajar") router.push(`/darmasaba/pendidikan/info-sekolah/${jenjangPendidikan}/pengajar`);
}} }}
> >
Lihat Detail Lihat Detail
</Button> </Button>
</Group> </Group>
</Paper> </Paper>
</Skeleton> </Skeleton>
</motion.div> </motion.div>
)) ))

View File

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

View File

@@ -55,7 +55,7 @@ function Page({ params }: PageProps) {
<Group justify="space-between" align="center" mb="md"> <Group justify="space-between" align="center" mb="md">
<Group gap="sm"> <Group gap="sm">
<IconMicroscope size={28} stroke={1.5} color={colors['blue-button']} /> <IconMicroscope size={28} stroke={1.5} color={colors['blue-button']} />
<Title order={2} fz="xl">Daftar Pengajar</Title> <Title order={2} fz="xl" c={colors['blue-button']}>Daftar Pengajar</Title>
</Group> </Group>
<TextInput <TextInput
placeholder='pencarian' placeholder='pencarian'
@@ -84,7 +84,7 @@ function Page({ params }: PageProps) {
<TableTr> <TableTr>
<TableTh w="30%">Nama Pengajar</TableTh> <TableTh w="30%">Nama Pengajar</TableTh>
<TableTh w="30%">Nama Lembaga</TableTh> <TableTh w="30%">Nama Lembaga</TableTh>
<TableTh w="40%">Jenjang Pendidikan</TableTh> <TableTh w="40%">Mengajar Di Jenjang Pendidikan</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
@@ -95,7 +95,7 @@ function Page({ params }: PageProps) {
<TableTd>{item.lembaga.jenjangPendidikan?.nama || '-'}</TableTd> <TableTd>{item.lembaga.jenjangPendidikan?.nama || '-'}</TableTd>
</TableTr> </TableTr>
))} ))}
</TableTbody> </TableTbody>
</Table> </Table>
)} )}
</Paper> </Paper>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
'use client' 'use client'
import pendidikanNonFormalState from '@/app/admin/(dashboard)/_state/pendidikan/pendidikan-non-formal'; import pendidikanNonFormalState from '@/app/admin/(dashboard)/_state/pendidikan/pendidikan-non-formal';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Paper, SimpleGrid, Skeleton, Stack, Text, Title, Tooltip } from '@mantine/core'; import { Box, Group, Paper, SimpleGrid, Skeleton, Stack, Text, Title, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { IconMapPin, IconTarget, IconBook2 } from '@tabler/icons-react'; import { IconMapPin, IconTarget, IconBook2 } from '@tabler/icons-react';
@@ -43,7 +43,7 @@ function Page() {
Pendidikan Non Formal Pendidikan Non Formal
</Title> </Title>
<Text ta="center" fz="lg" lh={1.6} c="black" maw={800} mx="auto"> <Text ta="center" fz="lg" lh={1.6} c="black" maw={800} mx="auto">
Bentuk pendidikan di luar sekolah yang terstruktur, bertujuan memberikan keterampilan, pengetahuan, dan pengembangan karakter masyarakat dari berbagai usia serta latar belakang. Pendidikan non formal merupakan bentuk pendidikan di luar sekolah yang terstruktur, bertujuan untuk memberikan keterampilan, pengetahuan, serta pengembangan karakter masyarakat dari berbagai usia dan latar belakang.
</Text> </Text>
</Box> </Box>
<SimpleGrid <SimpleGrid
@@ -59,13 +59,17 @@ function Page() {
withBorder withBorder
> >
<Stack> <Stack>
<Tooltip label="Fokus utama program" withArrow> <Group align="center" gap={8} wrap="nowrap">
<Title order={2} fw="bold" c={colors['blue-button']} mb="xs" flex="center"> <Tooltip label="Fokus utama program" withArrow>
<IconTarget size={28} style={{ marginRight: 8 }} /> <Box display="flex" style={{ alignItems: "center" }}>
<IconTarget color={colors['blue-button']} size={26} />
</Box>
</Tooltip>
<Text fw={700} fz="xl" c={colors['blue-button']}>
{stateTujuanPendidikanNonFormal.findById.data?.judul} {stateTujuanPendidikanNonFormal.findById.data?.judul}
</Title> </Text>
</Tooltip> </Group>
<Text fz="md" lh={1.7} c="dark" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: stateTujuanPendidikanNonFormal.findById.data?.deskripsi }} /> <Text fz="md" lh={1.7} c="dark" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: stateTujuanPendidikanNonFormal.findById.data?.deskripsi }} />
</Stack> </Stack>
</Paper> </Paper>
<Paper <Paper
@@ -76,13 +80,17 @@ function Page() {
withBorder withBorder
> >
<Stack> <Stack>
<Tooltip label="Lokasi pelaksanaan kegiatan" withArrow> <Group align="center" gap={8} wrap="nowrap">
<Title order={2} fw="bold" c={colors['blue-button']} mb="xs" flex="center"> <Tooltip label="Lokasi pelaksanaan kegiatan" withArrow>
<IconMapPin size={28} style={{ marginRight: 8 }} /> <Box display="flex" style={{ alignItems: "center" }}>
<IconMapPin color={colors['blue-button']} size={26} />
</Box>
</Tooltip>
<Text fw={700} fz="xl" c={colors['blue-button']}>
{stateTempatKegiatan.findById.data?.judul} {stateTempatKegiatan.findById.data?.judul}
</Title> </Text>
</Tooltip> </Group>
<Text fz="md" style={{wordBreak: "break-word", whiteSpace: "normal"}} lh={1.7} c="dark" dangerouslySetInnerHTML={{ __html: stateTempatKegiatan.findById.data?.deskripsi }} /> <Text fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal" }} lh={1.7} c="dark" dangerouslySetInnerHTML={{ __html: stateTempatKegiatan.findById.data?.deskripsi }} />
</Stack> </Stack>
</Paper> </Paper>
</SimpleGrid> </SimpleGrid>
@@ -95,13 +103,17 @@ function Page() {
withBorder withBorder
> >
<Stack> <Stack>
<Tooltip label="Ragam jenis program yang tersedia" withArrow> <Group align="center" gap={8} wrap="nowrap">
<Title order={2} fw="bold" c={colors['blue-button']} mb="xs" flex="center"> <Tooltip label="Ragam jenis program yang tersedia" withArrow>
<IconBook2 size={28} style={{ marginRight: 8 }} /> <Box display="flex" style={{ alignItems: "center" }}>
<IconBook2 color={colors['blue-button']} size={26} />
</Box>
</Tooltip>
<Text fw={700} fz="xl" c={colors['blue-button']}>
{stateJenisProgram.findById.data?.judul} {stateJenisProgram.findById.data?.judul}
</Title> </Text>
</Tooltip> </Group>
<Text fz="md" lh={1.7} c="dark" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: stateJenisProgram.findById.data?.deskripsi }} /> <Text fz="md" lh={1.7} c="dark" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: stateJenisProgram.findById.data?.deskripsi }} />
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

@@ -16,16 +16,11 @@ import {
TextInput, TextInput,
} from '@mantine/core'; } from '@mantine/core';
import { DateInput } from '@mantine/dates'; import { DateInput } from '@mantine/dates';
import { import { IconArrowRight, IconBook2, IconUser } from '@tabler/icons-react';
IconArrowRight,
IconBook2,
IconUser
} from '@tabler/icons-react';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useSnapshot } from 'valtio'; import { useSnapshot } from 'valtio';
export interface ModalPeminjamanProps { export interface ModalPeminjamanProps {
opened: boolean; opened: boolean;
onClose: () => void; onClose: () => void;
@@ -45,11 +40,12 @@ export default function ModalPeminjaman({
}: ModalPeminjamanProps) { }: ModalPeminjamanProps) {
const snap = useSnapshot(perpustakaanDigitalState.peminjamanBuku); const snap = useSnapshot(perpustakaanDigitalState.peminjamanBuku);
// reset form setiap modal dibuka const BATAS_HARI_PINJAM = 4;
// Reset form setiap modal dibuka
useEffect(() => { useEffect(() => {
if (opened && buku) { if (opened && buku) {
perpustakaanDigitalState.peminjamanBuku.create.form = { perpustakaanDigitalState.peminjamanBuku.create.form = {
...perpustakaanDigitalState.peminjamanBuku.create.form,
bukuId: buku.id, bukuId: buku.id,
nama: '', nama: '',
noTelp: '', noTelp: '',
@@ -99,7 +95,14 @@ export default function ModalPeminjaman({
</Badge> </Badge>
)} )}
<Text fz="sm" c="dimmed" lineClamp={3} dangerouslySetInnerHTML={{ __html: buku.deskripsi || 'Tidak ada deskripsi' }} /> <Text
fz="sm"
c="dimmed"
lineClamp={3}
dangerouslySetInnerHTML={{
__html: buku.deskripsi || 'Tidak ada deskripsi',
}}
/>
</Stack> </Stack>
</Group> </Group>
@@ -112,7 +115,8 @@ export default function ModalPeminjaman({
leftSection={<IconUser size={16} />} leftSection={<IconUser size={16} />}
value={snap.create.form.nama} value={snap.create.form.nama}
onChange={(e) => onChange={(e) =>
(perpustakaanDigitalState.peminjamanBuku.create.form.nama = e.currentTarget.value) (perpustakaanDigitalState.peminjamanBuku.create.form.nama =
e.currentTarget.value)
} }
required required
/> />
@@ -123,7 +127,8 @@ export default function ModalPeminjaman({
leftSection={<IconUser size={16} />} leftSection={<IconUser size={16} />}
value={snap.create.form.noTelp} value={snap.create.form.noTelp}
onChange={(e) => onChange={(e) =>
(perpustakaanDigitalState.peminjamanBuku.create.form.noTelp = e.currentTarget.value) (perpustakaanDigitalState.peminjamanBuku.create.form.noTelp =
e.currentTarget.value)
} }
required required
/> />
@@ -134,11 +139,13 @@ export default function ModalPeminjaman({
leftSection={<IconUser size={16} />} leftSection={<IconUser size={16} />}
value={snap.create.form.alamat} value={snap.create.form.alamat}
onChange={(e) => onChange={(e) =>
(perpustakaanDigitalState.peminjamanBuku.create.form.alamat = e.currentTarget.value) (perpustakaanDigitalState.peminjamanBuku.create.form.alamat =
e.currentTarget.value)
} }
required required
/> />
{/* === OTOMATIS SET BATAS DAN TANGGAL KEMBALI === */}
<DateInput <DateInput
label="Tanggal Pinjam" label="Tanggal Pinjam"
placeholder="Pilih tanggal pinjam" placeholder="Pilih tanggal pinjam"
@@ -148,64 +155,83 @@ export default function ModalPeminjaman({
: null : null
} }
onChange={(date) => { onChange={(date) => {
perpustakaanDigitalState.peminjamanBuku.create.form.tanggalPinjam = if (date) {
date ? new Date(date).toISOString() : ''; const tanggalPinjam = new Date(date);
// simpan tanggal pinjam
perpustakaanDigitalState.peminjamanBuku.create.form.tanggalPinjam =
tanggalPinjam.toISOString();
// hitung batas +4 hari
const batasKembali = new Date(tanggalPinjam);
batasKembali.setDate(batasKembali.getDate() + BATAS_HARI_PINJAM);
// set batas & tanggal kembali otomatis
perpustakaanDigitalState.peminjamanBuku.create.form.batasKembali =
batasKembali.toISOString();
perpustakaanDigitalState.peminjamanBuku.create.form.tanggalKembali =
batasKembali.toISOString();
toast.info(
`Batas pengembalian otomatis diset ke ${batasKembali.toLocaleDateString('id-ID')} (+${BATAS_HARI_PINJAM} hari).`
);
} else {
perpustakaanDigitalState.peminjamanBuku.create.form.tanggalPinjam = '';
perpustakaanDigitalState.peminjamanBuku.create.form.batasKembali = '';
perpustakaanDigitalState.peminjamanBuku.create.form.tanggalKembali = '';
}
}} }}
required required
/> />
<Box> <Box>
<Text>Catatan</Text> <Text fw={500}>Catatan</Text>
<CreateEditor <CreateEditor
value={snap.create.form.catatan} value={snap.create.form.catatan}
onChange={(e) => onChange={(val) =>
(perpustakaanDigitalState.peminjamanBuku.create.form.catatan = e) (perpustakaanDigitalState.peminjamanBuku.create.form.catatan =
val)
} }
/> />
</Box> </Box>
<DateInput
label="Tanggal Kembali"
placeholder="Pilih tanggal kembali"
value={
snap.create.form.tanggalKembali
? new Date(snap.create.form.tanggalKembali)
: null
}
onChange={(date) => {
perpustakaanDigitalState.peminjamanBuku.create.form.tanggalKembali =
date ? new Date(date).toISOString() : '';
}}
required
/>
<DateInput <DateInput
label="Batas Pengembalian" label="Batas Pengembalian"
placeholder="Pilih tanggal kembali" placeholder="Otomatis diatur +4 hari dari tanggal pinjam"
value={ value={
snap.create.form.batasKembali snap.create.form.batasKembali
? new Date(snap.create.form.batasKembali) ? new Date(snap.create.form.batasKembali)
: null : null
} }
onChange={(date) => { disabled
perpustakaanDigitalState.peminjamanBuku.create.form.batasKembali = readOnly
date ? new Date(date).toISOString() : '';
}}
required
/> />
<DateInput
label="Tanggal Kembali"
placeholder="Otomatis sama dengan batas pengembalian"
value={
snap.create.form.tanggalKembali
? new Date(snap.create.form.tanggalKembali)
: null
}
disabled
readOnly
/>
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
loading={snap.create.loading} loading={snap.create.loading}
disabled={ disabled={
!snap.create.form.nama || !snap.create.form.nama || !snap.create.form.tanggalPinjam
!snap.create.form.tanggalPinjam ||
!snap.create.form.batasKembali ||
!snap.create.form.tanggalKembali
} }
rightSection={<IconArrowRight size={16} />} rightSection={<IconArrowRight size={16} />}
radius="xl" radius="xl"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
> >
Pinjam Buku Pinjam Buku
</Button> </Button>

View File

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

Some files were not shown because too many files have changed in this diff Show More