Compare commits
5 Commits
main
...
nico/4-nov
| Author | SHA1 | Date | |
|---|---|---|---|
| fb57698dc9 | |||
| d128313e71 | |||
| 7b4bb1e58e | |||
| 0befe6a3f2 | |||
| a6663bbcee |
@@ -19,6 +19,7 @@
|
||||
"@elysiajs/static": "^1.3.0",
|
||||
"@elysiajs/stream": "^1.1.0",
|
||||
"@elysiajs/swagger": "^1.2.0",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@mantine/carousel": "^7.16.2",
|
||||
"@mantine/charts": "^7.17.1",
|
||||
"@mantine/core": "^7.17.4",
|
||||
@@ -26,6 +27,7 @@
|
||||
"@mantine/dropzone": "^8.1.1",
|
||||
"@mantine/form": "^8.1.0",
|
||||
"@mantine/hooks": "^7.17.4",
|
||||
"@mantine/modals": "^8.3.6",
|
||||
"@mantine/tiptap": "^7.17.4",
|
||||
"@paljs/types": "^8.1.0",
|
||||
"@prisma/client": "^6.3.1",
|
||||
@@ -55,8 +57,9 @@
|
||||
"dayjs": "^1.11.13",
|
||||
"dotenv": "^17.2.3",
|
||||
"elysia": "^1.3.5",
|
||||
"embla-carousel-autoplay": "^8.5.2",
|
||||
"embla-carousel-react": "^7.1.0",
|
||||
"embla-carousel": "^8.6.0",
|
||||
"embla-carousel-autoplay": "^8.6.0",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"extract-zip": "^2.0.1",
|
||||
"form-data": "^4.0.2",
|
||||
"framer-motion": "^12.23.5",
|
||||
@@ -80,6 +83,7 @@
|
||||
"prisma": "^6.3.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-exif-orientation-img": "^0.1.5",
|
||||
"react-international-phone": "^4.6.0",
|
||||
"react-leaflet": "^5.0.0",
|
||||
"react-simple-toasts": "^6.1.0",
|
||||
|
||||
@@ -6,9 +6,9 @@ import { proxy } from "valtio";
|
||||
import { z } from "zod";
|
||||
|
||||
const templateForm = z.object({
|
||||
name: z.string().min(1, "Nama minimal 1 karakter"),
|
||||
deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
|
||||
slug: z.string().min(1, "Deskripsi singkat minimal 1 karakter"),
|
||||
name: z.string().min(5, "Nama minimal 5 karakter"),
|
||||
deskripsi: z.string().min(5, "Deskripsi minimal 5 karakter"),
|
||||
slug: z.string().min(5, "Deskripsi singkat minimal 5 karakter"),
|
||||
icon: z.string().min(1, "Icon minimal 1 karakter"),
|
||||
});
|
||||
|
||||
@@ -29,7 +29,8 @@ const programKreatifState = proxy({
|
||||
const err = `[${cek.error.issues
|
||||
.map((v) => `${v.path.join(".")}`)
|
||||
.join("\n")}] required`;
|
||||
return toast.error(err);
|
||||
toast.error(err);
|
||||
return false; // ⬅️ ini penting
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -37,18 +38,24 @@ const programKreatifState = proxy({
|
||||
const res = await ApiFetch.api.inovasi.programkreatif["create"].post(
|
||||
programKreatifState.create.form
|
||||
);
|
||||
|
||||
if (res.status === 200) {
|
||||
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) {
|
||||
console.log((error as Error).message);
|
||||
console.error((error as Error).message);
|
||||
toast.error("Terjadi kesalahan saat create");
|
||||
return false;
|
||||
} finally {
|
||||
programKreatifState.create.loading = false;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
},
|
||||
findMany: {
|
||||
data: null as any[] | null,
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Image,
|
||||
Paper,
|
||||
Stack,
|
||||
Text,
|
||||
@@ -21,6 +20,7 @@ import { useProxy } from 'valtio/utils';
|
||||
import CreateEditor from '../../../_com/createEditor';
|
||||
import desaDigitalState from '../../../_state/inovasi/desa-digital';
|
||||
import { Dropzone } from '@mantine/dropzone';
|
||||
import ExifOrientationImg from 'react-exif-orientation-img';
|
||||
|
||||
export default function CreateDesaDigital() {
|
||||
const stateDesaDigital = useProxy(desaDigitalState);
|
||||
@@ -173,17 +173,16 @@ export default function CreateDesaDigital() {
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={previewImage}
|
||||
alt="Preview"
|
||||
radius="md"
|
||||
style={{
|
||||
maxHeight: 220,
|
||||
objectFit: 'cover',
|
||||
border: '1px solid #e0e0e0',
|
||||
}}
|
||||
loading="lazy"
|
||||
/>
|
||||
<ExifOrientationImg
|
||||
src={previewImage}
|
||||
alt="Preview"
|
||||
style={{
|
||||
maxHeight: 220,
|
||||
objectFit: 'cover',
|
||||
border: '1px solid #e0e0e0',
|
||||
borderRadius: 12,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -54,8 +54,8 @@ function DetailInfoTeknologiTepatGuna() {
|
||||
{/* Card Utama */}
|
||||
<Paper
|
||||
withBorder
|
||||
w={{ base: "100%", md: "70%", lg: "60%" }}
|
||||
bg="#ECEEF8"
|
||||
w={{ base: "100%", md: "50%" }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
@@ -65,7 +65,7 @@ function DetailInfoTeknologiTepatGuna() {
|
||||
Detail Info Teknologi Tepat Guna
|
||||
</Text>
|
||||
|
||||
<Paper bg={colors['BG-trans']} p="md" radius="md" shadow="xs">
|
||||
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
|
||||
<Stack gap="sm">
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Judul</Text>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
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 colors from '@/con/colors';
|
||||
import {
|
||||
@@ -11,8 +12,7 @@ import {
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
Tooltip,
|
||||
Title
|
||||
} from '@mantine/core';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
@@ -20,7 +20,6 @@ import { useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import SelectIconProgramEdit from '../../../../_com/selectIconEdit';
|
||||
import { IconKey } from '@/app/admin/(dashboard)/_com/iconMap';
|
||||
|
||||
interface FormProgramKreatif {
|
||||
name: string;
|
||||
@@ -41,6 +40,15 @@ function EditProgramKreatifDesa() {
|
||||
icon: '',
|
||||
});
|
||||
|
||||
const [originalData, setOriginalData] = useState<FormProgramKreatif>({
|
||||
name: '',
|
||||
deskripsi: '',
|
||||
slug: '',
|
||||
icon: '',
|
||||
});
|
||||
|
||||
const [isDataChanged, setIsDataChanged] = useState(false);
|
||||
|
||||
// Load data hanya sekali berdasarkan params.id
|
||||
useEffect(() => {
|
||||
const loadProgramKreatif = async () => {
|
||||
@@ -51,12 +59,14 @@ function EditProgramKreatifDesa() {
|
||||
const data = await stateProgramKreatif.update.load(id);
|
||||
if (data) {
|
||||
stateProgramKreatif.update.id = id;
|
||||
setFormData({
|
||||
const loadedData = {
|
||||
name: data.name || '',
|
||||
slug: data.slug || '',
|
||||
deskripsi: data.deskripsi || '',
|
||||
icon: data.icon || '',
|
||||
});
|
||||
};
|
||||
setFormData(loadedData);
|
||||
setOriginalData(loadedData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading program kreatif:', error);
|
||||
@@ -67,12 +77,49 @@ function EditProgramKreatifDesa() {
|
||||
loadProgramKreatif();
|
||||
}, [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 =
|
||||
(field: keyof FormProgramKreatif) =>
|
||||
(value: string) => {
|
||||
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 () => {
|
||||
try {
|
||||
stateProgramKreatif.update.form = {
|
||||
@@ -82,6 +129,11 @@ function EditProgramKreatifDesa() {
|
||||
icon: formData.icon.trim(),
|
||||
};
|
||||
await stateProgramKreatif.update.submit();
|
||||
|
||||
// Reset isDataChanged agar tidak muncul konfirmasi setelah save
|
||||
setOriginalData(formData);
|
||||
setIsDataChanged(false);
|
||||
|
||||
router.push('/admin/inovasi/program-kreatif-desa');
|
||||
} catch (error) {
|
||||
console.error('Error updating program kreatif:', error);
|
||||
@@ -92,16 +144,14 @@ function EditProgramKreatifDesa() {
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Group mb="md">
|
||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
p="xs"
|
||||
radius="md"
|
||||
>
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={handleBackClick}
|
||||
p="xs"
|
||||
radius="md"
|
||||
>
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Edit Program Kreatif Desa
|
||||
</Title>
|
||||
|
||||
@@ -32,11 +32,15 @@ function CreateProgramKreatifDesa() {
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
await stateCreate.create.create();
|
||||
resetForm();
|
||||
router.push("/admin/inovasi/program-kreatif-desa");
|
||||
const success = await stateCreate.create.create();
|
||||
|
||||
if (success) {
|
||||
resetForm();
|
||||
router.push("/admin/inovasi/program-kreatif-desa");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
{/* Tombol kembali */}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
||||
import laporanPublikState from '@/app/admin/(dashboard)/_state/keamanan/laporan-publik';
|
||||
@@ -52,15 +53,14 @@ function EditLaporanPublik() {
|
||||
try {
|
||||
const data = await stateLaporan.edit.load(id);
|
||||
if (data) {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
judul: data.judul ?? prev.judul,
|
||||
lokasi: data.lokasi ?? prev.lokasi,
|
||||
tanggalWaktu: data.tanggalWaktu ?? prev.tanggalWaktu,
|
||||
status: (data.status as Status) ?? prev.status,
|
||||
penanganan: data.penanganan?.[0]?.deskripsi ?? prev.penanganan,
|
||||
kronologi: data.kronologi ?? prev.kronologi,
|
||||
}));
|
||||
setFormData({
|
||||
judul: data.judul ?? '',
|
||||
lokasi: data.lokasi ?? '',
|
||||
tanggalWaktu: data.tanggalWaktu ?? '',
|
||||
status: (data.status as Status) ?? 'Proses',
|
||||
penanganan: data.penanganan?.[0]?.deskripsi ?? '',
|
||||
kronologi: data.kronologi ?? '',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading laporan publik:", error);
|
||||
@@ -69,7 +69,8 @@ function EditLaporanPublik() {
|
||||
};
|
||||
|
||||
loadLaporanPublik();
|
||||
}, [params?.id, stateLaporan.edit]);
|
||||
}, [params?.id]);
|
||||
|
||||
|
||||
|
||||
const handleChange = (field: string, value: string | Status) => {
|
||||
|
||||
@@ -12,8 +12,7 @@ import {
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
Tooltip,
|
||||
Title
|
||||
} from '@mantine/core';
|
||||
import { Dropzone } from '@mantine/dropzone';
|
||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||
@@ -96,11 +95,9 @@ function EditMediaSosial() {
|
||||
py="md"
|
||||
>
|
||||
<Group mb="md">
|
||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Edit Media Sosial
|
||||
</Title>
|
||||
|
||||
@@ -11,8 +11,7 @@ import {
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
Tooltip,
|
||||
Title
|
||||
} from '@mantine/core';
|
||||
import { Dropzone } from '@mantine/dropzone';
|
||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||
@@ -69,11 +68,9 @@ export default function CreateMediaSosial() {
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Group mb="md">
|
||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Tambah Media Sosial
|
||||
</Title>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Center, Group, Image, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
|
||||
import { Box, Button, Center, Group, Image, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
@@ -56,11 +56,9 @@ function ListMediaSosial({ search }: { search: string }) {
|
||||
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={4}>Daftar Media Sosial</Title>
|
||||
<Tooltip label="Tambah Media Sosial" withArrow>
|
||||
<Button leftSection={<IconPlus size={18} />} color="blue" variant="light" onClick={() => router.push('/admin/landing-page/profile/media-sosial/create')}>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
<Box style={{ overflowX: "auto" }}>
|
||||
<Table highlightOnHover>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
Alert, Box, Button, Center, Group, Image,
|
||||
Paper, Stack, Text, TextInput, Title, Tooltip
|
||||
Paper, Stack, Text, TextInput, Title
|
||||
} from '@mantine/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
@@ -177,11 +177,9 @@ function EditPejabatDesa() {
|
||||
<Box>
|
||||
<Stack gap="xs">
|
||||
<Group mb="md">
|
||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Edit Pejabat Desa
|
||||
</Title>
|
||||
|
||||
@@ -13,8 +13,7 @@ import {
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
Tooltip,
|
||||
Title
|
||||
} from '@mantine/core';
|
||||
import { Dropzone } from '@mantine/dropzone';
|
||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||
@@ -104,11 +103,9 @@ function EditProgramInovasi() {
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Group mb="md">
|
||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Edit Program Inovasi
|
||||
</Title>
|
||||
|
||||
@@ -12,8 +12,7 @@ import {
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
Tooltip
|
||||
Title
|
||||
} from '@mantine/core';
|
||||
import { Dropzone } from '@mantine/dropzone';
|
||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||
@@ -70,11 +69,9 @@ function CreateProgramInovasi() {
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Group mb="md">
|
||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Tambah Program Inovasi
|
||||
</Title>
|
||||
|
||||
@@ -51,8 +51,7 @@ function ListProgramInovasi({ search }: { search: string }) {
|
||||
<Paper bg={colors['white-1']} withBorder p="lg" radius="md" shadow="sm">
|
||||
<Group justify='space-between'>
|
||||
<Title order={4}>Daftar Program Inovasi</Title>
|
||||
<Tooltip label="Tambah Program Inovasi" withArrow>
|
||||
<Button
|
||||
<Button
|
||||
color="blue"
|
||||
leftSection={<IconPlus size={18} />}
|
||||
variant="light"
|
||||
@@ -61,7 +60,6 @@ function ListProgramInovasi({ search }: { search: string }) {
|
||||
>
|
||||
Tambah Program
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
<Box style={{ overflowX: 'auto' }}>
|
||||
<Table highlightOnHover striped verticalSpacing="sm">
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Grid, GridCol, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconEdit } from '@tabler/icons-react';
|
||||
@@ -26,10 +25,10 @@ function Page() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box p={{ base: 'md', md: 'xl' }}>
|
||||
<Paper withBorder radius="md" p={{ base: 'md', md: 'lg' }} bg={colors['white-1']}>
|
||||
<Box p="md">
|
||||
<Paper withBorder p={{ base: 'md', md: 'lg' }} radius="md">
|
||||
{/* Header */}
|
||||
<Grid align="center" mb="lg">
|
||||
<Grid align="center" mb={{ base: 'md', md: 'lg' }}>
|
||||
<GridCol span={{ base: 12, md: 11 }}>
|
||||
<Title order={3} fw={600} c="dark">
|
||||
Preview Bentuk Konservasi Berdasarkan Adat
|
||||
@@ -55,8 +54,8 @@ function Page() {
|
||||
|
||||
{/* Konten */}
|
||||
<Stack gap="md">
|
||||
<Paper radius="md" p={{ base: 'md', md: 'xl' }} bg={colors['BG-trans']} shadow="sm">
|
||||
<Box mb="md">
|
||||
<Paper p={{ base: 'md', md: 'xl' }} bg="#ECEEF8" radius="md">
|
||||
<Box mb="md" px={{ base: 0, md: 20 }}>
|
||||
<Text
|
||||
fz={{ base: 'xl', md: '2xl' }}
|
||||
fw={600}
|
||||
@@ -67,7 +66,7 @@ function Page() {
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<Box px={{ base: 0, md: 20 }}>
|
||||
<Text
|
||||
fz={{ base: 'md', md: 'lg' }}
|
||||
ta="justify"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'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 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 }) {
|
||||
const router = useRouter();
|
||||
@@ -16,14 +16,12 @@ function LayoutTabsPengelolaanSampahBankSampah({ children }: { children: React.R
|
||||
value: "listpengelolaansampahbanksampah",
|
||||
href: "/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah",
|
||||
icon: <IconTrash size={18} stroke={1.8} />,
|
||||
tooltip: "Kelola data pengelolaan sampah bank sampah",
|
||||
},
|
||||
{
|
||||
label: "Keterangan Bank Sampah Terdekat",
|
||||
value: "keteranganbanksampahterdekat",
|
||||
href: "/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat",
|
||||
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) => (
|
||||
<Tooltip
|
||||
key={i}
|
||||
label={tab.tooltip}
|
||||
position="bottom"
|
||||
withArrow
|
||||
transitionProps={{ transition: 'pop', duration: 200 }}
|
||||
>
|
||||
<TabsTab
|
||||
key={i}
|
||||
value={tab.value}
|
||||
leftSection={tab.icon}
|
||||
style={{
|
||||
@@ -92,7 +84,6 @@ function LayoutTabsPengelolaanSampahBankSampah({ children }: { children: React.R
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTab>
|
||||
</Tooltip>
|
||||
))}
|
||||
</TabsList>
|
||||
</ScrollArea>
|
||||
|
||||
@@ -65,26 +65,32 @@ function ListDataPerpustakaan({ search }: { search: string }) {
|
||||
<Table striped highlightOnHover withRowBorders style={{ minWidth: '700px' }}>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>No</TableTh>
|
||||
<TableTh>Judul</TableTh>
|
||||
<TableTh>Kategori</TableTh>
|
||||
<TableTh>Detail</TableTh>
|
||||
<TableTh style={{ width: '5%' }}>No</TableTh>
|
||||
<TableTh style={{ width: '25%' }}>Judul</TableTh>
|
||||
<TableTh style={{ width: '25%' }}>Kategori</TableTh>
|
||||
<TableTh style={{ width: '23%' }}>Deskripsi</TableTh>
|
||||
<TableTh style={{ width: '22%' }}>Detail</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item, index) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>
|
||||
<TableTd style={{ width: '5%' }}>
|
||||
<Text truncate fz="sm">{index + 1}</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<TableTd style={{ width: '20%' }}>
|
||||
<Text truncate fz="sm">{item.judul}</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<TableTd style={{ width: '20%' }}>
|
||||
<Text truncate fz="sm">{item.kategori.name}</Text>
|
||||
</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
|
||||
variant="light"
|
||||
color="blue"
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
|
||||
import NewsReader from '@/app/darmasaba/_com/NewsReader';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Center, Container, Image, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { Box, Center, Container, Group, Image, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
@@ -49,6 +50,9 @@ function Page() {
|
||||
|
||||
return (
|
||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"} px={{ base: "md", md: 0 }}>
|
||||
<Group px={{ base: "md", md: 100 }}>
|
||||
<NewsReader />
|
||||
</Group>
|
||||
<Container w={{ base: "100%", md: "50%" }} >
|
||||
<Box pb={20}>
|
||||
<Text ta={"center"} fz={"2.4rem"} c={colors["blue-button"]} fw={"bold"}>
|
||||
@@ -67,6 +71,7 @@ function Page() {
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Stack gap={"xs"}>
|
||||
<Text
|
||||
id='news-content'
|
||||
py={20}
|
||||
fz={{ base: "sm", md: "lg" }}
|
||||
lh={{ base: 1.6, md: 1.8 }} // ✅ line-height lebih rapat dan responsif
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useShallowEffect } from '@mantine/hooks';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import BackButton from '../../../layanan/_com/BackButto';
|
||||
import NewsReader from '@/app/darmasaba/_com/NewsReader';
|
||||
|
||||
function Page() {
|
||||
const detail = useProxy(stateDesaPengumuman.pengumuman.findUnique)
|
||||
@@ -30,6 +31,9 @@ function Page() {
|
||||
<BackButton />
|
||||
</Box>
|
||||
<Container size="lg" px="md">
|
||||
<Group>
|
||||
<NewsReader />
|
||||
</Group>
|
||||
<Stack gap="xs" >
|
||||
<Group justify={"space-between"} align={"center"}>
|
||||
<Text fz={{ base: "2rem", md: "2rem" }} c={colors["blue-button"]} fw="bold" >
|
||||
@@ -42,7 +46,7 @@ function Page() {
|
||||
</Group>
|
||||
</Group>
|
||||
<Paper bg={colors["white-1"]} p="md">
|
||||
<Text fz={"md"} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: detail.data?.content }} />
|
||||
<Text id='news-content' 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',
|
||||
|
||||
@@ -67,16 +67,26 @@ function Page() {
|
||||
<Text ta="center" fw={600} fz={{ base: "md", md: "lg" }} c="dimmed">
|
||||
Informasi & Pelayanan Potensi Desa Digital
|
||||
</Text>
|
||||
<Image
|
||||
src={state.findUnique.data?.image?.link || ''}
|
||||
alt={state.findUnique.data?.name || 'Potensi Desa'}
|
||||
radius="lg"
|
||||
fit="cover"
|
||||
{/* ✅ Bagian gambar dibuat konsisten tanpa CSS manual */}
|
||||
<Box
|
||||
w="100%"
|
||||
h={{ base: 220, md: 400 }}
|
||||
fallbackSrc="https://placehold.co/800x400?text=Gambar+tidak+tersedia"
|
||||
loading="lazy"
|
||||
/>
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
borderRadius: 'var(--mantine-radius-lg)',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={state.findUnique.data?.image?.link || ''}
|
||||
alt={state.findUnique.data?.name || 'Potensi Desa'}
|
||||
fit="cover"
|
||||
w="100%"
|
||||
h="100%"
|
||||
fallbackSrc="https://placehold.co/800x400?text=Gambar+tidak+tersedia"
|
||||
loading="lazy"
|
||||
radius="lg"
|
||||
/>
|
||||
</Box>
|
||||
<Text py="md" fz={{ base: "sm", md: "md" }} ta="justify" lh={1.8} dangerouslySetInnerHTML={{ __html: state.findUnique.data?.deskripsi || 'Belum ada deskripsi untuk potensi desa ini.' }} />
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
'use client'
|
||||
import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi';
|
||||
import colors from '@/con/colors';
|
||||
import { BackgroundImage, Box, Button, Center, Flex, Group, Paper, SimpleGrid, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
|
||||
import { BackgroundImage, Box, Button, Center, Flex, Group, Paper, SimpleGrid, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { IconEye } from '@tabler/icons-react';
|
||||
import { useTransitionRouter } from 'next-view-transitions';
|
||||
import { useEffect, useState } from 'react';
|
||||
@@ -12,6 +12,7 @@ import BackButton from '../layanan/_com/BackButto';
|
||||
function Page() {
|
||||
const router = useTransitionRouter()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [hoveredId, setHoveredId] = useState<string | null>(null)
|
||||
const state = useProxy(potensiDesaState)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -88,20 +89,55 @@ function Page() {
|
||||
src={v.image?.link || ''}
|
||||
h={360}
|
||||
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
|
||||
pos="absolute"
|
||||
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">
|
||||
{/* Kategori badge - always visible */}
|
||||
<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>
|
||||
</Paper>
|
||||
</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
|
||||
fw={800}
|
||||
c="white"
|
||||
@@ -113,20 +149,28 @@ function Page() {
|
||||
{v.name}
|
||||
</Text>
|
||||
</Box>
|
||||
<Group justify="center">
|
||||
<Tooltip label="Lihat detail potensi" withArrow>
|
||||
<Button
|
||||
radius="xl"
|
||||
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>
|
||||
</Tooltip>
|
||||
|
||||
{/* Button - visible on hover */}
|
||||
<Group
|
||||
justify="center"
|
||||
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'
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
radius="xl"
|
||||
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>
|
||||
</Stack>
|
||||
</BackgroundImage>
|
||||
|
||||
@@ -11,6 +11,7 @@ import ProfilPerbekel from './ui/profilPerbekel';
|
||||
import MotoDesa from './ui/motoDesa';
|
||||
import SemuaPerbekel from './ui/semuaPerbekel';
|
||||
import ScrollToTopButton from '@/app/darmasaba/_com/scrollToTopButton';
|
||||
import StrukturPerangkatDesa from './struktur-perangkat-desa/page';
|
||||
|
||||
function Page() {
|
||||
return (
|
||||
@@ -26,16 +27,17 @@ function Page() {
|
||||
</Text>
|
||||
</Stack>
|
||||
</Container>
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Stack px={{ base: "md", md: 100 }} gap={"xl"}>
|
||||
<ProfileDesa />
|
||||
<SejarahDesa />
|
||||
<VisimisiDesa />
|
||||
<LambangDesa />
|
||||
<MaskotDesa />
|
||||
<ProfilPerbekel />
|
||||
<StrukturPerangkatDesa/>
|
||||
<MotoDesa />
|
||||
<SemuaPerbekel />
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
{/* Tombol Scroll ke Atas */}
|
||||
<ScrollToTopButton />
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
'use client';
|
||||
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID';
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
Box,
|
||||
Divider,
|
||||
Group,
|
||||
Image,
|
||||
Paper,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import BackButton from '../../_com/BackButto';
|
||||
|
||||
function DetailPegawaiUser() {
|
||||
const statePegawai = useProxy(stateStrukturPPID.pegawai);
|
||||
const params = useParams();
|
||||
|
||||
useShallowEffect(() => {
|
||||
stateStrukturPPID.posisiOrganisasi.findMany.load();
|
||||
statePegawai.findUnique.load(params?.id as string);
|
||||
}, []);
|
||||
|
||||
|
||||
if (!statePegawai.findUnique.data) {
|
||||
return (
|
||||
<Stack py="lg">
|
||||
<Skeleton height={500} radius="md" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
const data = statePegawai.findUnique.data;
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'md', md: 100 }} py="xl">
|
||||
{/* Back button */}
|
||||
<Group mb="lg" px={{ base: 'md', md: 100 }}>
|
||||
<BackButton/>
|
||||
</Group>
|
||||
|
||||
<Paper
|
||||
w={{ base: '100%', md: '70%' }}
|
||||
mx="auto"
|
||||
p="xl"
|
||||
radius="lg"
|
||||
shadow="sm"
|
||||
bg="white"
|
||||
style={{
|
||||
border: '1px solid #eaeaea',
|
||||
}}
|
||||
>
|
||||
<Stack align="center" gap="md">
|
||||
{/* Foto Profil */}
|
||||
<Image
|
||||
src={data.image?.link || '/placeholder-profile.png'}
|
||||
alt={data.namaLengkap || 'Foto Profil'}
|
||||
w={160}
|
||||
h={160}
|
||||
radius={100}
|
||||
fit="cover"
|
||||
style={{ border: `2px solid ${colors['blue-button']}` }}
|
||||
loading="lazy"
|
||||
/>
|
||||
|
||||
{/* Nama & Jabatan */}
|
||||
<Stack align="center" gap={2}>
|
||||
<Title order={3} fw={700} c={colors['blue-button']}>
|
||||
{data.namaLengkap || '-'} {data.gelarAkademik || ''}
|
||||
</Title>
|
||||
<Text fz="sm" c="dimmed">
|
||||
{data.posisi?.nama || 'Posisi tidak tersedia'}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Divider my="lg" />
|
||||
|
||||
{/* Informasi Detail */}
|
||||
<Stack gap="md">
|
||||
<InfoRow label="Email" value={data.email} />
|
||||
<InfoRow label="Telepon" value={data.telepon} />
|
||||
<InfoRow label="Alamat" value={data.alamat} multiline />
|
||||
<InfoRow
|
||||
label="Tanggal Masuk"
|
||||
value={
|
||||
data.tanggalMasuk
|
||||
? new Date(data.tanggalMasuk).toLocaleDateString('id-ID', {
|
||||
day: '2-digit',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})
|
||||
: '-'
|
||||
}
|
||||
/>
|
||||
<InfoRow
|
||||
label="Status"
|
||||
value={data.isActive ? 'Aktif' : 'Tidak Aktif'}
|
||||
valueColor={data.isActive ? 'green' : 'red'}
|
||||
/>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
/* Komponen kecil untuk menampilkan baris informasi */
|
||||
function InfoRow({
|
||||
label,
|
||||
value,
|
||||
valueColor,
|
||||
multiline = false,
|
||||
}: {
|
||||
label: string;
|
||||
value?: string | null;
|
||||
valueColor?: string;
|
||||
multiline?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} c="dark">
|
||||
{label}
|
||||
</Text>
|
||||
<Text
|
||||
fz="sm"
|
||||
c={valueColor || 'dimmed'}
|
||||
style={{
|
||||
whiteSpace: multiline ? 'normal' : 'nowrap',
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
>
|
||||
{value || '-'}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default DetailPegawaiUser;
|
||||
@@ -0,0 +1,469 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client'
|
||||
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID'
|
||||
import ScrollToTopButton from '@/app/darmasaba/_com/scrollToTopButton'
|
||||
import colors from '@/con/colors'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Center,
|
||||
Group,
|
||||
Image,
|
||||
Loader,
|
||||
Paper,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
Transition
|
||||
} from '@mantine/core'
|
||||
import {
|
||||
IconArrowsMaximize,
|
||||
IconArrowsMinimize,
|
||||
IconRefresh,
|
||||
IconSearch,
|
||||
IconUsers,
|
||||
IconZoomIn,
|
||||
IconZoomOut,
|
||||
} from '@tabler/icons-react'
|
||||
import { debounce } from 'lodash'
|
||||
import { useTransitionRouter } from 'next-view-transitions'
|
||||
import { OrganizationChart } from 'primereact/organizationchart'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useProxy } from 'valtio/utils'
|
||||
import './struktur.css'
|
||||
import BackButton from '../_com/BackButto'
|
||||
|
||||
export default function StrukturPerangkatDesa() {
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
minHeight: '100vh',
|
||||
background: colors['Bg'],
|
||||
color: '#E6F0FF',
|
||||
paddingBottom: 48,
|
||||
}}
|
||||
>
|
||||
<Box px={{ base: 'md', md: 100 }} py={"xl"}>
|
||||
<BackButton />
|
||||
|
||||
<Stack align="center" gap="xl" mt="xl">
|
||||
<Title
|
||||
order={1}
|
||||
ta="center"
|
||||
c={colors['blue-button']}
|
||||
fz={{ base: 28, md: 36, lg: 44 }}
|
||||
>
|
||||
Struktur Perangkat Desa
|
||||
</Title>
|
||||
<Text ta="center" c="black" maw={800}>
|
||||
Gambaran visual peran dan pegawai yang ditugaskan. Arahkan kursor
|
||||
untuk melihat detail atau klik node untuk fokus tampilan.
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Box mt="lg">
|
||||
<StrukturPerangkatDesaNode />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<ScrollToTopButton />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function StrukturPerangkatDesaNode() {
|
||||
const stateOrganisasi: any = useProxy(stateStrukturPPID.pegawai)
|
||||
const router = useTransitionRouter()
|
||||
const chartContainerRef = useRef<HTMLDivElement>(null)
|
||||
const [scale, setScale] = useState(1)
|
||||
const [isFullscreen, setFullscreen] = useState(false)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
|
||||
// debounce pencarian
|
||||
const debouncedSearch = useRef(
|
||||
debounce((value: string) => {
|
||||
setSearchQuery(value)
|
||||
}, 400)
|
||||
).current
|
||||
|
||||
useEffect(() => {
|
||||
void stateOrganisasi.findMany.load()
|
||||
}, [])
|
||||
|
||||
const isLoading =
|
||||
!stateOrganisasi.findMany.data && stateOrganisasi.findMany.loading !== false
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Center py={48}>
|
||||
<Stack align="center" gap="sm">
|
||||
<Loader size="lg" />
|
||||
<Text fw={600}>Memuat struktur organisasi…</Text>
|
||||
<Text c="dimmed" size="sm">
|
||||
Mengambil data pegawai dan posisi. Mohon tunggu sebentar.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
)
|
||||
}
|
||||
|
||||
const data = stateOrganisasi.findMany.data || []
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<Center py={40}>
|
||||
<Stack align="center" gap="md">
|
||||
<Paper
|
||||
radius="md"
|
||||
p="xl"
|
||||
style={{
|
||||
width: 560,
|
||||
background: 'rgba(28,110,164,0.2)',
|
||||
border: `1px solid rgba(255,255,255,0.1)`,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<Center>
|
||||
<IconUsers size={56} />
|
||||
</Center>
|
||||
<Title order={3} mt="md">
|
||||
Data pegawai belum tersedia
|
||||
</Title>
|
||||
<Text c="dimmed" mt="xs">
|
||||
Belum ada data pegawai yang tercatat untuk PPID.
|
||||
</Text>
|
||||
<Group justify="center" mt="lg">
|
||||
<Button
|
||||
leftSection={<IconRefresh size={16} />}
|
||||
variant="gradient"
|
||||
gradient={{ from: 'indigo', to: 'cyan' }}
|
||||
onClick={() => stateOrganisasi.findMany.load()}
|
||||
>
|
||||
Muat Ulang
|
||||
</Button>
|
||||
</Group>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Center>
|
||||
)
|
||||
}
|
||||
|
||||
// 🧩 buat struktur organisasi
|
||||
const posisiMap = new Map<string, any>()
|
||||
const aktifPegawai = data.filter((p: any) => p.isActive)
|
||||
|
||||
for (const pegawai of aktifPegawai) {
|
||||
const posisiId = pegawai.posisi.id
|
||||
if (!posisiMap.has(posisiId)) {
|
||||
posisiMap.set(posisiId, {
|
||||
...pegawai.posisi,
|
||||
pegawaiList: [],
|
||||
children: [],
|
||||
})
|
||||
}
|
||||
posisiMap.get(posisiId)!.pegawaiList.push(pegawai)
|
||||
}
|
||||
|
||||
const root: any[] = []
|
||||
posisiMap.forEach((posisi) => {
|
||||
if (posisi.parentId) {
|
||||
const parent = posisiMap.get(posisi.parentId)
|
||||
if (parent) parent.children.push(posisi)
|
||||
else root.push(posisi)
|
||||
} else root.push(posisi)
|
||||
})
|
||||
|
||||
const toOrgChartFormat = (node: any): any => {
|
||||
const pegawai = node.pegawaiList?.[0]
|
||||
return {
|
||||
expanded: true,
|
||||
data: {
|
||||
id: pegawai?.id,
|
||||
name: pegawai?.namaLengkap || 'Belum Ditugaskan',
|
||||
title: node.nama || 'Tanpa Jabatan',
|
||||
image: pegawai?.image?.link || '/img/default.png',
|
||||
},
|
||||
children: node.children?.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 & 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 (
|
||||
<Stack align="center" mt="xl">
|
||||
{/* 🔍 Controls */}
|
||||
<Paper
|
||||
shadow="xs"
|
||||
p="md"
|
||||
radius="md"
|
||||
style={{
|
||||
background: colors['blue-button']
|
||||
}}
|
||||
>
|
||||
<Group gap="sm" wrap="wrap" justify="center">
|
||||
<TextInput
|
||||
placeholder="Cari nama atau jabatan..."
|
||||
leftSection={<IconSearch size={16} />}
|
||||
onChange={(e) => debouncedSearch(e.target.value)}
|
||||
styles={{
|
||||
input: {
|
||||
minWidth: 250,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Group gap="xs">
|
||||
<Button
|
||||
variant="light"
|
||||
bg={colors['blue-button-2']}
|
||||
size="sm"
|
||||
onClick={handleZoomOut}
|
||||
leftSection={<IconZoomOut size={16} />}
|
||||
c={colors['blue-button']}
|
||||
>
|
||||
Zoom Out
|
||||
</Button>
|
||||
|
||||
<Box
|
||||
bg={colors['blue-button-2']}
|
||||
c={colors['blue-button']}
|
||||
px={16}
|
||||
py={8}
|
||||
style={{
|
||||
fontSize: 14,
|
||||
fontWeight: 700,
|
||||
borderRadius: '8px',
|
||||
minWidth: 70,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
{Math.round(scale * 100)}%
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
bg={colors['blue-button-2']}
|
||||
c={colors['blue-button']}
|
||||
variant="light"
|
||||
size="sm"
|
||||
onClick={handleZoomIn}
|
||||
leftSection={<IconZoomIn size={16} />}
|
||||
>
|
||||
Zoom In
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
bg={colors['blue-button-2']}
|
||||
c={colors['blue-button']}
|
||||
variant="light"
|
||||
size="sm"
|
||||
onClick={resetZoom}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
bg={colors['blue-button-2']}
|
||||
c={colors['blue-button']}
|
||||
size="sm"
|
||||
onClick={toggleFullscreen}
|
||||
leftSection={
|
||||
isFullscreen ? (
|
||||
<IconArrowsMinimize size={16} />
|
||||
) : (
|
||||
<IconArrowsMaximize size={16} />
|
||||
)
|
||||
}
|
||||
>
|
||||
Fullscreen
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
</Paper>
|
||||
|
||||
{/* 🧩 Chart Container */}
|
||||
<Center style={{ width: '100%' }}>
|
||||
<Box
|
||||
ref={chartContainerRef}
|
||||
style={{
|
||||
overflowX: 'auto',
|
||||
overflowY: 'auto',
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
padding: '32px 16px',
|
||||
transition: 'transform 0.2s ease',
|
||||
transform: `scale(${scale})`,
|
||||
transformOrigin: 'center top',
|
||||
}}
|
||||
>
|
||||
<OrganizationChart
|
||||
value={chartData}
|
||||
nodeTemplate={(node) => <NodeCard node={node} router={router} />}
|
||||
className="p-organizationchart p-organizationchart-horizontal"
|
||||
/>
|
||||
</Box>
|
||||
</Center>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
function NodeCard({ node, router }: any) {
|
||||
const imageSrc = node?.data?.image || '/img/default.png'
|
||||
const name = node?.data?.name || 'Tanpa Nama'
|
||||
const title = node?.data?.title || 'Tanpa Jabatan'
|
||||
const hasId = Boolean(node?.data?.id)
|
||||
|
||||
return (
|
||||
<Transition mounted transition="pop" duration={300}>
|
||||
{(styles) => (
|
||||
<Card
|
||||
shadow="md"
|
||||
radius="xl"
|
||||
withBorder
|
||||
style={{
|
||||
...styles,
|
||||
width: 240,
|
||||
minHeight: 280,
|
||||
padding: 20,
|
||||
background: 'linear-gradient(135deg, rgba(28,110,164,0.15) 0%, rgba(255,255,255,0.95) 100%)',
|
||||
borderColor: 'rgba(28, 110, 164, 0.3)',
|
||||
borderWidth: 2,
|
||||
transition: 'all 0.3s ease',
|
||||
cursor: hasId ? 'pointer' : 'default',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (hasId) {
|
||||
e.currentTarget.style.transform = 'translateY(-4px)'
|
||||
e.currentTarget.style.boxShadow = '0 8px 24px rgba(28, 110, 164, 0.25)'
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (hasId) {
|
||||
e.currentTarget.style.transform = 'translateY(0)'
|
||||
e.currentTarget.style.boxShadow = ''
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Stack align="center" gap={12}>
|
||||
{/* Photo */}
|
||||
<Box
|
||||
style={{
|
||||
width: 96,
|
||||
height: 96,
|
||||
borderRadius: '50%',
|
||||
overflow: 'hidden',
|
||||
border: '3px solid rgba(28, 110, 164, 0.4)',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
|
||||
background: 'white',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={imageSrc}
|
||||
alt={name}
|
||||
width={96}
|
||||
height={96}
|
||||
fit="cover"
|
||||
loading="lazy"
|
||||
style={{
|
||||
objectFit: 'cover',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Name */}
|
||||
<Text
|
||||
fw={700}
|
||||
size="sm"
|
||||
ta="center"
|
||||
c={colors['blue-button']}
|
||||
lineClamp={2}
|
||||
style={{
|
||||
minHeight: 40,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
wordBreak: 'break-word',
|
||||
lineHeight: 1.3,
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</Text>
|
||||
|
||||
{/* Title/Position */}
|
||||
<Text
|
||||
size="xs"
|
||||
c="dimmed"
|
||||
ta="center"
|
||||
fw={500}
|
||||
lineClamp={2}
|
||||
style={{
|
||||
minHeight: 32,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
wordBreak: 'break-word',
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
|
||||
{/* Detail Button */}
|
||||
{hasId && (
|
||||
<Button
|
||||
variant="gradient"
|
||||
gradient={{ from: 'blue', to: 'cyan' }}
|
||||
size="xs"
|
||||
fullWidth
|
||||
mt={8}
|
||||
radius="md"
|
||||
onClick={() =>
|
||||
router.push(`/darmasaba/desa/profile/struktur-perangkat-desa/${node.data.id}`)
|
||||
}
|
||||
style={{
|
||||
height: 32,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
Lihat Detail
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</Card>
|
||||
)}
|
||||
</Transition>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/* ============================================
|
||||
STRUKTUR ORGANISASI PPID - STYLING
|
||||
============================================ */
|
||||
|
||||
/* Tabel chart selalu center */
|
||||
.p-organizationchart-table {
|
||||
margin: 0 auto !important;
|
||||
}
|
||||
|
||||
/* Jarak vertikal antar level - lebih lega */
|
||||
.p-organizationchart-line-down {
|
||||
height: 32px !important;
|
||||
}
|
||||
|
||||
/* Padding di dalam node - lebih rapi */
|
||||
.p-organizationchart-node-content {
|
||||
padding: 0 !important;
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
/* Garis connector antar node - lebih tebal dan jelas */
|
||||
.p-organizationchart-line-down,
|
||||
.p-organizationchart-line-left,
|
||||
.p-organizationchart-line-right,
|
||||
.p-organizationchart-line-top {
|
||||
border-color: rgba(28, 110, 164, 0.4) !important;
|
||||
border-width: 2px !important;
|
||||
}
|
||||
|
||||
/* Garis horizontal */
|
||||
.p-organizationchart-line-left,
|
||||
.p-organizationchart-line-right {
|
||||
border-top-width: 2px !important;
|
||||
}
|
||||
|
||||
/* Jarak horizontal antar node - lebih proporsional */
|
||||
.p-organizationchart-table > tbody > tr > td {
|
||||
padding: 0 24px !important;
|
||||
vertical-align: top !important;
|
||||
}
|
||||
|
||||
/* Node container spacing */
|
||||
.p-organizationchart-node {
|
||||
padding: 8px !important;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.p-organizationchart-table > tbody > tr > td {
|
||||
padding: 0 12px !important;
|
||||
}
|
||||
|
||||
.p-organizationchart-line-down {
|
||||
height: 24px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Smooth transitions untuk zoom */
|
||||
.p-organizationchart {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
/* Fullscreen mode adjustments */
|
||||
.p-organizationchart-table:fullscreen {
|
||||
background: rgba(230, 240, 255, 0.98);
|
||||
padding: 40px;
|
||||
}
|
||||
@@ -24,7 +24,7 @@ function LambangDesa() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box pb={90}>
|
||||
<Box>
|
||||
<Stack align="center" gap="lg">
|
||||
<Box pb="lg">
|
||||
<Center>
|
||||
@@ -59,7 +59,7 @@ function LambangDesa() {
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
fz={{ base: 'md', md: 'lg' }}
|
||||
fz={{ base: '1.125rem', md: '1.375rem' }}
|
||||
lh={1.8}
|
||||
c="dark"
|
||||
ta="justify"
|
||||
|
||||
@@ -28,7 +28,7 @@ function MaskotDesa() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box pb={80}>
|
||||
<Box>
|
||||
<Stack align="center" gap="xl">
|
||||
<Stack align="center" gap={10}>
|
||||
<Image src="/pudak-icon.png" alt="Ikon Desa" w={{ base: 160, md: 240 }} loading="lazy"/>
|
||||
|
||||
@@ -36,7 +36,7 @@ const letters = ["S", "I", "G", "A", "P"];
|
||||
|
||||
function MotoDesa() {
|
||||
return (
|
||||
<Box pb={80} px={{ base: "md", md: "xl" }}>
|
||||
<Box px={{ base: "md", md: "xl" }}>
|
||||
<Stack align="center" gap="lg">
|
||||
<Box>
|
||||
<Text
|
||||
|
||||
@@ -25,7 +25,7 @@ function ProfilPerbekel() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box pb={80} px="md">
|
||||
<Box px="md">
|
||||
<Stack align="center" gap={0} mb={40}>
|
||||
<Text
|
||||
c={colors['blue-button']}
|
||||
@@ -95,7 +95,7 @@ function ProfilPerbekel() {
|
||||
<Box>
|
||||
<Stack gap={6}>
|
||||
<Stack align="center" gap={6}>
|
||||
<IconUser size={22} color={colors['blue-button']} />
|
||||
<IconUser size={22} />
|
||||
<Text fz={{ base: "1.2rem", md: "1.5rem" }} fw="bold">Biodata</Text>
|
||||
</Stack>
|
||||
<Text
|
||||
@@ -111,12 +111,12 @@ function ProfilPerbekel() {
|
||||
<Box>
|
||||
<Stack gap={6}>
|
||||
<Stack align="center" gap={6}>
|
||||
<IconBriefcase size={22} color={colors['blue-button']} />
|
||||
<IconBriefcase size={22} />
|
||||
<Text fz={{ base: "1.2rem", md: "1.5rem" }} fw="bold">Pengalaman</Text>
|
||||
</Stack>
|
||||
<Text
|
||||
fz={{ base: "1rem", md: "1.2rem" }}
|
||||
ta="justify"
|
||||
ta="left"
|
||||
lh={1.6}
|
||||
dangerouslySetInnerHTML={{ __html: data.pengalaman }}
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
@@ -138,7 +138,7 @@ function ProfilPerbekel() {
|
||||
<Stack gap="xl">
|
||||
<Box>
|
||||
<Stack align="center" gap={6} >
|
||||
<IconUsers size={22} color={colors['blue-button']} />
|
||||
<IconUsers size={22} />
|
||||
<Text fz={{ base: "1.2rem", md: "1.5rem" }} fw="bold">Pengalaman Organisasi</Text>
|
||||
</Stack>
|
||||
<Text
|
||||
@@ -152,7 +152,7 @@ function ProfilPerbekel() {
|
||||
|
||||
<Box>
|
||||
<Stack align="center" gap={6} mb={6}>
|
||||
<IconTargetArrow size={22} color={colors['blue-button']} />
|
||||
<IconTargetArrow size={22} />
|
||||
<Text fz={{ base: "1.2rem", md: "1.5rem" }} fw="bold">Program Kerja Unggulan</Text>
|
||||
</Stack>
|
||||
<Box px={10}>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Box, Center, Paper } from '@mantine/core';
|
||||
|
||||
function ProfileDesa() {
|
||||
return (
|
||||
<Box pb={90}>
|
||||
<Box>
|
||||
<Center>
|
||||
<Paper p={"xl"} bg={colors['white-trans-1']} w={{ base: "100%", md: "100%" }}>
|
||||
<Center>
|
||||
|
||||
@@ -24,7 +24,7 @@ function SejarahDesa() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py="xl">
|
||||
<Box>
|
||||
<Stack align="center" gap="xl">
|
||||
<Stack align="center" gap="sm">
|
||||
<Center>
|
||||
|
||||
@@ -36,7 +36,7 @@ function SemuaPerbekel() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box pb={80}>
|
||||
<Box>
|
||||
<Stack align="center" gap="lg">
|
||||
<Box>
|
||||
<Text
|
||||
|
||||
@@ -24,7 +24,7 @@ function VisiMisiDesa() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py="xl">
|
||||
<Box>
|
||||
<Stack align="center" gap="xl">
|
||||
<Image
|
||||
src="/darmasaba-icon.png"
|
||||
|
||||
@@ -52,7 +52,7 @@ function Page() {
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Box px={{ base: 'md', md: 100 }} pb={80}>
|
||||
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
|
||||
Lowongan Kerja Lokal
|
||||
</Text>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client'
|
||||
|
||||
import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi'
|
||||
import colors from '@/con/colors'
|
||||
import {
|
||||
@@ -9,20 +9,28 @@ import {
|
||||
Button,
|
||||
Card,
|
||||
Center,
|
||||
Container,
|
||||
Group,
|
||||
Image,
|
||||
Loader,
|
||||
Paper,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
Tooltip,
|
||||
Transition,
|
||||
Transition
|
||||
} from '@mantine/core'
|
||||
import { IconRefresh, IconSearch, IconUsers } from '@tabler/icons-react'
|
||||
import {
|
||||
IconArrowsMaximize,
|
||||
IconArrowsMinimize,
|
||||
IconRefresh,
|
||||
IconSearch,
|
||||
IconUsers,
|
||||
IconZoomIn,
|
||||
IconZoomOut,
|
||||
} from '@tabler/icons-react'
|
||||
import { debounce } from 'lodash'
|
||||
import { OrganizationChart } from 'primereact/organizationchart'
|
||||
import { useEffect } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useProxy } from 'valtio/utils'
|
||||
import BackButton from '../../desa/layanan/_com/BackButto'
|
||||
|
||||
@@ -36,35 +44,40 @@ export default function Page() {
|
||||
paddingBottom: 48,
|
||||
}}
|
||||
>
|
||||
<Container size="xl" py="xl">
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
<Box px={{ base: 'md', md: 100 }} py="xl">
|
||||
<BackButton />
|
||||
<Stack align="center" gap="xl" mt="xl">
|
||||
<Title
|
||||
order={1}
|
||||
ta="center"
|
||||
c={colors['blue-button']}
|
||||
fz={{ base: 28, md: 36, lg: 44 }}
|
||||
|
||||
>
|
||||
Struktur Organisasi Dan SK Pengurus BumDes
|
||||
Struktur Organisasi & SK Pengurus BumDes
|
||||
</Title>
|
||||
<Text ta="center" c="black" maw={800}>
|
||||
Gambaran visual peran dan pegawai yang ditugaskan. Arahkan kursor
|
||||
untuk melihat detail atau klik node untuk fokus tampilan.
|
||||
Gambaran visual peran dan pengurus yang ditugaskan. Gunakan kontrol
|
||||
di bawah untuk mencari, memperbesar, atau melihat lebih jelas.
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Box mt="lg">
|
||||
<StrukturOrganisasiBumDes />
|
||||
</Box>
|
||||
</Container>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function StrukturOrganisasiBumDes() {
|
||||
const stateOrganisasi: any = useProxy(stateStrukturBumDes.pegawai)
|
||||
const chartContainerRef = useRef<HTMLDivElement>(null)
|
||||
const [scale, setScale] = useState(1)
|
||||
const [isFullscreen, setFullscreen] = useState(false)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const debouncedSearch = useRef(
|
||||
debounce((value: string) => setSearchQuery(value), 400)
|
||||
).current
|
||||
|
||||
useEffect(() => {
|
||||
void stateOrganisasi.findMany.load()
|
||||
@@ -81,17 +94,15 @@ function StrukturOrganisasiBumDes() {
|
||||
<Loader size="lg" />
|
||||
<Text fw={600}>Memuat struktur organisasi…</Text>
|
||||
<Text c="dimmed" size="sm">
|
||||
Mengambil data pegawai dan posisi. Mohon tunggu sebentar.
|
||||
Mengambil data pengurus dan posisi. Mohon tunggu sebentar.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
!stateOrganisasi.findMany.data ||
|
||||
stateOrganisasi.findMany.data.length === 0
|
||||
) {
|
||||
const data = stateOrganisasi.findMany.data || []
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<Center py={40}>
|
||||
<Stack align="center" gap="md">
|
||||
@@ -109,11 +120,10 @@ function StrukturOrganisasiBumDes() {
|
||||
<IconUsers size={56} />
|
||||
</Center>
|
||||
<Title order={3} mt="md">
|
||||
Data pegawai belum tersedia
|
||||
Data pengurus belum tersedia
|
||||
</Title>
|
||||
<Text c="dimmed" mt="xs">
|
||||
Belum ada data pegawai yang tercatat untuk BumDes. Silakan coba
|
||||
muat ulang atau periksa sumber data.
|
||||
Belum ada data pengurus yang tercatat untuk BumDes.
|
||||
</Text>
|
||||
<Group justify="center" mt="lg">
|
||||
<Button
|
||||
@@ -124,15 +134,6 @@ function StrukturOrganisasiBumDes() {
|
||||
>
|
||||
Muat Ulang
|
||||
</Button>
|
||||
<Button
|
||||
leftSection={<IconSearch size={16} />}
|
||||
variant="subtle"
|
||||
onClick={() =>
|
||||
stateOrganisasi.findMany.load({ query: { q: '' } })
|
||||
}
|
||||
>
|
||||
Cari Pegawai
|
||||
</Button>
|
||||
</Group>
|
||||
</Paper>
|
||||
</Stack>
|
||||
@@ -140,161 +141,232 @@ function StrukturOrganisasiBumDes() {
|
||||
)
|
||||
}
|
||||
|
||||
// 📊 susun struktur organisasi
|
||||
const posisiMap = new Map<string, any>()
|
||||
|
||||
const aktifPegawai = stateOrganisasi.findMany.data.filter((p: any) => p.isActive);
|
||||
const aktifPegawai = data.filter((p: any) => p.isActive)
|
||||
|
||||
for (const pegawai of aktifPegawai) {
|
||||
const posisiId = pegawai.posisi.id;
|
||||
const posisiId = pegawai.posisi.id
|
||||
if (!posisiMap.has(posisiId)) {
|
||||
posisiMap.set(posisiId, {
|
||||
...pegawai.posisi,
|
||||
pegawaiList: [],
|
||||
children: [],
|
||||
});
|
||||
})
|
||||
}
|
||||
posisiMap.get(posisiId)!.pegawaiList.push(pegawai);
|
||||
posisiMap.get(posisiId)!.pegawaiList.push(pegawai)
|
||||
}
|
||||
|
||||
// First, create a map of all unique positions
|
||||
const allPositions = new Map();
|
||||
aktifPegawai.forEach((pegawai: any) => {
|
||||
if (!allPositions.has(pegawai.posisi.id)) {
|
||||
allPositions.set(pegawai.posisi.id, {
|
||||
...pegawai.posisi,
|
||||
pegawaiList: [],
|
||||
children: []
|
||||
});
|
||||
}
|
||||
});
|
||||
const root: any[] = []
|
||||
posisiMap.forEach((posisi) => {
|
||||
if (posisi.parentId) {
|
||||
const parent = posisiMap.get(posisi.parentId)
|
||||
if (parent) parent.children.push(posisi)
|
||||
else root.push(posisi)
|
||||
} else root.push(posisi)
|
||||
})
|
||||
|
||||
// Then assign employees to their positions
|
||||
aktifPegawai.forEach((pegawai: any) => {
|
||||
const posisi = allPositions.get(pegawai.posisi.id);
|
||||
if (posisi) {
|
||||
posisi.pegawaiList.push(pegawai);
|
||||
}
|
||||
});
|
||||
|
||||
// Now build the hierarchy
|
||||
const root = [];
|
||||
for (const [_, posisi] of allPositions) {
|
||||
if (posisi.parentId) {
|
||||
const parent = allPositions.get(posisi.parentId);
|
||||
if (parent) {
|
||||
parent.children.push(posisi);
|
||||
} else {
|
||||
// Only add to root if it's a top-level position
|
||||
if (!posisi.parentId) {
|
||||
root.push(posisi);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
root.push(posisi);
|
||||
}
|
||||
}
|
||||
function toOrgChartFormat(node: any): any {
|
||||
const toOrgChartFormat = (node: any): any => {
|
||||
const pegawai = node.pegawaiList?.[0]
|
||||
return {
|
||||
expanded: true,
|
||||
type: 'person',
|
||||
styleClass: 'p-person',
|
||||
data: {
|
||||
name: node.pegawaiList?.[0]?.namaLengkap || 'Belum ditugaskan',
|
||||
title: node.nama || 'Tanpa jabatan',
|
||||
image: node.pegawaiList?.[0]?.image?.link || '/img/default.png',
|
||||
id: pegawai?.id,
|
||||
name: pegawai?.namaLengkap || 'Belum Ditugaskan',
|
||||
title: node.nama || 'Tanpa Jabatan',
|
||||
image: pegawai?.image?.link || '/img/default.png',
|
||||
description: node.deskripsi || '',
|
||||
positionId: node.id || null,
|
||||
},
|
||||
children: node.children?.map(toOrgChartFormat) || [],
|
||||
}
|
||||
}
|
||||
|
||||
const chartData = root.map(toOrgChartFormat)
|
||||
let chartData = root.map(toOrgChartFormat)
|
||||
|
||||
// 🔍 filter by search
|
||||
if (searchQuery) {
|
||||
const filterNodes = (nodes: any[]): any[] =>
|
||||
nodes
|
||||
.map((n) => ({
|
||||
...n,
|
||||
children: filterNodes(n.children || []),
|
||||
}))
|
||||
.filter(
|
||||
(n) =>
|
||||
n.data.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
n.data.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
n.children.length > 0
|
||||
)
|
||||
chartData = filterNodes(chartData)
|
||||
}
|
||||
|
||||
// 🔍 fullscreen dan zoom control
|
||||
const toggleFullscreen = () => {
|
||||
if (!document.fullscreenElement) {
|
||||
chartContainerRef.current?.requestFullscreen()
|
||||
setFullscreen(true)
|
||||
} else {
|
||||
document.exitFullscreen()
|
||||
setFullscreen(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleZoomIn = () => setScale((s) => Math.min(s + 0.1, 2))
|
||||
const handleZoomOut = () => setScale((s) => Math.max(s - 0.1, 0.5))
|
||||
const resetZoom = () => setScale(1)
|
||||
|
||||
return (
|
||||
<Box py={16} >
|
||||
<Paper
|
||||
radius="md"
|
||||
p="md"
|
||||
style={{
|
||||
background: 'rgba(28,110,164,0.2)',
|
||||
border: `1px solid rgba(255,255,255,0.1)`,
|
||||
overflowX: 'auto',
|
||||
}}
|
||||
>
|
||||
<OrganizationChart
|
||||
value={chartData}
|
||||
nodeTemplate={nodeTemplate}
|
||||
/>
|
||||
<Stack align="center" mt="xl">
|
||||
{/* 🧭 Kontrol atas */}
|
||||
<Paper shadow="xs" p="md" radius="md" bg={colors['blue-button']}>
|
||||
<Group gap="sm" wrap="wrap" justify="center">
|
||||
<TextInput
|
||||
placeholder="Cari nama atau jabatan..."
|
||||
leftSection={<IconSearch size={16} />}
|
||||
onChange={(e) => debouncedSearch(e.target.value)}
|
||||
styles={{
|
||||
input: {
|
||||
minWidth: 250,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Group gap="xs">
|
||||
<Button
|
||||
variant="light"
|
||||
bg={colors['blue-button-2']}
|
||||
c={colors['blue-button']}
|
||||
size="sm"
|
||||
onClick={handleZoomOut}
|
||||
leftSection={<IconZoomOut size={16} />}
|
||||
>
|
||||
Zoom Out
|
||||
</Button>
|
||||
<Box
|
||||
bg={colors['blue-button-2']}
|
||||
c={colors['blue-button']}
|
||||
px={16}
|
||||
py={8}
|
||||
style={{
|
||||
fontSize: 14,
|
||||
fontWeight: 700,
|
||||
borderRadius: '8px',
|
||||
minWidth: 70,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
{Math.round(scale * 100)}%
|
||||
</Box>
|
||||
<Button
|
||||
variant="light"
|
||||
bg={colors['blue-button-2']}
|
||||
c={colors['blue-button']}
|
||||
size="sm"
|
||||
onClick={handleZoomIn}
|
||||
leftSection={<IconZoomIn size={16} />}
|
||||
>
|
||||
Zoom In
|
||||
</Button>
|
||||
<Button
|
||||
variant="light"
|
||||
bg={colors['blue-button-2']}
|
||||
c={colors['blue-button']}
|
||||
size="sm"
|
||||
onClick={resetZoom}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
<Button
|
||||
variant="light"
|
||||
bg={colors['blue-button-2']}
|
||||
c={colors['blue-button']}
|
||||
size="sm"
|
||||
onClick={toggleFullscreen}
|
||||
leftSection={
|
||||
isFullscreen ? (
|
||||
<IconArrowsMinimize size={16} />
|
||||
) : (
|
||||
<IconArrowsMaximize size={16} />
|
||||
)
|
||||
}
|
||||
>
|
||||
Fullscreen
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
{/* 📊 Chart Container */}
|
||||
<Center style={{ width: '100%' }}>
|
||||
<Box
|
||||
ref={chartContainerRef}
|
||||
style={{
|
||||
overflowX: 'auto',
|
||||
overflowY: 'auto',
|
||||
width: '100%',
|
||||
padding: '32px 16px',
|
||||
transition: 'transform 0.2s ease',
|
||||
transform: `scale(${scale})`,
|
||||
transformOrigin: 'center top',
|
||||
}}
|
||||
>
|
||||
<OrganizationChart
|
||||
value={chartData}
|
||||
nodeTemplate={(node) => <NodeCard node={node} />}
|
||||
className="p-organizationchart p-organizationchart-horizontal"
|
||||
/>
|
||||
</Box>
|
||||
</Center>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
function nodeTemplate(node: any) {
|
||||
function NodeCard({ node }: any) {
|
||||
const imageSrc = node?.data?.image || '/img/default.png'
|
||||
const name = node?.data?.name || 'Tanpa Nama'
|
||||
const title = node?.data?.title || 'Tanpa Jabatan'
|
||||
const description = node?.data?.description || ''
|
||||
|
||||
return (
|
||||
<Transition mounted transition="pop" duration={240}>
|
||||
<Transition mounted transition="pop" duration={300}>
|
||||
{(styles) => (
|
||||
<Card
|
||||
radius="lg"
|
||||
shadow="md"
|
||||
radius="xl"
|
||||
withBorder
|
||||
style={{
|
||||
...styles,
|
||||
width: 260,
|
||||
padding: 16,
|
||||
background: 'rgba(28,110,164,0.3)',
|
||||
borderColor: 'rgba(255,255,255,0.15)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
textAlign: 'center',
|
||||
width: 240,
|
||||
padding: 20,
|
||||
background:
|
||||
'linear-gradient(135deg, rgba(28,110,164,0.15) 0%, rgba(255,255,255,0.95) 100%)',
|
||||
borderColor: 'rgba(28, 110, 164, 0.3)',
|
||||
transition: 'all 0.3s ease',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={imageSrc}
|
||||
alt={name}
|
||||
radius="md"
|
||||
width={120}
|
||||
height={120}
|
||||
fit="cover"
|
||||
style={{
|
||||
objectFit: 'cover',
|
||||
border: '2px solid rgba(255,255,255,0.2)',
|
||||
marginBottom: 12,
|
||||
}}
|
||||
loading='lazy'
|
||||
/>
|
||||
<Text fw={700}>{name}</Text>
|
||||
<Text size="sm" c="dimmed" mt={4}>
|
||||
{title}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" mt={8} lineClamp={3}>
|
||||
{description || 'Belum ada deskripsi.'}
|
||||
</Text>
|
||||
<Tooltip label="Kembali ke struktur organisasi" withArrow position="bottom">
|
||||
<Button
|
||||
variant="light"
|
||||
size="xs"
|
||||
mt="md"
|
||||
onClick={() => {
|
||||
const id = node?.data?.positionId
|
||||
if (id && (window as any).scrollTo) {
|
||||
;(window as any).scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
<Stack align="center" gap={10}>
|
||||
<Box
|
||||
style={{
|
||||
width: 90,
|
||||
height: 90,
|
||||
borderRadius: '50%',
|
||||
overflow: 'hidden',
|
||||
border: '3px solid rgba(28, 110, 164, 0.4)',
|
||||
}}
|
||||
>
|
||||
Kembali
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Image src={imageSrc} alt={name} fit="cover" loading="lazy" />
|
||||
</Box>
|
||||
<Text fw={700} size="sm" ta="center" c={colors['blue-button']}>
|
||||
{name}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" ta="center">
|
||||
{title}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" ta="center" lineClamp={3}>
|
||||
{description || 'Belum ada deskripsi.'}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Card>
|
||||
)}
|
||||
</Transition>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ function Page() {
|
||||
}}
|
||||
>
|
||||
<Paper p={'xl'} >
|
||||
<Stack gap={"xs"}>
|
||||
<Text fz={'h3'} fw={'bold'} c={colors['blue-button']}>Tujuan Ide Inovatif Ini</Text>
|
||||
<List>
|
||||
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Mendorong partisipasi aktif masyarakat</ListItem>
|
||||
@@ -62,6 +63,7 @@ function Page() {
|
||||
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Memecahkan tantangan komunal</ListItem>
|
||||
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Mengembangkan potensi kreativitas warga</ListItem>
|
||||
</List>
|
||||
</Stack>
|
||||
</Paper>
|
||||
<Paper p={'xl'} >
|
||||
<Flex align={'center'} justify={'space-between'}>
|
||||
|
||||
@@ -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;
|
||||
@@ -1,17 +1,19 @@
|
||||
'use client'
|
||||
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 BackButton from '../../desa/layanan/_com/BackButto';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import desaDigitalState from '@/app/admin/(dashboard)/_state/inovasi/desa-digital';
|
||||
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||
import { IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
function Page() {
|
||||
const [search, setSearch] = useState("")
|
||||
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
|
||||
const state = useProxy(desaDigitalState)
|
||||
const router = useRouter()
|
||||
const {
|
||||
data,
|
||||
page,
|
||||
@@ -39,10 +41,10 @@ function Page() {
|
||||
<BackButton />
|
||||
</Box>
|
||||
<Box px={{ base: 'md', md: 100 }} >
|
||||
<Grid align='center'>
|
||||
<Grid align='center'>
|
||||
<GridCol span={{ base: 12, md: 9 }}>
|
||||
<Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
|
||||
Desa Digital / Smart Village
|
||||
Desa Digital / Smart Village
|
||||
</Text>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 3 }}>
|
||||
@@ -70,12 +72,62 @@ function Page() {
|
||||
>
|
||||
{filteredData.map((v, k) => {
|
||||
return (
|
||||
<Paper p={'xl'} key={k}>
|
||||
<Image src={v.image.link? v.image.link : ''} pb={10} radius={10} alt='' loading="lazy"/>
|
||||
<Text fz={'h3'} fw={'bold'} c={colors['blue-button']}>{v.name}</Text>
|
||||
<Box>
|
||||
<Text fz={"md"} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: v.deskripsi }} />
|
||||
</Box>
|
||||
<Paper
|
||||
key={k}
|
||||
radius="xl"
|
||||
shadow="md"
|
||||
withBorder
|
||||
p="lg"
|
||||
bg={colors['white-trans-1']}
|
||||
style={{
|
||||
transition: 'all 200ms ease',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between', // ✅ biar button selalu di bawah
|
||||
height: '100%', // ✅ bikin tinggi seragam
|
||||
}}
|
||||
>
|
||||
<Stack 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>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -57,7 +57,8 @@ function Page() {
|
||||
/>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
<Text fz={'h4'}>Desa Darmasaba berkomitmen mengembangkan teknologi tepat guna yang sesuai dengan kebutuhan masyarakat, mendukung pembangunan berkelanjutan, dan meningkatkan kualitas hidup warga.</Text>
|
||||
<Text fz={'md'}>Desa Darmasaba berkomitmen mengembangkan teknologi tepat guna yang sesuai dengan kebutuhan masyarakat,</Text>
|
||||
<Text fz={'md'}>mendukung pembangunan berkelanjutan, dan meningkatkan kualitas hidup warga.</Text>
|
||||
</Box>
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Stack gap={'lg'} p={'lg'}>
|
||||
@@ -71,11 +72,22 @@ function Page() {
|
||||
{filteredData.map((v, k) => {
|
||||
return (
|
||||
<Paper p={'xl'} key={k}>
|
||||
<Image src={v.image.link || ''} pb={10} radius={10} alt='' loading="lazy"/>
|
||||
<Text fz={'h3'} fw={'bold'} c={colors['blue-button']}>{v.name}</Text>
|
||||
<Box pr={'lg'} pb={10}>
|
||||
<Text fz={'h4'} fw={'bold'} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: v.deskripsi }} />
|
||||
</Box>
|
||||
<Stack gap={"xs"}>
|
||||
<Image src={v.image.link || ''} pb={10} radius={10} alt='' loading="lazy" />
|
||||
<Text fz={'h3'} fw={'bold'}>{v.name}</Text>
|
||||
<Box pr={'lg'} pb={10}>
|
||||
<Text
|
||||
size="md"
|
||||
ta="justify"
|
||||
lh={1} // line height biar enak dibaca
|
||||
style={{
|
||||
wordBreak: "break-word",
|
||||
whiteSpace: "normal",
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -75,18 +75,18 @@ function AdministrasiOnline() {
|
||||
<Title order={3}>Ajukan Administrasi Online</Title>
|
||||
<TextInput
|
||||
label={<Text fz="sm" fw="bold">Nama</Text>}
|
||||
placeholder="masukkan nama"
|
||||
placeholder="Masukkan nama"
|
||||
onChange={(val) => (state.administrasiOnline.create.form.name = val.target.value)}
|
||||
/>
|
||||
<TextInput
|
||||
label={<Text fz="sm" fw="bold">Alamat</Text>}
|
||||
placeholder="masukkan alamat"
|
||||
placeholder="Masukkan alamat"
|
||||
onChange={(val) => (state.administrasiOnline.create.form.alamat = val.target.value)}
|
||||
/>
|
||||
<TextInput
|
||||
type="number"
|
||||
label={<Text fz="sm" fw="bold">Nomor Telepon</Text>}
|
||||
placeholder="masukkan nomor telepon"
|
||||
placeholder="Masukkan nomor telepon"
|
||||
onChange={(val) => (state.administrasiOnline.create.form.nomorTelepon = val.target.value)}
|
||||
/>
|
||||
<Select
|
||||
@@ -95,7 +95,7 @@ function AdministrasiOnline() {
|
||||
state.administrasiOnline.create.form.jenisLayananId = val ?? "";
|
||||
}}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Jenis Layanan</Text>}
|
||||
placeholder="Pilih kategori produk"
|
||||
placeholder="Pilih jenis layanan"
|
||||
data={
|
||||
state.jenisLayanan.findMany.data?.map((v) => ({
|
||||
value: v.id,
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -1,20 +1,23 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
|
||||
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Card, Divider, Grid, GridCol, Image, Paper, Stack, Text, Title } from '@mantine/core';
|
||||
import { Badge, Box, Card, Divider, Grid, GridCol, Group, Image, Paper, Stack, Text, Title } from '@mantine/core';
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import { useEffect } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import BackButton from '../../../desa/layanan/_com/BackButto';
|
||||
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
|
||||
import { useTransitionRouter } from 'next-view-transitions';
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
function InformasiDesa() {
|
||||
const stateBerita = useProxy(stateDashboardBerita.berita)
|
||||
const statePengumuman = useProxy(stateDesaPengumuman.pengumuman)
|
||||
const router = useTransitionRouter()
|
||||
const stateBerita = useProxy(stateDashboardBerita.berita);
|
||||
const statePengumuman = useProxy(stateDesaPengumuman.pengumuman);
|
||||
|
||||
useEffect(() => {
|
||||
stateBerita.findFirst.load();
|
||||
@@ -23,116 +26,216 @@ function InformasiDesa() {
|
||||
statePengumuman.findRecent.load();
|
||||
}, []);
|
||||
|
||||
const dataBerita = stateBerita.findFirst.data
|
||||
const dataPengumuman = statePengumuman.findFirst.data
|
||||
|
||||
const dataBerita = stateBerita.findFirst.data;
|
||||
const dataPengumuman = statePengumuman.findFirst.data;
|
||||
|
||||
return (
|
||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap={22}>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
<Box px={{ base: 'md', md: 100 }} >
|
||||
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
|
||||
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Title ta="center" fz={{ base: 'h1', md: '2.5rem' }} c={colors['blue-button']} fw="bold">
|
||||
Informasi Desa
|
||||
</Text>
|
||||
</Title>
|
||||
</Box>
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Stack gap={10}>
|
||||
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Stack gap={30}>
|
||||
|
||||
{/* === BERITA UTAMA === */}
|
||||
{dataBerita && (
|
||||
<Paper shadow="md" radius="md" p="md">
|
||||
<Grid>
|
||||
<GridCol span={{ md: 6, base: 12 }}>
|
||||
<Image
|
||||
src={dataBerita.image?.link || "/fallback.jpg"}
|
||||
alt={dataBerita.judul}
|
||||
radius="md"
|
||||
fit="cover"
|
||||
height={250}
|
||||
maw={600}
|
||||
loading="lazy"
|
||||
/>
|
||||
</GridCol>
|
||||
<GridCol span={{ md: 6, base: 12 }}>
|
||||
<Box>
|
||||
<Text fz="sm" c="dimmed">{dataBerita.kategoriBerita?.name} • {dayjs(dataBerita.createdAt).fromNow()}</Text>
|
||||
<Title order={1} fw="bold">{dataBerita.judul}</Title>
|
||||
<Text ta={"justify"} mt="xs" fz="md" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: dataBerita.content }} />
|
||||
</Box>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
</Paper>
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => router.push(`/darmasaba/inovasi/layanan-online-desa/informasi-desa/detail-berita/${dataBerita.id}`)}
|
||||
>
|
||||
<Paper shadow="md" radius="lg" p="lg" withBorder>
|
||||
<Grid align="center" gutter="xl">
|
||||
<GridCol span={{ base: 12, md: 6 }}>
|
||||
<Image
|
||||
src={dataBerita.image?.link || '/fallback.jpg'}
|
||||
alt={dataBerita.judul}
|
||||
radius="md"
|
||||
fit="cover"
|
||||
height={280}
|
||||
loading="lazy"
|
||||
style={{ objectPosition: 'center' }}
|
||||
/>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 6 }}>
|
||||
<Stack gap="xs">
|
||||
<Title order={2} fw={800}>
|
||||
{dataBerita.judul}
|
||||
</Title>
|
||||
<Group justify='space-between'>
|
||||
<Badge bg={colors['blue-button']}>
|
||||
{dataBerita.kategoriBerita?.name}
|
||||
</Badge>
|
||||
<Text fz="sm" c="dimmed">
|
||||
{dayjs(dataBerita.createdAt).fromNow()}
|
||||
</Text>
|
||||
</Group>
|
||||
<Text
|
||||
ta="justify"
|
||||
mt="xs"
|
||||
fz="md"
|
||||
lh={1.7}
|
||||
lineClamp={6}
|
||||
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
|
||||
dangerouslySetInnerHTML={{ __html: dataBerita.content }}
|
||||
/>
|
||||
</Stack>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
</Paper>
|
||||
</motion.div>
|
||||
)}
|
||||
<Stack py={10}>
|
||||
<Title order={3}>Berita Terbaru</Title>
|
||||
<Grid>
|
||||
|
||||
{/* === BERITA TERBARU === */}
|
||||
<Stack>
|
||||
<Title order={3} fw={700}>
|
||||
Berita Terbaru
|
||||
</Title>
|
||||
<Grid gutter="xl">
|
||||
{stateBerita.findRecent.data.map((item) => (
|
||||
<GridCol span={{ base: 12, sm: 6, md: 3 }} key={item.id}>
|
||||
<Card shadow="sm" radius="md" withBorder h="100%">
|
||||
<Card.Section>
|
||||
<Image
|
||||
src={item.image?.link || "/placeholder.jpg"}
|
||||
alt={item.judul}
|
||||
height={160} // gambar fix height
|
||||
fit="cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
</Card.Section>
|
||||
<Stack gap="xs" mt="sm">
|
||||
<Text fw={600} lineClamp={2}>
|
||||
{item.judul}
|
||||
</Text>
|
||||
<Text size="sm" color="dimmed" lineClamp={2}>
|
||||
{item.deskripsi}
|
||||
</Text>
|
||||
<Text size="xs" c="gray">
|
||||
{dayjs(item.createdAt).fromNow()}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Card>
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => router.push(`/darmasaba/inovasi/layanan-online-desa/informasi-desa/detail-berita/${item.id}`)}
|
||||
>
|
||||
<Card
|
||||
shadow="sm"
|
||||
radius="md"
|
||||
withBorder
|
||||
h="100%"
|
||||
>
|
||||
<Card.Section>
|
||||
<Image
|
||||
src={item.image?.link || '/placeholder.jpg'}
|
||||
alt={item.judul}
|
||||
height={160}
|
||||
fit="cover"
|
||||
radius="sm"
|
||||
loading="lazy"
|
||||
/>
|
||||
</Card.Section>
|
||||
<Stack gap={4} mt="sm">
|
||||
<Text ta="justify"
|
||||
size="sm"
|
||||
c="dimmed"
|
||||
lineClamp={3}
|
||||
style={{
|
||||
wordBreak: "break-word",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "normal",
|
||||
lineHeight: 1.5,
|
||||
display: "-webkit-box",
|
||||
WebkitLineClamp: 3,
|
||||
WebkitBoxOrient: "vertical",
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }} />
|
||||
<Text size="xs" c="gray">
|
||||
{dayjs(item.createdAt).fromNow()}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</GridCol>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
</Stack>
|
||||
<Divider color={colors['blue-button']} my="md" />
|
||||
<Grid>
|
||||
<GridCol span={{ md: 6, base: 12 }}>
|
||||
|
||||
<Divider color={colors['blue-button']} my="lg" />
|
||||
|
||||
{/* === PENGUMUMAN === */}
|
||||
<Grid gutter="xl" align="stretch">
|
||||
<GridCol span={{ base: 12, md: 6 }}>
|
||||
{dataPengumuman && (
|
||||
<Paper h={"97%"} shadow="md" radius="md" p="md">
|
||||
<Stack gap={"xs"}>
|
||||
<Title order={1} fw="bold">{dataPengumuman.judul}</Title>
|
||||
<Text fz="sm" c="dimmed">{dataPengumuman.CategoryPengumuman?.name} • {dayjs(dataPengumuman.createdAt).fromNow()}</Text>
|
||||
<Box>
|
||||
<Text ta={"justify"} mt="xs" fz="md" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: dataPengumuman.content }} />
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
<motion.div
|
||||
onClick={() => router.push(`/darmasaba/inovasi/layanan-online-desa/informasi-desa/detail-pengumuman/${dataPengumuman.id}`)}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<Paper shadow="md" radius="lg" p="lg" h="100%" withBorder>
|
||||
<Stack gap="xs">
|
||||
<Title order={2} fw={800}>
|
||||
{dataPengumuman.judul}
|
||||
</Title>
|
||||
<Group justify='space-between'>
|
||||
<Badge bg={colors['blue-button']}>
|
||||
{dataPengumuman.CategoryPengumuman?.name}
|
||||
</Badge>
|
||||
<Text fz="sm" c="dimmed">
|
||||
{dayjs(dataPengumuman.createdAt).fromNow()}
|
||||
</Text>
|
||||
</Group>
|
||||
<Text
|
||||
ta="justify"
|
||||
mt="xs"
|
||||
fz="md"
|
||||
lh={1.7}
|
||||
lineClamp={8}
|
||||
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
|
||||
dangerouslySetInnerHTML={{ __html: dataPengumuman.content }}
|
||||
/>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</motion.div>
|
||||
)}
|
||||
</GridCol>
|
||||
<GridCol span={{ md: 6, base: 12 }}>
|
||||
<Stack py={10}>
|
||||
<Title order={3}>Pengumuman Terbaru</Title>
|
||||
<Grid>
|
||||
|
||||
<GridCol span={{ base: 12, md: 6 }}>
|
||||
<Stack>
|
||||
<Title order={3} fw={700}>
|
||||
Pengumuman Terbaru
|
||||
</Title>
|
||||
<Grid gutter="lg">
|
||||
{statePengumuman.findRecent.data.map((item) => (
|
||||
<GridCol span={{ base: 12, sm: 8, md: 6 }} key={item.id}>
|
||||
<Card shadow="sm" radius="md" withBorder h="100%">
|
||||
<Stack gap="xs" mt="sm">
|
||||
<Text fw={600} lineClamp={2}>
|
||||
{item.judul}
|
||||
</Text>
|
||||
<Text size="sm" color="dimmed" lineClamp={2}>
|
||||
{item.deskripsi}
|
||||
</Text>
|
||||
<Text size="xs" c="gray">
|
||||
{dayjs(item.createdAt).fromNow()}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Card>
|
||||
<GridCol span={{ base: 12, sm: 6 }} key={item.id}>
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => router.push(`/darmasaba/inovasi/layanan-online-desa/informasi-desa/detail-pengumuman/${item.id}`)}
|
||||
>
|
||||
<Card
|
||||
shadow="xs"
|
||||
radius="md"
|
||||
withBorder
|
||||
h="100%"
|
||||
p="md"
|
||||
style={{ transition: '0.2s ease' }}
|
||||
className="hover:shadow-md"
|
||||
>
|
||||
<Stack gap="xs">
|
||||
<Text fw={600} lineClamp={2}>
|
||||
{item.judul}
|
||||
</Text>
|
||||
<Text
|
||||
ta="justify"
|
||||
mt="xs"
|
||||
fz="md"
|
||||
lh={1.7}
|
||||
lineClamp={2}
|
||||
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
|
||||
dangerouslySetInnerHTML={{ __html: item.content }}
|
||||
/>
|
||||
<Text size="xs" c="gray">
|
||||
{dayjs(item.createdAt).fromNow()}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</GridCol>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
</Stack>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
|
||||
@@ -100,33 +100,33 @@ function PengaduanMasyarakat() {
|
||||
<Title order={3}>Ajukan Pengaduan Masyarakat</Title>
|
||||
<TextInput
|
||||
label={<Text fz="sm" fw="bold">Nama</Text>}
|
||||
placeholder="masukkan nama"
|
||||
placeholder="Masukkan nama"
|
||||
onChange={(val) => (state.pengaduanMasyarakat.create.form.name = val.target.value)}
|
||||
/>
|
||||
<TextInput
|
||||
label={<Text fz="sm" fw="bold">Email</Text>}
|
||||
placeholder="masukkan email"
|
||||
placeholder="Masukkan email"
|
||||
onChange={(val) => (state.pengaduanMasyarakat.create.form.email = val.target.value)}
|
||||
/>
|
||||
<TextInput
|
||||
type="number"
|
||||
label={<Text fz="sm" fw="bold">Nomor Telepon</Text>}
|
||||
placeholder="masukkan nomor telepon"
|
||||
placeholder="Masukkan nomor telepon"
|
||||
onChange={(val) => (state.pengaduanMasyarakat.create.form.nomorTelepon = val.target.value)}
|
||||
/>
|
||||
<TextInput
|
||||
label={<Text fz="sm" fw="bold">NIK</Text>}
|
||||
placeholder="masukkan nik"
|
||||
placeholder="Masukkan nik"
|
||||
onChange={(val) => (state.pengaduanMasyarakat.create.form.nik = val.target.value)}
|
||||
/>
|
||||
<TextInput
|
||||
label={<Text fz="sm" fw="bold">Judul Pengaduan</Text>}
|
||||
placeholder="masukkan judul pengaduan"
|
||||
placeholder="Masukkan judul pengaduan"
|
||||
onChange={(val) => (state.pengaduanMasyarakat.create.form.judulPengaduan = val.target.value)}
|
||||
/>
|
||||
<TextInput
|
||||
label={<Text fz="sm" fw="bold">Lokasi Kejadian</Text>}
|
||||
placeholder="masukkan lokasi kejadian"
|
||||
placeholder="Masukkan lokasi kejadian"
|
||||
onChange={(val) => (state.pengaduanMasyarakat.create.form.lokasiKejadian = val.target.value)}
|
||||
/>
|
||||
<Box>
|
||||
@@ -144,7 +144,7 @@ function PengaduanMasyarakat() {
|
||||
state.pengaduanMasyarakat.create.form.jenisPengaduanId = val ?? "";
|
||||
}}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Jenis Pengaduan</Text>}
|
||||
placeholder="Pilih kategori produk"
|
||||
placeholder="Pilih jenis pengaduan"
|
||||
data={
|
||||
state.jenisPengaduan.findMany.data?.map((v) => ({
|
||||
value: v.id,
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
'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 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() {
|
||||
const stateProgramKreatif = useProxy(programKreatifState);
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
|
||||
useShallowEffect(() => {
|
||||
@@ -31,14 +31,7 @@ function Page() {
|
||||
<Box px={{ base: 'md', md: 100 }} py="md">
|
||||
{/* Tombol Kembali */}
|
||||
<Box mb="md">
|
||||
<Text
|
||||
c={colors['blue-button']}
|
||||
fw="bold"
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
← Kembali
|
||||
</Text>
|
||||
<BackButton/>
|
||||
</Box>
|
||||
|
||||
{/* Konten Utama */}
|
||||
|
||||
@@ -117,7 +117,7 @@ function Page() {
|
||||
)}
|
||||
</Center>
|
||||
<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>
|
||||
<Button onClick={() => router.push(`/darmasaba/inovasi/program-kreatif-desa/${v.id}`)} bg={colors['blue-button']}>Selengkapnya</Button>
|
||||
</Center>
|
||||
|
||||
@@ -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
|
||||
@@ -1,17 +1,18 @@
|
||||
'use client'
|
||||
import keamananLingkunganState from '@/app/admin/(dashboard)/_state/keamanan/keamanan-lingkungan';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Center, Grid, GridCol, Image, Pagination, Paper, SimpleGrid, Skeleton, Spoiler, Stack, Text, TextInput } from '@mantine/core';
|
||||
import { Box, Button, Center, Grid, GridCol, Image, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
|
||||
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||
import { IconSearch } from '@tabler/icons-react';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
|
||||
function Page() {
|
||||
const state = useProxy(keamananLingkunganState)
|
||||
const [expandedMap, setExpandedMap] = useState<Record<number, boolean>>({});
|
||||
const router = useRouter()
|
||||
const [search, setSearch] = useState('')
|
||||
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
|
||||
const {
|
||||
@@ -26,13 +27,6 @@ function Page() {
|
||||
load(page, 3, debouncedSearch)
|
||||
}, [page, debouncedSearch])
|
||||
|
||||
const toggleExpanded = (index: number, value: boolean) => {
|
||||
setExpandedMap((prev) => ({
|
||||
...prev,
|
||||
[index]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Box py={10}>
|
||||
@@ -65,60 +59,100 @@ function Page() {
|
||||
</GridCol>
|
||||
</Grid>
|
||||
<Text px={{ base: 'md', md: 100 }} ta={"justify"} fz="md" mt={4} >
|
||||
Pecalang dan Patwal (Patroli Pengawal) bertugas memastikan desa tetap aman, tertib, dan kondusif bagi seluruh warga.
|
||||
Pecalang dan Patwal (Patroli Pengawal) bertugas memastikan desa tetap aman, tertib, dan kondusif bagi seluruh warga.
|
||||
</Text>
|
||||
</Box>
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Stack gap={'lg'}>
|
||||
<SimpleGrid
|
||||
pb={10}
|
||||
cols={{
|
||||
base: 1,
|
||||
md: 3,
|
||||
}}>
|
||||
{data.map((v, k) => {
|
||||
return (
|
||||
<Paper radius={10} key={k} bg={colors["white-trans-1"]}>
|
||||
<Stack gap={'xs'}>
|
||||
<Center px={10} py={20}>
|
||||
<Image loading="lazy" src={v.image?.link} alt='' />
|
||||
</Center>
|
||||
<Box px={'lg'}>
|
||||
<Box pb={20}>
|
||||
<Text pb={10} c={colors["blue-button"]} fw={"bold"} fz={"h3"}>
|
||||
{v.name}
|
||||
</Text>
|
||||
<Spoiler
|
||||
showLabel={
|
||||
<Text fw="bold" fz="sm" c={colors['blue-button']}>
|
||||
Show more
|
||||
</Text>
|
||||
}
|
||||
hideLabel={
|
||||
<Text fw="bold" fz="sm" c={colors['blue-button']}>
|
||||
Hide details
|
||||
</Text>
|
||||
}
|
||||
expanded={expandedMap[k] || false}
|
||||
onExpandedChange={(val) => toggleExpanded(k, val)}
|
||||
>
|
||||
<Text pb={10} fz={"h4"} ta={'justify'} style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: v.deskripsi }} />
|
||||
</Spoiler>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
)
|
||||
})}
|
||||
cols={{ base: 1, sm: 2, md: 3 }} spacing="xl" mt="lg">
|
||||
{data.map((v, k) => (
|
||||
<Paper
|
||||
key={k}
|
||||
radius="xl"
|
||||
shadow="md"
|
||||
withBorder
|
||||
p="lg"
|
||||
bg={colors['white-trans-1']}
|
||||
style={{
|
||||
transition: 'all 200ms ease',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<Stack align="center" gap="sm" style={{ flexGrow: 1 }}>
|
||||
<Box
|
||||
style={{
|
||||
width: '100%',
|
||||
aspectRatio: '16/9',
|
||||
borderRadius: '12px',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={v.image?.link}
|
||||
alt={v.name}
|
||||
fit="cover"
|
||||
loading="lazy"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
transition: 'transform 0.4s ease',
|
||||
}}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.05)')}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')}
|
||||
/>
|
||||
</Box>
|
||||
<Text ta="center" fw={700} fz="lg" c={colors['blue-button']}>
|
||||
{v.name}
|
||||
</Text>
|
||||
<Text
|
||||
fz="sm"
|
||||
ta="justify"
|
||||
lineClamp={3}
|
||||
lh={1.6}
|
||||
style={{
|
||||
minHeight: '4.8em',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
|
||||
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
|
||||
/>
|
||||
</Text>
|
||||
</Stack>
|
||||
<Center mt="md">
|
||||
<Button
|
||||
variant="light"
|
||||
onClick={() => {
|
||||
router.push(`/darmasaba/keamanan/keamanan-lingkungan-pecalang-patwal/${v.id}`)
|
||||
}}
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
</Center>
|
||||
</Paper>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Center>
|
||||
|
||||
<Center mt="xl">
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => load(newPage)} // ini penting!
|
||||
onChange={(newPage) => load(newPage, 3, search)}
|
||||
total={totalPages}
|
||||
my="md"
|
||||
size="lg"
|
||||
radius="xl"
|
||||
styles={{
|
||||
control: {
|
||||
border: `1px solid ${colors['blue-button']}`,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Center>
|
||||
</Stack>
|
||||
|
||||
@@ -45,7 +45,7 @@ function Page() {
|
||||
<Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
|
||||
Kontak Darurat
|
||||
</Text>
|
||||
<Text fz={{ base: "h4", md: "h3" }} >
|
||||
<Text fz="md" >
|
||||
Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung.
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
@@ -1,27 +1,26 @@
|
||||
'use client'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Center,
|
||||
Group,
|
||||
Pagination,
|
||||
Paper,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
Tooltip,
|
||||
Button,
|
||||
Paper,
|
||||
Title
|
||||
} from '@mantine/core';
|
||||
import { IconSearch, IconArrowRight } from '@tabler/icons-react';
|
||||
import { IconArrowRight, IconSearch } from '@tabler/icons-react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '@/app/admin/(dashboard)/_com/header';
|
||||
import pencegahanKriminalitasState from '@/app/admin/(dashboard)/_state/keamanan/pencegahan-kriminalitas';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { useState } from 'react';
|
||||
import HeaderSearch from '@/app/admin/(dashboard)/_com/header';
|
||||
import { IconArrowLeft } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
function PencegahanKriminalitas() {
|
||||
const [search, setSearch] = useState("");
|
||||
@@ -96,7 +95,6 @@ function ListPencegahanKriminalitas({ search }: { search: string }) {
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
/>
|
||||
<Group justify="flex-end" mt="sm">
|
||||
<Tooltip label="Lihat detail program" withArrow>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="gradient"
|
||||
@@ -106,7 +104,6 @@ function ListPencegahanKriminalitas({ search }: { search: string }) {
|
||||
>
|
||||
Lihat Detail
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
@@ -9,21 +9,16 @@ import { useProxy } from 'valtio/utils';
|
||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
|
||||
function Page() {
|
||||
const state = useProxy(polsekTerdekatState.findFirst);
|
||||
const router = useRouter()
|
||||
const {
|
||||
data,
|
||||
loading,
|
||||
load,
|
||||
} = state;
|
||||
const router = useRouter();
|
||||
const { data, loading, load } = state;
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, []);
|
||||
|
||||
// kalau masih loading
|
||||
// Loading state
|
||||
if (loading) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
@@ -32,104 +27,175 @@ function Page() {
|
||||
);
|
||||
}
|
||||
|
||||
// kalau data kosong
|
||||
// Data kosong
|
||||
if (!data) {
|
||||
return (
|
||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
<Box pb={10} px={{ base: 20, md: 100 }}>
|
||||
<Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
|
||||
Kantor Polisi Terdekat
|
||||
</Text>
|
||||
<Text pb={15} fz={'md'} >
|
||||
Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung
|
||||
</Text>
|
||||
</Box>
|
||||
<Center py="xl">
|
||||
<Text fz="lg" fw="bold" c="red">
|
||||
Data Polsek tidak ada
|
||||
</Text>
|
||||
</Center>
|
||||
</Stack >
|
||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap={22}>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
<Box pb={10} px={{ base: 20, md: 100 }}>
|
||||
<Text fz={{ base: 'h1', md: '2.5rem' }} c={colors['blue-button']} fw="bold">
|
||||
Kantor Polisi Terdekat
|
||||
</Text>
|
||||
<Text pb={15} fz="md">
|
||||
Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung
|
||||
</Text>
|
||||
</Box>
|
||||
<Center py="xl">
|
||||
<Text fz="lg" fw="bold" c="red">
|
||||
Data Polsek tidak ada
|
||||
</Text>
|
||||
</Center>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap={22}>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
|
||||
<Box pb={10} px={{ base: 20, md: 100 }}>
|
||||
<Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
|
||||
<Text fz={{ base: 'h1', md: '2.5rem' }} c={colors['blue-button']} fw="bold">
|
||||
Kantor Polisi Terdekat
|
||||
</Text>
|
||||
<Text pb={15} fz={'h4'} >
|
||||
<Text pb={15} fz="h4">
|
||||
Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung
|
||||
</Text>
|
||||
</Box>
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Stack gap={'lg'}>
|
||||
<Paper radius={10} bg={colors["white-trans-1"]} p={'xl'}>
|
||||
<Stack gap={'xs'}>
|
||||
<SimpleGrid
|
||||
cols={{
|
||||
base: 1,
|
||||
md: 2,
|
||||
}}
|
||||
>
|
||||
{/* Content Sebelah Kiri */}
|
||||
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Stack gap="lg">
|
||||
<Paper radius={10} bg={colors['white-trans-1']} p="xl">
|
||||
<Stack gap="xs">
|
||||
<SimpleGrid cols={{ base: 1, md: 2 }}>
|
||||
{loading ? (
|
||||
<Center><Skeleton h={400} /></Center>
|
||||
) : data ? (
|
||||
<Center>
|
||||
<Skeleton h={400} />
|
||||
</Center>
|
||||
) : (
|
||||
<>
|
||||
{/* === KIRI === */}
|
||||
<Box>
|
||||
<Text c={colors["blue-button"]} fw={'bold'} fz={'h2'}>{data.nama}</Text>
|
||||
<Text c={colors["blue-button"]} fz={'sm'}>{data.jarakKeDesa}</Text>
|
||||
<Flex py={10} gap={9} align={'center'}>
|
||||
<IconPin size={25} color={colors["blue-button"]} />
|
||||
<Text c={colors["blue-button"]} fz={'lg'}>{data.alamat}</Text>
|
||||
</Flex>
|
||||
<Flex gap={9} align={'center'}>
|
||||
<IconPhone size={25} color={colors["blue-button"]} />
|
||||
<Text c={colors["blue-button"]} fz={'lg'}>{data.nomorTelepon}</Text>
|
||||
</Flex>
|
||||
<Flex py={10} gap={9} align={'center'}>
|
||||
<IconClock size={25} color={colors["blue-button"]} />
|
||||
<Text c={colors["blue-button"]} fz={'lg'}>{data.jamOperasional}</Text>
|
||||
</Flex>
|
||||
<Box>
|
||||
<Text c={colors["blue-button"]} fw={'bold'} fz={'h2'}>Layanan Yang Tersedia :</Text>
|
||||
<SimpleGrid
|
||||
py={10}
|
||||
cols={{
|
||||
base: 1,
|
||||
md: 2,
|
||||
<Text c={colors['blue-button']} fw="bold" fz="h2">
|
||||
{data.nama}
|
||||
</Text>
|
||||
<Text c={colors['blue-button']} fz="sm">
|
||||
{data.jarakKeDesa}
|
||||
</Text>
|
||||
|
||||
{/* Alamat */}
|
||||
<Flex
|
||||
py={10}
|
||||
gap={9}
|
||||
align="flex-start"
|
||||
wrap="wrap"
|
||||
style={{ wordBreak: 'break-word' }}
|
||||
>
|
||||
<Box w={25} mt={3}>
|
||||
<IconPin size={22} />
|
||||
</Box>
|
||||
<Text
|
||||
fz="lg"
|
||||
style={{
|
||||
flex: 1,
|
||||
wordBreak: 'break-word',
|
||||
lineHeight: 1.4,
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Text c={colors["blue-button"]} fz={'lg'}>{data.layananPolsek.nama}</Text>
|
||||
</Box>
|
||||
{data.alamat}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
{/* Telepon */}
|
||||
<Flex
|
||||
gap={9}
|
||||
align="flex-start"
|
||||
wrap="wrap"
|
||||
style={{ wordBreak: 'break-word' }}
|
||||
>
|
||||
<Box w={25} mt={3}>
|
||||
<IconPhone size={22} />
|
||||
</Box>
|
||||
<Text fz="lg">
|
||||
{data.nomorTelepon}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
{/* Jam Operasional */}
|
||||
<Flex
|
||||
py={10}
|
||||
gap={9}
|
||||
align="flex-start"
|
||||
wrap="wrap"
|
||||
style={{ wordBreak: 'break-word' }}
|
||||
>
|
||||
<Box w={25} mt={3}>
|
||||
<IconClock size={22} />
|
||||
</Box>
|
||||
<Text fz="lg">
|
||||
{data.jamOperasional}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
{/* Layanan */}
|
||||
<Box>
|
||||
<Text c={colors['blue-button']} fw="bold" fz="h2">
|
||||
Layanan Yang Tersedia :
|
||||
</Text>
|
||||
<SimpleGrid py={10} cols={{ base: 1, md: 2 }}>
|
||||
<Text fz="lg">
|
||||
{data.layananPolsek.nama}
|
||||
</Text>
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Button bg={colors["blue-button"]} onClick={() => router.push(`/darmasaba/keamanan/polsek-terdekat/semua-polsek`)} rightSection={<IconArrowDown size={20} />}>Lihat Kantor Polisi Lainnya</Button>
|
||||
<Button
|
||||
bg={colors['blue-button']}
|
||||
onClick={() =>
|
||||
router.push(`/darmasaba/keamanan/polsek-terdekat/semua-polsek`)
|
||||
}
|
||||
rightSection={<IconArrowDown size={20} />}
|
||||
>
|
||||
Lihat Kantor Polisi Lainnya
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box pos={'relative'}>
|
||||
|
||||
{/* === KANAN === */}
|
||||
<Box pos="relative">
|
||||
<Box style={{ position: 'absolute', top: 0, right: 0 }}>
|
||||
<Badge size='lg' c={'#287407'} bg={'#A8EDC4'}>Buka</Badge>
|
||||
<Badge size="lg" c="#287407" bg="#A8EDC4">
|
||||
Buka
|
||||
</Badge>
|
||||
</Box>
|
||||
|
||||
<Box pt={40}>
|
||||
<iframe style={{ border: 2, width: "100%" }} src={data.embedMapUrl} width="550" height="300" ></iframe>
|
||||
<iframe
|
||||
style={{ border: 2, width: '100%' }}
|
||||
src={data.embedMapUrl}
|
||||
width="550"
|
||||
height="300"
|
||||
></iframe>
|
||||
</Box>
|
||||
|
||||
<Box pt={20}>
|
||||
<Button onClick={() => router.push(data.linkPetunjukArah)} fullWidth bg={colors["blue-button"]} radius={10} leftSection={<IconNavigation size={20} />}>Petunjuk Arah</Button>
|
||||
<Button
|
||||
onClick={() => router.push(data.linkPetunjukArah)}
|
||||
fullWidth
|
||||
bg={colors['blue-button']}
|
||||
radius={10}
|
||||
leftSection={<IconNavigation size={20} />}
|
||||
>
|
||||
Petunjuk Arah
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
) : null}
|
||||
)}
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
@@ -56,8 +56,11 @@ function Page() {
|
||||
/>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
<Text px={{ base: 'md', md: 100 }} ta={"justify"} fz={{ base: "h4", md: "h3" }} >
|
||||
Keamanan dan ketertiban lingkungan di Desa Darmasaba dijaga melalui peran aktif Pecalang dan Patwal (Patroli Pengawal). Mereka bertugas memastikan desa tetap aman, tertib, dan kondusif bagi seluruh warga.
|
||||
<Text px={{ base: 'md', md: 100 }} ta={"justify"} fz="md" >
|
||||
Keamanan dan ketertiban lingkungan di Desa Darmasaba dijaga melalui peran aktif Pecalang dan Patwal (Patroli Pengawal).
|
||||
</Text>
|
||||
<Text px={{ base: 'md', md: 100 }} ta={"justify"} fz="md" >
|
||||
Mereka bertugas memastikan desa tetap aman, tertib, dan kondusif bagi seluruh warga.
|
||||
</Text>
|
||||
</Box>
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
@@ -82,7 +85,7 @@ function Page() {
|
||||
{v.judul}
|
||||
</Text>
|
||||
<Box>
|
||||
<Text pb={10} fz={"md"} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: v.deskripsi }} />
|
||||
<Text pb={10} fz={"md"} style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: v.deskripsi }} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -54,14 +54,14 @@ function ArtikelKesehatanPage() {
|
||||
<Image style={{ borderTopLeftRadius: '10px', borderTopRightRadius: '10px' }} src={item.image?.link} alt={item.title} height={200} fit="cover" loading="lazy" />
|
||||
</Card.Section>
|
||||
<Stack gap="xs" mt="md">
|
||||
<Text fw="bold" fz="xl" c="dark">{item.title}</Text>
|
||||
<Text fw="bold" fz="xl" c={colors['blue-button']}>{item.title}</Text>
|
||||
<Group gap="xs">
|
||||
<IconCalendar size={16} color={colors['blue-button']} />
|
||||
<IconCalendar size={16} color='gray' />
|
||||
<Text fz="sm" c="dimmed">
|
||||
{new Date(item.createdAt).toLocaleDateString('id-ID', { year: 'numeric', month: 'long', day: 'numeric' })} • Dinas Kesehatan
|
||||
</Text>
|
||||
</Group>
|
||||
<Text fz="md" c="dark" lineClamp={3}>
|
||||
<Text fz="md" lineClamp={3}>
|
||||
{item.content}
|
||||
</Text>
|
||||
<Group justify="flex-start">
|
||||
|
||||
@@ -73,14 +73,14 @@ function FasilitasKesehatanPage() {
|
||||
</Badge>
|
||||
</Group>
|
||||
<Group gap="xs">
|
||||
<IconMapPin size={18} stroke={1.5} color={colors['blue-button']} />
|
||||
<Text fz="sm" c="dimmed">
|
||||
<IconMapPin size={18} stroke={1.5} />
|
||||
<Text fz="sm">
|
||||
{item.informasiumum.alamat}
|
||||
</Text>
|
||||
</Group>
|
||||
<Group gap="xs">
|
||||
<IconClock size={18} stroke={1.5} color={colors['blue-button']} />
|
||||
<Text fz="sm" c="dimmed">
|
||||
<IconClock size={18} stroke={1.5} />
|
||||
<Text fz="sm">
|
||||
{item.informasiumum.jamOperasional}
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
@@ -49,7 +49,7 @@ function JadwalKegiatanPage() {
|
||||
>
|
||||
<Stack gap="sm">
|
||||
<Group justify="space-between">
|
||||
<Text fw={700} fz="xl">
|
||||
<Text fw={700} fz="xl" c={colors['blue-button']}>
|
||||
{item.informasijadwalkegiatan.name}
|
||||
</Text>
|
||||
<Text fw={600} fz="sm" c={colors['blue-button']}>
|
||||
@@ -62,12 +62,12 @@ function JadwalKegiatanPage() {
|
||||
</Group>
|
||||
|
||||
<Group gap="xs">
|
||||
<IconClockHour4 size={18} color={colors['blue-button']} />
|
||||
<IconClockHour4 size={18} />
|
||||
<Text fz="sm">{item.informasijadwalkegiatan.waktu}</Text>
|
||||
</Group>
|
||||
|
||||
<Group gap="xs">
|
||||
<IconMapPin size={18} color={colors['blue-button']} />
|
||||
<IconMapPin size={18} />
|
||||
<Text fz="sm">{item.informasijadwalkegiatan.lokasi}</Text>
|
||||
</Group>
|
||||
|
||||
|
||||
@@ -73,7 +73,6 @@ function DetailInfoWabahPenyakitUser() {
|
||||
<Text fz="lg" fw="bold">Deskripsi</Text>
|
||||
<Text
|
||||
fz="md"
|
||||
c="dimmed"
|
||||
dangerouslySetInnerHTML={{ __html: data.deskripsiLengkap || '-' }}
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
/>
|
||||
|
||||
@@ -53,7 +53,7 @@ function Page() {
|
||||
<Text fz={{ base: '2rem', md: '2.8rem' }} c={colors['blue-button']} fw={800}>
|
||||
Kontak Darurat
|
||||
</Text>
|
||||
<Text c="dimmed" fz="md" mt={4}>
|
||||
<Text fz="md" mt={4}>
|
||||
Hubungi layanan penting dengan cepat dan mudah
|
||||
</Text>
|
||||
</GridCol>
|
||||
@@ -88,59 +88,83 @@ function Page() {
|
||||
) : (
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="xl" mt="lg">
|
||||
{data.map((v, k) => (
|
||||
<Paper
|
||||
key={k}
|
||||
radius="xl"
|
||||
shadow="md"
|
||||
withBorder
|
||||
p="lg"
|
||||
bg={colors['white-trans-1']}
|
||||
style={{
|
||||
transition: 'all 200ms ease',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<Stack align="center" gap="sm">
|
||||
<Box
|
||||
style={{
|
||||
width: '100%',
|
||||
aspectRatio: '16/9',
|
||||
borderRadius: '12px',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={v.image.link}
|
||||
alt={v.name}
|
||||
fit="cover"
|
||||
loading="lazy"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
transition: 'transform 0.4s ease',
|
||||
}}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.05)')}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')}
|
||||
/>
|
||||
</Box>
|
||||
<Paper
|
||||
key={k}
|
||||
radius="xl"
|
||||
shadow="md"
|
||||
withBorder
|
||||
p="lg"
|
||||
bg={colors['white-trans-1']}
|
||||
style={{
|
||||
transition: 'all 200ms ease',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between', // ✅ biar button selalu di bawah
|
||||
height: '100%', // ✅ bikin tinggi seragam
|
||||
}}
|
||||
>
|
||||
<Stack align="center" gap="sm" style={{ flexGrow: 1 }}>
|
||||
<Box
|
||||
style={{
|
||||
width: '100%',
|
||||
aspectRatio: '16/9',
|
||||
borderRadius: '12px',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={v.image.link}
|
||||
alt={v.name}
|
||||
fit="cover"
|
||||
loading="lazy"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
transition: 'transform 0.4s ease',
|
||||
}}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.05)')}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Text ta="center" fw={700} fz="lg" c={colors['blue-button']}>
|
||||
{v.name}
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
fz="sm"
|
||||
ta="center"
|
||||
lineClamp={3}
|
||||
lh={1.6}
|
||||
style={{
|
||||
minHeight: '4.8em', // tinggi tetap 3 baris
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
|
||||
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
|
||||
/>
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
{/* ✅ Tombol selalu di bagian bawah card */}
|
||||
<Center mt="md">
|
||||
<Button
|
||||
variant="light"
|
||||
leftSection={<IconBrandWhatsapp size={18} />}
|
||||
component="a"
|
||||
href={`https://wa.me/${v.whatsapp.replace(/\D/g, '')}`}
|
||||
target="_blank"
|
||||
aria-label="Hubungi WhatsApp"
|
||||
>
|
||||
WhatsApp
|
||||
</Button>
|
||||
</Center>
|
||||
</Paper>
|
||||
|
||||
|
||||
<Text ta="center" fw={700} fz="lg" c={colors['blue-button']}>
|
||||
{v.name}
|
||||
</Text>
|
||||
<Text fz="sm" c="dimmed" ta="center" lineClamp={3}>
|
||||
<span style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: v.deskripsi }} />
|
||||
</Text>
|
||||
<Button
|
||||
variant="light"
|
||||
leftSection={<IconBrandWhatsapp size={18} />}
|
||||
component="a"
|
||||
href={`https://wa.me/${v.whatsapp.replace(/\D/g, '')}`}
|
||||
target="_blank"
|
||||
aria-label="Hubungi WhatsApp"
|
||||
>WhatsApp</Button>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
)}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
'use client';
|
||||
import penangananDarurat from '@/app/admin/(dashboard)/_state/kesehatan/penanganan-darurat/penangananDarurat';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { Box, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import BackButton from '../../../desa/layanan/_com/BackButto';
|
||||
|
||||
function DetailPenangananDaruratUser() {
|
||||
const state = useProxy(penangananDarurat);
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
|
||||
useShallowEffect(() => {
|
||||
@@ -32,14 +31,7 @@ function DetailPenangananDaruratUser() {
|
||||
<Box py={20}>
|
||||
{/* Tombol Back */}
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
leftSection={<IconArrowBack size={22} color={colors['blue-button']} />}
|
||||
mb={20}
|
||||
>
|
||||
Kembali
|
||||
</Button>
|
||||
<BackButton/>
|
||||
</Box>
|
||||
|
||||
{/* Wrapper Detail */}
|
||||
|
||||
@@ -52,7 +52,7 @@ function Page() {
|
||||
<Text fz={{ base: 30, md: 40 }} c={colors['blue-button']} fw={800} lh={1.2}>
|
||||
Penanganan Darurat
|
||||
</Text>
|
||||
<Text fz="md" c="dimmed" mt={4}>
|
||||
<Text fz="md" mt={4}>
|
||||
Informasi cepat dan jelas untuk situasi darurat kesehatan
|
||||
</Text>
|
||||
</GridCol>
|
||||
@@ -105,36 +105,28 @@ function Page() {
|
||||
onMouseLeave={(e) => (e.currentTarget.style.transform = 'translateY(0)')}
|
||||
>
|
||||
<Stack align="center" gap="md">
|
||||
<Center>
|
||||
<Box
|
||||
<Box
|
||||
style={{
|
||||
width: '100%',
|
||||
aspectRatio: '16/9',
|
||||
borderRadius: '12px',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={v.image.link}
|
||||
alt={v.name}
|
||||
fit="cover"
|
||||
loading="lazy"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: 180, // 🔥 tinggi fix biar semua seragam
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
backgroundColor: '#f0f2f5', // fallback kalau gambar loading
|
||||
height: '100%',
|
||||
transition: 'transform 0.4s ease',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={v.image?.link || '/img/default.png'}
|
||||
alt={v.name}
|
||||
fit="cover"
|
||||
width="100%"
|
||||
height="100%"
|
||||
loading="lazy"
|
||||
style={{
|
||||
objectFit: 'cover',
|
||||
objectPosition: 'center',
|
||||
transition: 'transform 0.4s ease',
|
||||
}}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.05)')}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')}
|
||||
/>
|
||||
</Box>
|
||||
/>
|
||||
</Box>
|
||||
|
||||
|
||||
</Center>
|
||||
<Stack gap={4} w="100%">
|
||||
<Text
|
||||
fz="lg"
|
||||
@@ -147,8 +139,7 @@ function Page() {
|
||||
</Text>
|
||||
<Box>
|
||||
<Text
|
||||
fz="sm"
|
||||
c="dimmed"
|
||||
fz="md"
|
||||
lineClamp={3}
|
||||
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
@@ -159,6 +150,8 @@ function Page() {
|
||||
size="md"
|
||||
component="a"
|
||||
href={`/darmasaba/kesehatan/penanganan-darurat/${v.id}`}
|
||||
bg={colors['blue-button']}
|
||||
c="white"
|
||||
>
|
||||
Lihat Detail
|
||||
</Button>
|
||||
|
||||
@@ -28,10 +28,11 @@ function Page() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap="xl">
|
||||
<Stack bg={colors.Bg} py="xl" gap="xl">
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Paper
|
||||
px={{ base: 'md', md: 100 }}
|
||||
py="xl"
|
||||
@@ -70,7 +71,7 @@ function Page() {
|
||||
<Group gap="xl">
|
||||
<Group gap="xs">
|
||||
<Tooltip label="Tanggal dibuat" withArrow>
|
||||
<IconCalendar size={20} stroke={1.5} />
|
||||
<IconCalendar color='gray' size={20} stroke={1.5} />
|
||||
</Tooltip>
|
||||
<Text size="sm" c="dimmed">
|
||||
{state.findUnique.data.createdAt
|
||||
@@ -84,13 +85,14 @@ function Page() {
|
||||
</Group>
|
||||
<Group gap="xs">
|
||||
<Tooltip label="Dibuat oleh" withArrow>
|
||||
<IconUser size={20} stroke={1.5} />
|
||||
<IconUser color='gray' size={20} stroke={1.5} />
|
||||
</Tooltip>
|
||||
<Text size="sm" c="dimmed">Admin Desa</Text>
|
||||
</Group>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
'use client'
|
||||
import programKesehatan from "@/app/admin/(dashboard)/_state/kesehatan/program-kesehatan/programKesehatan";
|
||||
import colors from "@/con/colors";
|
||||
import {
|
||||
Box,
|
||||
@@ -15,9 +16,9 @@ import {
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Tooltip,
|
||||
Transition,
|
||||
Transition
|
||||
} from "@mantine/core";
|
||||
import { useDebouncedValue, useShallowEffect } from "@mantine/hooks";
|
||||
import {
|
||||
IconBarbell,
|
||||
IconCalendar,
|
||||
@@ -26,12 +27,10 @@ import {
|
||||
IconUser,
|
||||
IconUsersGroup,
|
||||
} from "@tabler/icons-react";
|
||||
import BackButton from "../../desa/layanan/_com/BackButto";
|
||||
import { useProxy } from "valtio/utils";
|
||||
import programKesehatan from "@/app/admin/(dashboard)/_state/kesehatan/program-kesehatan/programKesehatan";
|
||||
import { useState } from "react";
|
||||
import { useDebouncedValue, useShallowEffect } from "@mantine/hooks";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { useProxy } from "valtio/utils";
|
||||
import BackButton from "../../desa/layanan/_com/BackButto";
|
||||
|
||||
const manfaatProgram = [
|
||||
{
|
||||
@@ -194,7 +193,6 @@ export default function Page() {
|
||||
<Text size="sm">Admin Desa</Text>
|
||||
</Group>
|
||||
</Group>
|
||||
<Tooltip label="Lihat detail program" withArrow>
|
||||
<Button
|
||||
mt="lg"
|
||||
fullWidth
|
||||
@@ -211,7 +209,6 @@ export default function Page() {
|
||||
>
|
||||
Lihat Detail
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
@@ -93,20 +93,23 @@ function Page() {
|
||||
<Text fw={600} fz="lg" lineClamp={1}>{v.name}</Text>
|
||||
<Badge color="blue" variant="light" radius="sm" fz="xs">Aktif</Badge>
|
||||
</Group>
|
||||
<Stack gap={4}>
|
||||
<Group gap="xs">
|
||||
<IconMapPin size={16} />
|
||||
<Text fz="sm" c="dimmed" lineClamp={2}>{v.alamat}</Text>
|
||||
<Stack gap={6}>
|
||||
<Group gap="xs" align="flex-start" wrap="nowrap">
|
||||
<Box pt={2}><IconMapPin size={16} /></Box>
|
||||
<Text fz="sm" c="dimmed">{v.alamat}</Text>
|
||||
</Group>
|
||||
<Group gap="xs">
|
||||
<IconPhone size={16} />
|
||||
|
||||
<Group gap="xs" align="flex-start" wrap="nowrap">
|
||||
<Box pt={2}><IconPhone size={16} /></Box>
|
||||
<Text fz="sm" c="dimmed">{v.kontak.kontakPuskesmas}</Text>
|
||||
</Group>
|
||||
<Group gap="xs">
|
||||
<IconMail size={16} />
|
||||
|
||||
<Group gap="xs" align="flex-start" wrap="nowrap">
|
||||
<Box pt={2}><IconMail size={16} /></Box>
|
||||
<Text fz="sm" c="dimmed">{v.kontak.email}</Text>
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
<Anchor
|
||||
href={`/darmasaba/kesehatan/puskesmas/${v.id}`}
|
||||
fz="sm"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Create a new component: components/EdukasiCard.tsx
|
||||
'use client';
|
||||
|
||||
import { Box, Paper, Stack, Text, Tooltip } from '@mantine/core';
|
||||
import { Box, Paper, Stack, Text } from '@mantine/core';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
interface EdukasiCardProps {
|
||||
@@ -31,7 +31,6 @@ export function EdukasiCard({ icon, title, description, color = '#1e88e5' }: Edu
|
||||
<Box>
|
||||
<Stack align="center" gap="xs" mb="md">
|
||||
<Box style={{ color }}>{icon}</Box>
|
||||
<Tooltip label={title} maw={250} multiline withArrow position="top">
|
||||
<Text
|
||||
fz={{ base: 'h5', md: 'h4' }}
|
||||
fw={700}
|
||||
@@ -45,10 +44,8 @@ export function EdukasiCard({ icon, title, description, color = '#1e88e5' }: Edu
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
dangerouslySetInnerHTML={{ __html: title }}
|
||||
/>
|
||||
</Stack>
|
||||
<Text
|
||||
size="sm"
|
||||
|
||||
@@ -62,7 +62,6 @@ export default function EdukasiLingkunganPage() {
|
||||
</Text>
|
||||
<Text
|
||||
fz={{ base: 'md', md: 'lg' }}
|
||||
c="dimmed"
|
||||
maw={800}
|
||||
mx="auto"
|
||||
>
|
||||
@@ -78,21 +77,21 @@ export default function EdukasiLingkunganPage() {
|
||||
verticalSpacing={{ base: 'md', md: 'xl' }}
|
||||
>
|
||||
<EdukasiCard
|
||||
icon={<IconLeaf size={32} />}
|
||||
icon={<IconLeaf size={45} />}
|
||||
title={tujuan.data?.judul || ''}
|
||||
description={tujuan.data?.deskripsi || ''}
|
||||
color={colors['blue-button']}
|
||||
/>
|
||||
|
||||
<EdukasiCard
|
||||
icon={<IconRecycle size={32} />}
|
||||
icon={<IconRecycle size={45} />}
|
||||
title={materi.data?.judul || ''}
|
||||
description={materi.data?.deskripsi || ''}
|
||||
color={colors['blue-button']}
|
||||
/>
|
||||
|
||||
<EdukasiCard
|
||||
icon={<IconPlant2 size={32} />}
|
||||
icon={<IconPlant2 size={45} />}
|
||||
title={contoh.data?.judul || ''}
|
||||
description={contoh.data?.deskripsi || ''}
|
||||
color={colors['blue-button']}
|
||||
|
||||
@@ -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 { 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() {
|
||||
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) {
|
||||
if (!data) {
|
||||
return (
|
||||
<Center>
|
||||
<Skeleton height={500} />
|
||||
</Center>
|
||||
<Stack py={20}>
|
||||
<Skeleton height={400} radius="md" />
|
||||
<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 (
|
||||
<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>
|
||||
<Text
|
||||
ta={"center"}
|
||||
fw={"bold"}
|
||||
fz={"1.5rem"}
|
||||
>
|
||||
Informasi Kegiatan Gotong Royong
|
||||
</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} style={{wordBreak: "break-word", whiteSpace: "normal"}} fz={{ base: "sm", md: "lg" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: state.findUnique.data?.deskripsiLengkap || '' }} />
|
||||
<Box py={30}>
|
||||
{/* Header Gambar */}
|
||||
|
||||
{/* Konten */}
|
||||
<Paper
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
maw={900}
|
||||
mx="auto"
|
||||
>
|
||||
<Stack gap="md">
|
||||
{data.image?.link && (
|
||||
<Image
|
||||
src={data.image.link}
|
||||
alt={data.judul || 'Kegiatan Desa'}
|
||||
radius="md"
|
||||
fit="cover"
|
||||
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>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
export default DetailKegiatanDesaUser;
|
||||
|
||||
@@ -44,13 +44,12 @@ function Page() {
|
||||
<Box style={{ display: 'flex', height: '100%' }}>
|
||||
<Paper
|
||||
p="lg"
|
||||
bg="linear-gradient(145deg, #DFE3E8FF 0%, #EFF1F4FF 100%)"
|
||||
style={{
|
||||
borderRadius: 16,
|
||||
boxShadow: '0 0 20px rgba(28, 110, 164, 0.5)',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
flexDirection: 'column',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)'
|
||||
}}
|
||||
>
|
||||
<Stack gap="md" px={20} style={{ height: '100%' }}>
|
||||
@@ -74,13 +73,12 @@ function Page() {
|
||||
<Box style={{ display: 'flex', height: '100%' }}>
|
||||
<Paper
|
||||
p="lg"
|
||||
bg="linear-gradient(145deg, #DFE3E8FF 0%, #EFF1F4FF 100%)"
|
||||
style={{
|
||||
borderRadius: 16,
|
||||
boxShadow: '0 0 20px rgba(28, 110, 164, 0.5)',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
flexDirection: 'column',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)'
|
||||
}}
|
||||
>
|
||||
<Stack gap="md" px={20} style={{ height: '100%' }}>
|
||||
@@ -105,13 +103,12 @@ function Page() {
|
||||
<Box>
|
||||
<Paper
|
||||
p="lg"
|
||||
bg="linear-gradient(145deg, #DFE3E8FF 0%, #EFF1F4FF 100%)"
|
||||
style={{
|
||||
borderRadius: 16,
|
||||
boxShadow: '0 0 20px rgba(28, 110, 164, 0.5)',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
flexDirection: 'column',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)'
|
||||
}}
|
||||
>
|
||||
<Stack gap="md" px={20} style={{ height: '100%' }}>
|
||||
|
||||
@@ -91,7 +91,7 @@ function Page() {
|
||||
<Box style={{ alignContent: 'center', alignItems: 'center' }}>
|
||||
{iconMap[v.icon] ? React.createElement(iconMap[v.icon]) : null}
|
||||
</Box>
|
||||
<Text fw={'bold'} fz={{ base: "lg", md: "xl" }} c={'black'}>{v.name}</Text>
|
||||
<Text fz={{ base: "lg", md: "xl" }} c={'black'}>{v.name}</Text>
|
||||
</Flex>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
248
src/app/darmasaba/(pages)/musik/musik-desa/page.tsx
Normal file
248
src/app/darmasaba/(pages)/musik/musik-desa/page.tsx
Normal file
@@ -0,0 +1,248 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client'
|
||||
import { ActionIcon, Avatar, Badge, Box, Card, Flex, Grid, Group, Paper, Slider, Stack, Text, TextInput } from '@mantine/core';
|
||||
import { IconArrowsShuffle, IconPlayerPauseFilled, IconPlayerPlayFilled, IconPlayerSkipBackFilled, IconPlayerSkipForwardFilled, IconRepeat, IconRepeatOff, IconSearch, IconVolume, IconVolumeOff, IconX } from '@tabler/icons-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||
|
||||
const MusicPlayer = () => {
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [currentTime, setCurrentTime] = useState(0);
|
||||
const [duration, setDuration] = useState(245);
|
||||
const [volume, setVolume] = useState(70);
|
||||
const [isMuted, setIsMuted] = useState(false);
|
||||
const [isRepeat, setIsRepeat] = useState(false);
|
||||
const [isShuffle, setIsShuffle] = useState(false);
|
||||
|
||||
const songs = [
|
||||
{ id: 1, title: 'Midnight Dreams', artist: 'The Wanderers', duration: '4:05', cover: 'https://images.unsplash.com/photo-1470225620780-dba8ba36b745?w=400&h=400&fit=crop' },
|
||||
{ id: 2, title: 'Summer Breeze', artist: 'Coastal Vibes', duration: '3:42', cover: 'https://images.unsplash.com/photo-1493225457124-a3eb161ffa5f?w=400&h=400&fit=crop' },
|
||||
{ id: 3, title: 'City Lights', artist: 'Urban Echo', duration: '4:18', cover: 'https://images.unsplash.com/photo-1514320291840-2e0a9bf2a9ae?w=400&h=400&fit=crop' },
|
||||
{ id: 4, title: 'Ocean Waves', artist: 'Serenity Sound', duration: '5:20', cover: 'https://images.unsplash.com/photo-1459749411175-04bf5292ceea?w=400&h=400&fit=crop' },
|
||||
{ id: 5, title: 'Neon Nights', artist: 'Electric Dreams', duration: '3:55', cover: 'https://images.unsplash.com/photo-1487180144351-b8472da7d491?w=400&h=400&fit=crop' },
|
||||
{ id: 6, title: 'Mountain High', artist: 'Peak Performers', duration: '4:32', cover: 'https://images.unsplash.com/photo-1511671782779-c97d3d27a1d4?w=400&h=400&fit=crop' }
|
||||
];
|
||||
|
||||
const [currentSong, setCurrentSong] = useState(songs[0]);
|
||||
|
||||
useEffect(() => {
|
||||
let interval: any;
|
||||
if (isPlaying) {
|
||||
interval = setInterval(() => {
|
||||
setCurrentTime(prev => {
|
||||
if (prev >= duration) {
|
||||
setIsPlaying(false);
|
||||
return 0;
|
||||
}
|
||||
return prev + 1;
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
return () => clearInterval(interval);
|
||||
}, [isPlaying, duration]);
|
||||
|
||||
const formatTime = (seconds: number) => {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const playSong = (song: any) => {
|
||||
setCurrentSong(song);
|
||||
setCurrentTime(0);
|
||||
setIsPlaying(true);
|
||||
const durationInSeconds = parseInt(song.duration.split(':')[0]) * 60 + parseInt(song.duration.split(':')[1]);
|
||||
setDuration(durationInSeconds);
|
||||
};
|
||||
|
||||
const toggleMute = () => {
|
||||
setIsMuted(!isMuted);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'md', md: 100 }} py="xl">
|
||||
<Paper
|
||||
mx="auto"
|
||||
p="xl"
|
||||
radius="lg"
|
||||
shadow="sm"
|
||||
bg="white"
|
||||
style={{
|
||||
border: '1px solid #eaeaea',
|
||||
}}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<BackButton />
|
||||
<Group justify="space-between" mb="xl" mt={"md"}>
|
||||
<div>
|
||||
<Text size="32px" fw={700} c="#0B4F78">Selamat Datang Kembali</Text>
|
||||
<Text size="md" c="#5A6C7D">Temukan musik favorit Anda hari ini</Text>
|
||||
</div>
|
||||
<Group gap="md">
|
||||
<TextInput
|
||||
placeholder="Cari lagu..."
|
||||
leftSection={<IconSearch size={18} />}
|
||||
radius="xl"
|
||||
w={280}
|
||||
styles={{ input: { backgroundColor: '#fff' } }}
|
||||
/>
|
||||
</Group>
|
||||
</Group>
|
||||
<Stack gap="xl">
|
||||
<div>
|
||||
<Text size="xl" fw={700} c="#0B4F78" mb="md">Sedang Diputar</Text>
|
||||
<Card radius="md" p="xl" shadow="md">
|
||||
<Group align="center" gap="xl">
|
||||
<Avatar src={currentSong.cover} size={180} radius="md" />
|
||||
<Stack gap="md" style={{ flex: 1 }}>
|
||||
<div>
|
||||
<Text size="28px" fw={700} c="#0B4F78">{currentSong.title}</Text>
|
||||
<Text size="lg" c="#5A6C7D">{currentSong.artist}</Text>
|
||||
</div>
|
||||
<Group gap="xs" align="center">
|
||||
<Text size="xs" c="#5A6C7D" w={42}>{formatTime(currentTime)}</Text>
|
||||
<Slider
|
||||
value={currentTime}
|
||||
max={duration}
|
||||
onChange={setCurrentTime}
|
||||
color="#0B4F78"
|
||||
size="sm"
|
||||
style={{ flex: 1 }}
|
||||
styles={{ thumb: { borderWidth: 2 } }}
|
||||
/>
|
||||
<Text size="xs" c="#5A6C7D" w={42}>{formatTime(duration)}</Text>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text size="xl" fw={700} c="#0B4F78" mb="md">Daftar Putar</Text>
|
||||
<Grid gutter="md">
|
||||
{songs.map(song => (
|
||||
<Grid.Col span={{ base: 12, sm: 6, lg: 4 }} key={song.id}>
|
||||
<Card
|
||||
radius="md"
|
||||
p="md"
|
||||
shadow="sm"
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
border: currentSong.id === song.id ? '2px solid #0B4F78' : '2px solid transparent',
|
||||
transition: 'all 0.2s'
|
||||
}}
|
||||
onClick={() => playSong(song)}
|
||||
>
|
||||
<Group gap="md" align="center">
|
||||
<Avatar src={song.cover} size={64} radius="md" />
|
||||
<Stack gap={4} style={{ flex: 1, minWidth: 0 }}>
|
||||
<Text size="sm" fw={600} c="#0B4F78" truncate>{song.title}</Text>
|
||||
<Text size="xs" c="#5A6C7D">{song.artist}</Text>
|
||||
<Text size="xs" c="#8A9BA8">{song.duration}</Text>
|
||||
</Stack>
|
||||
{currentSong.id === song.id && isPlaying && (
|
||||
<Badge color="#0B4F78" variant="filled">Memutar</Badge>
|
||||
)}
|
||||
</Group>
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
))}
|
||||
</Grid>
|
||||
</div>
|
||||
</Stack>
|
||||
|
||||
</Stack>
|
||||
|
||||
</Paper>
|
||||
|
||||
<Paper
|
||||
mt="xl"
|
||||
mx="auto"
|
||||
p="xl"
|
||||
radius="lg"
|
||||
shadow="sm"
|
||||
bg="white"
|
||||
style={{
|
||||
border: '1px solid #eaeaea',
|
||||
}}
|
||||
>
|
||||
<Flex align="center" justify="space-between" gap="xl" h="100%">
|
||||
<Group gap="md" style={{ flex: 1 }}>
|
||||
<Avatar src={currentSong.cover} size={56} radius="md" />
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<Text size="sm" fw={600} c="#0B4F78" truncate>{currentSong.title}</Text>
|
||||
<Text size="xs" c="#5A6C7D">{currentSong.artist}</Text>
|
||||
</div>
|
||||
</Group>
|
||||
|
||||
<Stack gap="xs" style={{ flex: 1 }} align="center">
|
||||
<Group gap="md">
|
||||
<ActionIcon
|
||||
variant={isShuffle ? 'filled' : 'subtle'}
|
||||
color="#0B4F78"
|
||||
onClick={() => setIsShuffle(!isShuffle)}
|
||||
radius="xl"
|
||||
>
|
||||
{isShuffle ? <IconArrowsShuffle size={18} /> : <IconX size={18} />}
|
||||
</ActionIcon>
|
||||
<ActionIcon variant="light" color="#0B4F78" size={40} radius="xl">
|
||||
<IconPlayerSkipBackFilled size={20} />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
variant="filled"
|
||||
color="#0B4F78"
|
||||
size={56}
|
||||
radius="xl"
|
||||
onClick={() => setIsPlaying(!isPlaying)}
|
||||
>
|
||||
{isPlaying ? <IconPlayerPauseFilled size={26} /> : <IconPlayerPlayFilled size={26} />}
|
||||
</ActionIcon>
|
||||
<ActionIcon variant="light" color="#0B4F78" size={40} radius="xl">
|
||||
<IconPlayerSkipForwardFilled size={20} />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
variant={isRepeat ? 'filled' : 'subtle'}
|
||||
color="#0B4F78"
|
||||
onClick={() => setIsRepeat(!isRepeat)}
|
||||
radius="xl"
|
||||
>
|
||||
{isRepeat ? <IconRepeat size={18} /> : <IconRepeatOff size={18} />}
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
<Group gap="xs" style={{ width: '100%', maxWidth: 500 }}>
|
||||
<Text size="xs" c="#5A6C7D" w={40} ta="right">{formatTime(currentTime)}</Text>
|
||||
<Slider
|
||||
value={currentTime}
|
||||
max={duration}
|
||||
onChange={setCurrentTime}
|
||||
color="#0B4F78"
|
||||
size="xs"
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<Text size="xs" c="#5A6C7D" w={40}>{formatTime(duration)}</Text>
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
<Group gap="xs" style={{ flex: 1 }} justify="flex-end">
|
||||
<ActionIcon variant="subtle" color="gray" onClick={toggleMute}>
|
||||
{isMuted || volume === 0 ? <IconVolumeOff size={20} /> : <IconVolume size={20} />}
|
||||
</ActionIcon>
|
||||
<Slider
|
||||
value={isMuted ? 0 : volume}
|
||||
onChange={(val) => {
|
||||
setVolume(val);
|
||||
if (val > 0) setIsMuted(false);
|
||||
}}
|
||||
color="#0B4F78"
|
||||
size="xs"
|
||||
w={100}
|
||||
/>
|
||||
<Text size="xs" c="#5A6C7D" w={32}>{isMuted ? 0 : volume}%</Text>
|
||||
</Group>
|
||||
</Flex>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default MusicPlayer;
|
||||
@@ -1,13 +1,13 @@
|
||||
'use client'
|
||||
import beasiswaDesaState from '@/app/admin/(dashboard)/_state/pendidikan/beasiswa-desa';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Center, Group, Image, Modal, Pagination, Paper, Select, SimpleGrid, Skeleton, Stack, Stepper, StepperStep, Text, TextInput, Title } from '@mantine/core';
|
||||
import { Box, Button, Center, Group, Image, Modal, Pagination, Paper, Select, SimpleGrid, Skeleton, Stack, Stepper, Text, TextInput, Title } from '@mantine/core';
|
||||
import { useDisclosure, useShallowEffect } from '@mantine/hooks';
|
||||
import { IconArrowRight, IconCoin, IconInfoCircle, IconSchool, IconUsers } from '@tabler/icons-react';
|
||||
import { useTransitionRouter } from 'next-view-transitions';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||
import { useTransitionRouter } from 'next-view-transitions';
|
||||
|
||||
const dataBeasiswa = [
|
||||
{ id: 1, nama: 'Penerima Beasiswa', jumlah: '250+', icon: IconUsers },
|
||||
@@ -27,7 +27,7 @@ function Page() {
|
||||
tempatLahir: "",
|
||||
tanggalLahir: "",
|
||||
jenisKelamin: "",
|
||||
kewarganegaraan: "",
|
||||
kewarganegaraan: "WNI",
|
||||
agama: "",
|
||||
alamatKTP: "",
|
||||
alamatDomisili: "",
|
||||
@@ -50,9 +50,21 @@ function Page() {
|
||||
close();
|
||||
};
|
||||
|
||||
const timeline = [
|
||||
{ label: "1 Maret 2025", desc: "Pembukaan Pendaftaran", date: new Date("2025-03-01") },
|
||||
{ label: "15 Maret 2025", desc: "Seleksi Administrasi", date: new Date("2025-03-15") },
|
||||
{ label: "1 April 2025", desc: "Tes Potensi Akademik", date: new Date("2025-04-01") },
|
||||
{ label: "15 April 2025", desc: "Wawancara", date: new Date("2025-04-15") },
|
||||
{ label: "1 Mei 2025", desc: "Pengumuman Hasil", date: new Date("2025-05-01") },
|
||||
];
|
||||
|
||||
const [active, setActive] = useState(1);
|
||||
const nextStep = () => setActive((current) => (current < 5 ? current + 1 : current));
|
||||
const prevStep = () => setActive((current) => (current > 0 ? current - 1 : current));
|
||||
useShallowEffect(() => {
|
||||
const today = new Date();
|
||||
// cari berapa banyak tanggal yang sudah lewat
|
||||
const doneSteps = timeline.filter(item => today >= item.date).length;
|
||||
setActive(doneSteps); // active step diset sesuai tanggal
|
||||
}, []);
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
@@ -115,7 +127,7 @@ function Page() {
|
||||
{data.map((v, k) => (
|
||||
<Paper key={k} p="xl" radius="xl" shadow="sm" bg={colors['white-trans-1']}>
|
||||
<Title order={3} fw={700} c={colors['blue-button']} mb="xs">{v.judul}</Title>
|
||||
<Text fz="sm" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: v.deskripsi }}/>
|
||||
<Text fz="sm" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: v.deskripsi }} />
|
||||
</Paper>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
@@ -139,19 +151,22 @@ function Page() {
|
||||
Timeline Pendaftaran
|
||||
</Title>
|
||||
<Center>
|
||||
<Stepper mt={20} active={active} onStepClick={setActive} orientation="vertical" allowNextStepsSelect={false}>
|
||||
<StepperStep label="1 Maret 2025" description="Pembukaan Pendaftaran" />
|
||||
<StepperStep label="15 Maret 2025" description="Seleksi Administrasi" />
|
||||
<StepperStep label="1 April 2025" description="Tes Potensi Akademik" />
|
||||
<StepperStep label="15 April 2025" description="Wawancara" />
|
||||
<StepperStep label="1 Mei 2025" description="Pengumuman Hasil" />
|
||||
<Stepper
|
||||
mt={20}
|
||||
active={active}
|
||||
onStepClick={setActive}
|
||||
orientation="vertical"
|
||||
allowNextStepsSelect={false}
|
||||
>
|
||||
{timeline.map((item, index) => (
|
||||
<Stepper.Step
|
||||
key={index}
|
||||
label={item.label}
|
||||
description={item.desc}
|
||||
/>
|
||||
))}
|
||||
</Stepper>
|
||||
</Center>
|
||||
|
||||
<Group justify="center" mt="xl">
|
||||
<Button variant="default" radius="xl" onClick={prevStep}>Kembali</Button>
|
||||
<Button radius="xl" bg={colors['blue-button']} onClick={nextStep}>Lanjut</Button>
|
||||
</Group>
|
||||
</Box>
|
||||
|
||||
<Modal
|
||||
@@ -194,7 +209,11 @@ function Page() {
|
||||
<TextInput
|
||||
label="Kewarganegaraan"
|
||||
placeholder="Masukkan kewarganegaraan"
|
||||
onChange={(val) => { beasiswaDesa.create.form.kewarganegaraan = val.target.value }} />
|
||||
value={beasiswaDesa.create.form.kewarganegaraan || "WNI"} // tampilkan WNI kalau kosong
|
||||
onChange={(e) => {
|
||||
beasiswaDesa.create.form.kewarganegaraan = e.target.value;
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
label="Agama"
|
||||
placeholder="Pilih agama"
|
||||
|
||||
@@ -34,7 +34,7 @@ export default function BeasiswaPage() {
|
||||
tempatLahir: "",
|
||||
tanggalLahir: "",
|
||||
jenisKelamin: "",
|
||||
kewarganegaraan: "",
|
||||
kewarganegaraan: "WNI",
|
||||
agama: "",
|
||||
alamatKTP: "",
|
||||
alamatDomisili: "",
|
||||
@@ -71,7 +71,7 @@ export default function BeasiswaPage() {
|
||||
<Stack gap="md" maw={600}>
|
||||
<Group>
|
||||
<IconSchool size={30} color={colors["blue-button"]} />
|
||||
<Title order={2}>Program Beasiswa Pendidikan Desa Darmasaba</Title>
|
||||
<Title order={2} c={colors["blue-button"]}>Program Beasiswa Pendidikan Desa Darmasaba</Title>
|
||||
</Group>
|
||||
<Text>
|
||||
Program ini bertujuan untuk mendukung pendidikan generasi muda di Desa Darmasaba
|
||||
@@ -84,7 +84,7 @@ export default function BeasiswaPage() {
|
||||
<Container size="lg" py="xl">
|
||||
<Group mb="sm">
|
||||
<IconInfoCircle size={24} color={colors["blue-button"]} />
|
||||
<Title order={3}>Tentang Program</Title>
|
||||
<Title order={3} c={colors["blue-button"]}>Tentang Program</Title>
|
||||
</Group>
|
||||
<Text>
|
||||
Program Beasiswa Desa Darmasaba adalah inisiatif pemerintah desa untuk meningkatkan akses
|
||||
@@ -94,12 +94,11 @@ export default function BeasiswaPage() {
|
||||
|
||||
{/* Tambahkan info tahun berjalan di sini */}
|
||||
<Paper mt="md" p="md" radius="lg" shadow="xs" bg="#f8fbff" withBorder>
|
||||
<Text fw={500} c={colors["blue-button"]}>
|
||||
📅 Periode Beasiswa Tahun 2025
|
||||
<Text fw={500}>
|
||||
Periode Beasiswa Tahun 2025
|
||||
</Text>
|
||||
<Text fz="sm" c="dimmed">
|
||||
Pendaftaran beasiswa dibuka mulai <strong>1 Januari 2025</strong> dan ditutup pada
|
||||
<strong>31 Mei 2025</strong>.
|
||||
Pendaftaran beasiswa dibuka mulai <strong>1 Januari 2025</strong> dan ditutup pada <strong>31 Mei 2025</strong>.
|
||||
Pengumuman hasil seleksi akan diumumkan pada pertengahan Juni 2025 melalui website resmi Desa Darmasaba.
|
||||
</Text>
|
||||
</Paper>
|
||||
@@ -109,7 +108,7 @@ export default function BeasiswaPage() {
|
||||
<Container size="lg" py="xl">
|
||||
<Group mb="sm">
|
||||
<IconChecklist size={24} color={colors["blue-button"]} />
|
||||
<Title order={3}>Syarat Pendaftaran</Title>
|
||||
<Title order={3} c={colors["blue-button"]}>Syarat Pendaftaran</Title>
|
||||
</Group>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="lg">
|
||||
@@ -140,7 +139,7 @@ export default function BeasiswaPage() {
|
||||
<Container size="lg" py="xl">
|
||||
<Group mb="sm">
|
||||
<IconTimeline size={24} color={colors["blue-button"]} />
|
||||
<Title order={3}>Proses Seleksi</Title>
|
||||
<Title order={3} c={colors["blue-button"]}>Proses Seleksi</Title>
|
||||
</Group>
|
||||
|
||||
<Timeline active={4} bulletSize={24} lineWidth={2}>
|
||||
@@ -148,8 +147,8 @@ export default function BeasiswaPage() {
|
||||
<Text c="dimmed" size="sm">
|
||||
Calon peserta mengisi formulir pendaftaran dan mengunggah dokumen pendukung.
|
||||
</Text>
|
||||
<Text size="sm" fw={500} c={colors["blue-button"]} mt={4}>
|
||||
⏰ Estimasi waktu: 1 Februari – 31 Mei 2025
|
||||
<Text size="sm" fw={500} mt={4}>
|
||||
Estimasi waktu: 1 Februari – 31 Mei 2025
|
||||
</Text>
|
||||
</Timeline.Item>
|
||||
|
||||
@@ -157,8 +156,8 @@ export default function BeasiswaPage() {
|
||||
<Text c="dimmed" size="sm">
|
||||
Panitia memverifikasi kelengkapan dan validitas berkas.
|
||||
</Text>
|
||||
<Text size="sm" fw={500} c={colors["blue-button"]} mt={4}>
|
||||
⏰ Estimasi waktu: 5–7 hari kerja setelah penutupan pendaftaran
|
||||
<Text size="sm" fw={500} mt={4}>
|
||||
Estimasi waktu: 5–7 hari kerja setelah penutupan pendaftaran
|
||||
</Text>
|
||||
</Timeline.Item>
|
||||
|
||||
@@ -166,8 +165,8 @@ export default function BeasiswaPage() {
|
||||
<Text c="dimmed" size="sm">
|
||||
Peserta yang lolos administrasi akan diundang untuk wawancara langsung dengan tim seleksi.
|
||||
</Text>
|
||||
<Text size="sm" fw={500} c={colors["blue-button"]} mt={4}>
|
||||
⏰ Estimasi waktu: 7–10 hari kerja setelah pengumuman seleksi administrasi
|
||||
<Text size="sm" fw={500} mt={4}>
|
||||
Estimasi waktu: 7–10 hari kerja setelah pengumuman seleksi administrasi
|
||||
</Text>
|
||||
</Timeline.Item>
|
||||
|
||||
@@ -175,14 +174,14 @@ export default function BeasiswaPage() {
|
||||
<Text c="dimmed" size="sm">
|
||||
Daftar penerima beasiswa diumumkan melalui website resmi Desa Darmasaba.
|
||||
</Text>
|
||||
<Text size="sm" fw={500} c={colors["blue-button"]} mt={4}>
|
||||
⏰ Estimasi waktu: 5 hari kerja setelah tahap wawancara selesai
|
||||
<Text size="sm" fw={500} mt={4}>
|
||||
Estimasi waktu: 5 hari kerja setelah tahap wawancara selesai
|
||||
</Text>
|
||||
</Timeline.Item>
|
||||
</Timeline>
|
||||
|
||||
<Text c="dimmed" size="sm" mt="lg" ta="center">
|
||||
🗓️ Total estimasi keseluruhan proses: sekitar 3–4 minggu setelah penutupan pendaftaran
|
||||
Total estimasi keseluruhan proses: sekitar 3–4 minggu setelah penutupan pendaftaran
|
||||
</Text>
|
||||
</Container>
|
||||
|
||||
@@ -191,7 +190,7 @@ export default function BeasiswaPage() {
|
||||
<Container size="lg" py="xl">
|
||||
<Group mb="sm">
|
||||
<IconQuote size={24} color={colors["blue-button"]} />
|
||||
<Title order={3}>Cerita Sukses Penerima Beasiswa</Title>
|
||||
<Title order={3} c={colors["blue-button"]}>Cerita Sukses Penerima Beasiswa</Title>
|
||||
</Group>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="lg">
|
||||
@@ -219,12 +218,12 @@ export default function BeasiswaPage() {
|
||||
<Container size="lg" py="xl" ta="center">
|
||||
<Group justify="center" mb="sm">
|
||||
<IconUserPlus size={28} color={colors["blue-button"]} />
|
||||
<Title order={3}>Siap Bergabung dengan Program Ini?</Title>
|
||||
<Title order={3} c={colors["blue-button"]}>Siap Bergabung dengan Program Ini?</Title>
|
||||
</Group>
|
||||
<Text c="dimmed" mb="md">
|
||||
Segera daftar dan wujudkan mimpimu bersama Desa Darmasaba.
|
||||
</Text>
|
||||
<Button onClick={open} size="lg" radius="xl" color="blue">
|
||||
<Button onClick={open} size="lg" radius="xl" bg={colors["blue-button"]}>
|
||||
Daftar Sekarang
|
||||
</Button>
|
||||
</Container>
|
||||
@@ -269,7 +268,11 @@ export default function BeasiswaPage() {
|
||||
<TextInput
|
||||
label="Kewarganegaraan"
|
||||
placeholder="Masukkan kewarganegaraan"
|
||||
onChange={(val) => { beasiswaDesa.create.form.kewarganegaraan = val.target.value }} />
|
||||
value={beasiswaDesa.create.form.kewarganegaraan || "WNI"} // tampilkan WNI kalau kosong
|
||||
onChange={(e) => {
|
||||
beasiswaDesa.create.form.kewarganegaraan = e.target.value;
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
label="Agama"
|
||||
placeholder="Pilih agama"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
'use client'
|
||||
import stateBimbinganBelajarDesa from '@/app/admin/(dashboard)/_state/pendidikan/bimbingan-belajar-desa';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Paper, SimpleGrid, Skeleton, Stack, Text, Title, Tooltip, Divider, Badge, Group } from '@mantine/core';
|
||||
import { Box, Divider, Group, Paper, SimpleGrid, Skeleton, Stack, Text, Title, Tooltip } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconBook2, IconCalendarTime, IconMapPin } from '@tabler/icons-react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import { IconMapPin, IconCalendarTime, IconBook2 } from '@tabler/icons-react';
|
||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||
|
||||
function Page() {
|
||||
@@ -55,9 +55,9 @@ function Page() {
|
||||
<IconBook2 size={36} stroke={1.5} color={colors['blue-button']} />
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
|
||||
<Title order={3} fw={700} c={colors['blue-button']} mb="xs">
|
||||
{stateTujuanProgram.findById.data?.judul}
|
||||
</Badge>
|
||||
</Title>
|
||||
</Group>
|
||||
<Text fz="md" style={{wordBreak: "break-word", whiteSpace: "normal"}} lh={1.6} dangerouslySetInnerHTML={{ __html: stateTujuanProgram.findById.data?.deskripsi }} />
|
||||
</Stack>
|
||||
@@ -70,9 +70,9 @@ function Page() {
|
||||
<IconMapPin size={36} stroke={1.5} color={colors['blue-button']} />
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
|
||||
<Title order={3} fw={700} c={colors['blue-button']} mb="xs">
|
||||
{stateLokasiDanJadwal.findById.data?.judul}
|
||||
</Badge>
|
||||
</Title>
|
||||
</Group>
|
||||
<Text fz="md" lh={1.6} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: stateLokasiDanJadwal.findById.data?.deskripsi }} />
|
||||
</Stack>
|
||||
@@ -85,9 +85,9 @@ function Page() {
|
||||
<IconCalendarTime size={36} stroke={1.5} color={colors['blue-button']} />
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
|
||||
<Title order={3} fw={700} c={colors['blue-button']} mb="xs">
|
||||
{stateFasilitas.findById.data?.judul}
|
||||
</Badge>
|
||||
</Title>
|
||||
</Group>
|
||||
<Text fz="md" lh={1.6} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: stateFasilitas.findById.data?.deskripsi }} />
|
||||
</Stack>
|
||||
|
||||
@@ -57,7 +57,7 @@ function Page() {
|
||||
<Title order={1} fw={700} ta="center" c={colors['blue-button']}>
|
||||
Statistik Data Pendidikan
|
||||
</Title>
|
||||
<Text c="dimmed" size="sm" ta="center">
|
||||
<Text fz="md" ta="center">
|
||||
Visualisasi jumlah pendidikan berdasarkan kategori yang tersedia
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client';
|
||||
import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Center, Container, Group, Paper, SimpleGrid, Skeleton, Stack, Text, Tooltip, ActionIcon } from '@mantine/core';
|
||||
import { IconChalkboard, IconMicroscope, IconProps, IconRefresh, IconSchool, IconInfoCircle } from '@tabler/icons-react';
|
||||
import { motion } from 'framer-motion';
|
||||
@@ -119,11 +120,15 @@ export default function KategoriPage({ jenjangPendidikan }: { jenjangPendidikan:
|
||||
</Text>
|
||||
</Box>
|
||||
<Button
|
||||
leftSection={<IconRefresh size={16} />}
|
||||
leftSection={<IconRefresh color={colors['blue-button']} size={16} />}
|
||||
variant="outline"
|
||||
size="xs"
|
||||
onClick={handleRefresh}
|
||||
loading={stats.some(stat => stat.loading)}
|
||||
c={colors['blue-button']}
|
||||
style={{
|
||||
borderColor: colors['blue-button'],
|
||||
}}
|
||||
>
|
||||
Segarkan Data
|
||||
</Button>
|
||||
@@ -143,7 +148,7 @@ export default function KategoriPage({ jenjangPendidikan }: { jenjangPendidikan:
|
||||
aria-label="Tidak ada hasil"
|
||||
>
|
||||
<Center style={{ minHeight: 180, flexDirection: 'column' }}>
|
||||
<Text fz="lg" fw={800} c="#2563eb">
|
||||
<Text fz="lg" fw={800} c={colors['blue-button']}>
|
||||
Tidak ditemukan
|
||||
</Text>
|
||||
<Text c="dimmed" mt="6px">
|
||||
@@ -173,81 +178,81 @@ export default function KategoriPage({ jenjangPendidikan }: { jenjangPendidikan:
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<Skeleton visible={v.loading}>
|
||||
<Paper
|
||||
p="lg"
|
||||
radius="lg"
|
||||
style={{
|
||||
background: 'white',
|
||||
border: '1px solid #e2e8f0',
|
||||
boxShadow: '0 8px 28px rgba(0,0,0,0.06)',
|
||||
minHeight: 260,
|
||||
}}
|
||||
role="article"
|
||||
aria-label={`${v.nama} kartu statistik`}
|
||||
>
|
||||
<Stack gap="sm" mb="md">
|
||||
<Center>
|
||||
<Box
|
||||
style={{
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 16,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: '#eff6ff',
|
||||
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.6)',
|
||||
}}
|
||||
aria-hidden
|
||||
>
|
||||
{React.createElement(v.icon, {
|
||||
color: '#2563eb',
|
||||
size: 34,
|
||||
stroke: 1.6,
|
||||
})}
|
||||
</Box>
|
||||
</Center>
|
||||
<Paper
|
||||
p="lg"
|
||||
radius="lg"
|
||||
style={{
|
||||
background: 'white',
|
||||
border: '1px solid #e2e8f0',
|
||||
boxShadow: '0 8px 28px rgba(0,0,0,0.06)',
|
||||
minHeight: 260,
|
||||
}}
|
||||
role="article"
|
||||
aria-label={`${v.nama} kartu statistik`}
|
||||
>
|
||||
<Stack gap="sm" mb="md">
|
||||
<Center>
|
||||
<Box
|
||||
style={{
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 16,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: '#eff6ff',
|
||||
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.6)',
|
||||
}}
|
||||
aria-hidden
|
||||
>
|
||||
{React.createElement(v.icon, {
|
||||
color: colors['blue-button'],
|
||||
size: 34,
|
||||
stroke: 1.6,
|
||||
})}
|
||||
</Box>
|
||||
</Center>
|
||||
|
||||
<Group justify="center" align="center" gap="xs">
|
||||
<Stack gap={0}>
|
||||
<Text ta={"center"} fz={{ base: 18, md: 22 }} fw={800} c="#0f172a">
|
||||
{v.jumlah.toLocaleString()}
|
||||
</Text>
|
||||
<Group gap={6} align="center">
|
||||
<Text ta={"center"} fz="sm" fw={700} c="#2563eb">
|
||||
{v.nama}
|
||||
<Group justify="center" align="center" gap="xs">
|
||||
<Stack gap={0}>
|
||||
<Text ta={"center"} fz={{ base: 18, md: 22 }} fw={800} c="#0f172a">
|
||||
{v.jumlah.toLocaleString()}
|
||||
</Text>
|
||||
<Tooltip label={v.helper ?? ''} position="right" withArrow>
|
||||
<ActionIcon aria-label={`Info ${v.nama}`} variant="transparent" size="xs">
|
||||
<IconInfoCircle size={16} style={{ color: '#2563eb' }} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Stack>
|
||||
<Group gap={6} align="center">
|
||||
<Text ta={"center"} fz="sm" fw={700} c={colors['blue-button']}>
|
||||
{v.nama}
|
||||
</Text>
|
||||
<Tooltip label={v.helper ?? ''} position="right" withArrow>
|
||||
<ActionIcon aria-label={`Info ${v.nama}`} variant="transparent" size="xs">
|
||||
<IconInfoCircle size={16} style={{ color: colors['blue-button'] }} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
<Group justify="center" mt="8px">
|
||||
<Button
|
||||
radius="xl"
|
||||
variant="outline"
|
||||
aria-label={`Lihat detail ${v.nama}`}
|
||||
style={{
|
||||
borderColor: '#e2e8f0',
|
||||
color: '#2563eb',
|
||||
paddingLeft: 20,
|
||||
paddingRight: 20,
|
||||
}}
|
||||
onClick={() => {
|
||||
if (v.nama === "Lembaga Pendidikan") router.push(`/darmasaba/pendidikan/info-sekolah/${jenjangPendidikan}/lembaga`);
|
||||
if (v.nama === "Siswa Terdaftar") router.push(`/darmasaba/pendidikan/info-sekolah/${jenjangPendidikan}/siswa`);
|
||||
if (v.nama === "Tenaga Pengajar") router.push(`/darmasaba/pendidikan/info-sekolah/${jenjangPendidikan}/pengajar`);
|
||||
}}
|
||||
>
|
||||
Lihat Detail
|
||||
</Button>
|
||||
</Group>
|
||||
</Paper>
|
||||
<Group justify="center" mt="8px">
|
||||
<Button
|
||||
radius="xl"
|
||||
variant="outline"
|
||||
aria-label={`Lihat detail ${v.nama}`}
|
||||
style={{
|
||||
borderColor: colors['blue-button'],
|
||||
color: colors['blue-button'],
|
||||
paddingLeft: 20,
|
||||
paddingRight: 20,
|
||||
}}
|
||||
onClick={() => {
|
||||
if (v.nama === "Lembaga Pendidikan") router.push(`/darmasaba/pendidikan/info-sekolah/${jenjangPendidikan}/lembaga`);
|
||||
if (v.nama === "Siswa Terdaftar") router.push(`/darmasaba/pendidikan/info-sekolah/${jenjangPendidikan}/siswa`);
|
||||
if (v.nama === "Tenaga Pengajar") router.push(`/darmasaba/pendidikan/info-sekolah/${jenjangPendidikan}/pengajar`);
|
||||
}}
|
||||
>
|
||||
Lihat Detail
|
||||
</Button>
|
||||
</Group>
|
||||
</Paper>
|
||||
</Skeleton>
|
||||
</motion.div>
|
||||
))
|
||||
|
||||
@@ -55,7 +55,7 @@ function Page({ params }: PageProps) {
|
||||
<Group justify="space-between" align="center" mb="md">
|
||||
<Group gap="sm">
|
||||
<IconChalkboard size={28} stroke={1.5} color={colors['blue-button']} />
|
||||
<Title order={2} fz="xl">Daftar Lembaga Pendidikan</Title>
|
||||
<Title order={2} fz="xl" c={colors['blue-button']}>Daftar Lembaga Pendidikan</Title>
|
||||
</Group>
|
||||
<TextInput
|
||||
placeholder='pencarian'
|
||||
|
||||
@@ -55,7 +55,7 @@ function Page({ params }: PageProps) {
|
||||
<Group justify="space-between" align="center" mb="md">
|
||||
<Group gap="sm">
|
||||
<IconMicroscope size={28} stroke={1.5} color={colors['blue-button']} />
|
||||
<Title order={2} fz="xl">Daftar Pengajar</Title>
|
||||
<Title order={2} fz="xl" c={colors['blue-button']}>Daftar Pengajar</Title>
|
||||
</Group>
|
||||
<TextInput
|
||||
placeholder='pencarian'
|
||||
|
||||
@@ -55,7 +55,7 @@ function Page({ params }: PageProps) {
|
||||
<Group justify="space-between" align="center" mb="md">
|
||||
<Group gap="sm">
|
||||
<IconSchool size={28} stroke={1.5} color={colors['blue-button']} />
|
||||
<Title order={2} fz="xl">Daftar Siswa</Title>
|
||||
<Title order={2} fz="xl" c={colors['blue-button']}>Daftar Siswa</Title>
|
||||
</Group>
|
||||
<TextInput
|
||||
placeholder='pencarian'
|
||||
|
||||
@@ -96,23 +96,21 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
// pastikan path benar
|
||||
import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud';
|
||||
import {
|
||||
ActionIcon,
|
||||
Box,
|
||||
Button,
|
||||
Container,
|
||||
Group,
|
||||
Loader,
|
||||
Paper,
|
||||
Stack,
|
||||
Text,
|
||||
VisuallyHidden,
|
||||
Loader,
|
||||
Text
|
||||
} from '@mantine/core';
|
||||
import { IconArrowLeft } from '@tabler/icons-react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useSnapshot } from 'valtio';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud';
|
||||
import { useSnapshot } from 'valtio';
|
||||
import BackButton from '../../../desa/layanan/_com/BackButto';
|
||||
|
||||
type LayoutSekolahProps = {
|
||||
title?: string;
|
||||
@@ -153,10 +151,7 @@ export default function LayoutSekolah({
|
||||
<Container size="xl" py={{ base: 'md', md: 'xl' }}>
|
||||
<Stack gap="lg">
|
||||
{/* Back Button */}
|
||||
<ActionIcon onClick={() => window.history.back()} variant="light" radius="md" size="lg">
|
||||
<IconArrowLeft size={20} />
|
||||
<VisuallyHidden>Kembali</VisuallyHidden>
|
||||
</ActionIcon>
|
||||
<BackButton/>
|
||||
|
||||
{/* Search & Filter */}
|
||||
<Paper radius="lg" p="xl" withBorder>
|
||||
@@ -185,6 +180,8 @@ export default function LayoutSekolah({
|
||||
radius="xl"
|
||||
size="sm"
|
||||
variant={aktif ? 'filled' : 'light'}
|
||||
bg={aktif? colors['blue-button'] : '#BDCADE'}
|
||||
c={aktif ? colors['white-1'] : colors['blue-button']}
|
||||
>
|
||||
{k}
|
||||
</Button>
|
||||
|
||||
@@ -47,7 +47,7 @@ function Page() {
|
||||
<Group justify="space-between" align="center" mb="md">
|
||||
<Group gap="sm">
|
||||
<IconChalkboard size={28} stroke={1.5} color={colors['blue-button']} />
|
||||
<Title order={2} fz="xl">Daftar Lembaga Pendidikan</Title>
|
||||
<Title order={2} fz="xl" c={colors['blue-button']}>Daftar Lembaga Pendidikan</Title>
|
||||
</Group>
|
||||
<TextInput
|
||||
placeholder='pencarian'
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud';
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
ActionIcon,
|
||||
Box,
|
||||
@@ -130,11 +131,15 @@ export default function SekolahPage() {
|
||||
<Box>
|
||||
<Group justify="start" mb="md">
|
||||
<Button
|
||||
leftSection={<IconRefresh size={16} />}
|
||||
leftSection={<IconRefresh color={colors['blue-button']} size={16} />}
|
||||
variant="outline"
|
||||
size="xs"
|
||||
onClick={handleRefresh}
|
||||
loading={stats.some(stat => stat.loading)}
|
||||
c={colors['blue-button']}
|
||||
style={{
|
||||
borderColor: colors['blue-button'],
|
||||
}}
|
||||
>
|
||||
Segarkan Data
|
||||
</Button>
|
||||
@@ -154,7 +159,7 @@ export default function SekolahPage() {
|
||||
aria-label="Tidak ada hasil"
|
||||
>
|
||||
<Center style={{ minHeight: 180, flexDirection: 'column' }}>
|
||||
<Text fz="lg" fw={800} c="#2563eb">
|
||||
<Text fz="lg" fw={800} c={colors['blue-button']}>
|
||||
Tidak ditemukan
|
||||
</Text>
|
||||
<Text c="dimmed" mt="6px">
|
||||
@@ -212,7 +217,7 @@ export default function SekolahPage() {
|
||||
aria-hidden
|
||||
>
|
||||
{React.createElement(v.icon, {
|
||||
color: '#2563eb',
|
||||
color: colors['blue-button'],
|
||||
size: 34,
|
||||
stroke: 1.6,
|
||||
})}
|
||||
@@ -225,12 +230,12 @@ export default function SekolahPage() {
|
||||
{v.jumlah.toLocaleString()}
|
||||
</Text>
|
||||
<Group gap={6} align="center">
|
||||
<Text ta={"center"} fz="sm" fw={700} c="#2563eb">
|
||||
<Text ta={"center"} fz="sm" fw={700} c={colors['blue-button']}>
|
||||
{v.nama}
|
||||
</Text>
|
||||
<Tooltip label={v.helper ?? ''} position="right" withArrow>
|
||||
<ActionIcon aria-label={`Info ${v.nama}`} variant="transparent" size="xs">
|
||||
<IconInfoCircle size={16} style={{ color: '#2563eb' }} />
|
||||
<IconInfoCircle size={16} style={{ color: colors['blue-button'] }} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
@@ -244,8 +249,8 @@ export default function SekolahPage() {
|
||||
variant="outline"
|
||||
aria-label={`Lihat detail ${v.nama}`}
|
||||
style={{
|
||||
borderColor: '#e2e8f0',
|
||||
color: '#2563eb',
|
||||
borderColor: colors['blue-button'],
|
||||
color: colors['blue-button'],
|
||||
paddingLeft: 20,
|
||||
paddingRight: 20,
|
||||
}}
|
||||
|
||||
@@ -46,7 +46,7 @@ function Page() {
|
||||
<Group justify="space-between" align="center" mb="md">
|
||||
<Group gap="sm">
|
||||
<IconMicroscope size={28} stroke={1.5} color={colors['blue-button']} />
|
||||
<Title order={2} fz="xl">Daftar Pengajar</Title>
|
||||
<Title order={2} fz="xl" c={colors['blue-button']}>Daftar Pengajar</Title>
|
||||
</Group>
|
||||
<TextInput
|
||||
placeholder='pencarian'
|
||||
|
||||
@@ -47,7 +47,7 @@ function Page() {
|
||||
<Group justify="space-between" align="center" mb="md">
|
||||
<Group gap="sm">
|
||||
<IconSchool size={28} stroke={1.5} color={colors['blue-button']} />
|
||||
<Title order={2} fz="xl">Daftar Siswa</Title>
|
||||
<Title order={2} fz="xl" c={colors['blue-button']}>Daftar Siswa</Title>
|
||||
</Group>
|
||||
<TextInput
|
||||
placeholder='pencarian'
|
||||
|
||||
@@ -37,13 +37,15 @@ function Page() {
|
||||
|
||||
<Box px={{ base: 'md', md: 100 }} pb={50}>
|
||||
<Box mb="xl">
|
||||
<Title ta="center" order={1} fw="bold" c={colors['blue-button']} mb="sm">
|
||||
<Title ta="center" order={1} fw="bold" c={colors['blue-button']}>
|
||||
Program Pendidikan Anak
|
||||
</Title>
|
||||
<Box my={"sm"}>
|
||||
<Divider size="sm" color={colors['blue-button']} mx="auto" maw={550} />
|
||||
</Box>
|
||||
<Text ta="center" fz="lg" c="black" mb="lg" maw={800} mx="auto">
|
||||
Desa Darmasaba berkomitmen mencetak generasi muda yang cerdas, berkarakter, dan siap bersaing melalui program pendidikan yang inklusif dan berkelanjutan.
|
||||
</Text>
|
||||
<Divider size="sm" color={colors['blue-button']} mx="auto" maw={120} />
|
||||
</Box>
|
||||
|
||||
<SimpleGrid
|
||||
@@ -66,7 +68,7 @@ function Page() {
|
||||
</Title>
|
||||
</Group>
|
||||
<Tooltip label="Detail tujuan program pendidikan anak" position="top-start" withArrow>
|
||||
<Text fz="lg" lh={1.6} c="dark" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: stateTujuan.findById.data?.deskripsi }} />
|
||||
<Text fz="lg" lh={1.6} c="dark" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: stateTujuan.findById.data?.deskripsi }} />
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</Paper>
|
||||
@@ -87,7 +89,7 @@ function Page() {
|
||||
</Title>
|
||||
</Group>
|
||||
<Tooltip label="Detail program unggulan yang sedang berjalan" position="top-start" withArrow>
|
||||
<Text fz="lg" lh={1.6} c="dark" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: stateUnggulan.findById.data?.deskripsi }} />
|
||||
<Text fz="lg" lh={1.6} c="dark" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: stateUnggulan.findById.data?.deskripsi }} />
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Group,
|
||||
Image,
|
||||
Pagination,
|
||||
Paper,
|
||||
@@ -68,16 +69,16 @@ function Page() {
|
||||
<Image src="/darmasaba-icon.png" w={{ base: 70, md: 100 }} alt="Logo Desa Darmasaba" loading="lazy" />
|
||||
</Center>
|
||||
<Text ta="center" fz={{ base: "1.8rem", md: "2.5rem" }} c={colors["blue-button"]} fw="bold" lh={1.4}>
|
||||
Daftar Informasi Publik Desa Darmasaba
|
||||
Daftar Informasi Publik
|
||||
</Text>
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Stack gap="lg">
|
||||
<Paper p="lg" radius="xl" shadow="sm" withBorder>
|
||||
<Stack gap="sm">
|
||||
<Text fz={{ base: 'lg', md: 'xl' }} fw="bold" c={colors["blue-button"]}>
|
||||
<Text ta={"center"} fz={{ base: 'lg', md: 'xl' }} fw="bold" c={colors["blue-button"]}>
|
||||
Tentang Informasi Publik
|
||||
</Text>
|
||||
<Text fz={{ base: 'sm', md: 'md' }} c="dimmed">
|
||||
<Text ta={"center"} fz={{ base: 'sm', md: 'md' }} c="dimmed">
|
||||
Daftar Informasi Publik Desa Darmasaba adalah kumpulan data yang dapat diakses oleh masyarakat sesuai dengan ketentuan peraturan yang berlaku.
|
||||
</Text>
|
||||
</Stack>
|
||||
@@ -117,41 +118,45 @@ function Page() {
|
||||
<TableTr key={item.id}>
|
||||
<TableTd ta="center">{(page - 1) * 5 + index + 1}</TableTd>
|
||||
<TableTd>
|
||||
<Box w={150}>
|
||||
<Box>
|
||||
<Badge variant="light" size="lg" color="blue">
|
||||
{item.jenisInformasi}
|
||||
<Text fw={650} fz={"sm"} c={'blue'} lineClamp={1}>
|
||||
{item.jenisInformasi}
|
||||
</Text>
|
||||
</Badge>
|
||||
</Box>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Box w={150}>
|
||||
<Text lineClamp={1} fz="sm" c="dark" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||
<Box>
|
||||
<Text lineClamp={1} fz="sm" c="dark" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||
</Box>
|
||||
</TableTd>
|
||||
<TableTd ta="center">
|
||||
<Box w={150}>
|
||||
{item.tanggal ? new Date(item.tanggal).toLocaleDateString('id-ID', {
|
||||
day: '2-digit',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
}) : '-'}
|
||||
<Box>
|
||||
<Text ta={"center"}>
|
||||
{item.tanggal ? new Date(item.tanggal).toLocaleDateString('id-ID', {
|
||||
day: '2-digit',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
}) : '-'}
|
||||
</Text>
|
||||
</Box>
|
||||
</TableTd>
|
||||
<TableTd style={{ textAlign: 'center' }}>
|
||||
<Box w={150}>
|
||||
<Box>
|
||||
<Tooltip label="Lihat Detail" withArrow>
|
||||
<Button
|
||||
size="xs"
|
||||
radius="md"
|
||||
variant="light"
|
||||
color="blue"
|
||||
leftSection={<IconDeviceImacCog size={16} />}
|
||||
onClick={() => router.push(`/darmasaba/ppid/daftar-informasi-publik-desa-darmasaba/${item.id}`)}
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
color="blue"
|
||||
leftSection={<IconDeviceImacCog size={16} />}
|
||||
onClick={() => router.push(`/darmasaba/ppid/daftar-informasi-publik-desa-darmasaba/${item.id}`)}
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
@@ -174,12 +179,18 @@ function Page() {
|
||||
<Paper p="lg" radius="xl" shadow="xs" withBorder>
|
||||
<Stack gap="xs">
|
||||
<Text fz="lg" fw="bold" c={colors["blue-button"]}>Kontak PPID</Text>
|
||||
<Text fz="sm" c="dimmed" lh={1.6}>
|
||||
<IconMail size={16} style={{ marginRight: 6 }} /> Email: <Text span fw="500">ppid@desadarmasaba.id</Text>
|
||||
</Text>
|
||||
<Text fz="sm" c="dimmed" lh={1.6}>
|
||||
<IconBrandWhatsapp size={16} style={{ marginRight: 6 }} /> WhatsApp: <Text span fw="500">081-xxx-xxx-xxx</Text>
|
||||
</Text>
|
||||
<Group>
|
||||
<IconMail color='gray' size={16} style={{ marginRight: 6 }} />
|
||||
<Text c={"dimmed"} fz="sm" lh={1.6}>
|
||||
Email: <Text c={"dimmed"} span fw="500">ppid@desadarmasaba.id</Text>
|
||||
</Text>
|
||||
</Group>
|
||||
<Group>
|
||||
<IconBrandWhatsapp color='gray' size={16} style={{ marginRight: 6 }} />
|
||||
<Text c={"dimmed"} fz="sm" lh={1.6}>
|
||||
WhatsApp: <Text c={"dimmed"} span fw="500">081-xxx-xxx-xxx</Text>
|
||||
</Text>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Stack>
|
||||
@@ -239,7 +239,7 @@ const state = useProxy(indeksKepuasanState.responden);
|
||||
{/* Chart Rating */}
|
||||
<Paper bg={colors['white-1']} p="md" radius="md">
|
||||
<Stack>
|
||||
<Title order={4}>Pilihan</Title>
|
||||
<Title order={4}>Ulasan</Title>
|
||||
{donutDataRating.every(item => item.value === 0) ? (
|
||||
<Text c="dimmed" ta="center" my="md">
|
||||
Belum ada data untuk ditampilkan dalam grafik
|
||||
@@ -505,7 +505,7 @@ const state = useProxy(indeksKepuasanState.responden);
|
||||
{/* Chart Rating */}
|
||||
<Paper bg={colors['white-1']} p="md" radius="md">
|
||||
<Stack>
|
||||
<Title order={4}>Pilihan</Title>
|
||||
<Title order={4}>Ulasan</Title>
|
||||
{donutDataRating.every(item => item.value === 0) ? (
|
||||
<Text c="dimmed" ta="center" my="md">
|
||||
Belum ada data untuk ditampilkan dalam grafik
|
||||
@@ -39,9 +39,9 @@ function DetailPegawaiUser() {
|
||||
const data = statePegawai.findUnique.data;
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="xl">
|
||||
<Box px={{ base: 'md', md: 100 }} py="xl">
|
||||
{/* Back button */}
|
||||
<Group mb="lg">
|
||||
<Group mb="lg" px={{ base: 'md', md: 100 }}>
|
||||
<Box
|
||||
onClick={() => router.back()}
|
||||
style={{
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client'
|
||||
|
||||
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID'
|
||||
import ScrollToTopButton from '@/app/darmasaba/_com/scrollToTopButton'
|
||||
import colors from '@/con/colors'
|
||||
@@ -10,7 +9,6 @@ import {
|
||||
Button,
|
||||
Card,
|
||||
Center,
|
||||
Container,
|
||||
Group,
|
||||
Image,
|
||||
Loader,
|
||||
@@ -36,6 +34,7 @@ import { OrganizationChart } from 'primereact/organizationchart'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useProxy } from 'valtio/utils'
|
||||
import BackButton from '../../desa/layanan/_com/BackButto'
|
||||
import './struktur.css'
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
@@ -47,10 +46,9 @@ export default function Page() {
|
||||
paddingBottom: 48,
|
||||
}}
|
||||
>
|
||||
<Container size="xl" py="xl">
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
<Box px={{ base: 'md', md: 100 }} py={"xl"}>
|
||||
<BackButton />
|
||||
|
||||
<Stack align="center" gap="xl" mt="xl">
|
||||
<Title
|
||||
order={1}
|
||||
@@ -65,12 +63,12 @@ export default function Page() {
|
||||
untuk melihat detail atau klik node untuk fokus tampilan.
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Box mt="lg">
|
||||
<StrukturOrganisasiPPID />
|
||||
</Box>
|
||||
</Container>
|
||||
</Box>
|
||||
|
||||
{/* Tombol Scroll ke Atas */}
|
||||
<ScrollToTopButton />
|
||||
</Box>
|
||||
)
|
||||
@@ -84,7 +82,7 @@ function StrukturOrganisasiPPID() {
|
||||
const [isFullscreen, setFullscreen] = useState(false)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
|
||||
// debounce untuk pencarian
|
||||
// debounce pencarian
|
||||
const debouncedSearch = useRef(
|
||||
debounce((value: string) => {
|
||||
setSearchQuery(value)
|
||||
@@ -112,7 +110,8 @@ function StrukturOrganisasiPPID() {
|
||||
)
|
||||
}
|
||||
|
||||
if (!stateOrganisasi.findMany.data || stateOrganisasi.findMany.data.length === 0) {
|
||||
const data = stateOrganisasi.findMany.data || []
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<Center py={40}>
|
||||
<Stack align="center" gap="md">
|
||||
@@ -151,9 +150,9 @@ function StrukturOrganisasiPPID() {
|
||||
)
|
||||
}
|
||||
|
||||
// Buat struktur organisasi
|
||||
// 🧩 buat struktur organisasi
|
||||
const posisiMap = new Map<string, any>()
|
||||
const aktifPegawai = stateOrganisasi.findMany.data.filter((p: any) => p.isActive)
|
||||
const aktifPegawai = data.filter((p: any) => p.isActive)
|
||||
|
||||
for (const pegawai of aktifPegawai) {
|
||||
const posisiId = pegawai.posisi.id
|
||||
@@ -176,19 +175,15 @@ function StrukturOrganisasiPPID() {
|
||||
} else root.push(posisi)
|
||||
})
|
||||
|
||||
function toOrgChartFormat(node: any): any {
|
||||
const toOrgChartFormat = (node: any): any => {
|
||||
const pegawai = node.pegawaiList?.[0]
|
||||
return {
|
||||
expanded: true,
|
||||
type: 'person',
|
||||
styleClass: 'p-person',
|
||||
data: {
|
||||
id: pegawai?.id || null,
|
||||
name: pegawai?.namaLengkap || 'Belum ditugaskan',
|
||||
title: node.nama || 'Tanpa jabatan',
|
||||
id: pegawai?.id,
|
||||
name: pegawai?.namaLengkap || 'Belum Ditugaskan',
|
||||
title: node.nama || 'Tanpa Jabatan',
|
||||
image: pegawai?.image?.link || '/img/default.png',
|
||||
description: node.deskripsi || '',
|
||||
positionId: node.id || null,
|
||||
},
|
||||
children: node.children?.map(toOrgChartFormat) || [],
|
||||
}
|
||||
@@ -213,7 +208,7 @@ function StrukturOrganisasiPPID() {
|
||||
chartData = filterNodes(chartData)
|
||||
}
|
||||
|
||||
// 🧭 fungsi fullscreen
|
||||
// 🎬 fullscreen & zoom control
|
||||
const toggleFullscreen = () => {
|
||||
if (!document.fullscreenElement) {
|
||||
chartContainerRef.current?.requestFullscreen()
|
||||
@@ -224,138 +219,249 @@ function StrukturOrganisasiPPID() {
|
||||
}
|
||||
}
|
||||
|
||||
// 🧭 fungsi zoom
|
||||
const handleZoomIn = () => setScale((prev) => Math.min(prev + 0.1, 2))
|
||||
const handleZoomOut = () => setScale((prev) => Math.max(prev - 0.1, 0.5))
|
||||
const handleZoomIn = () => setScale((s) => Math.min(s + 0.1, 2))
|
||||
const handleZoomOut = () => setScale((s) => Math.max(s - 0.1, 0.5))
|
||||
const resetZoom = () => setScale(1)
|
||||
|
||||
return (
|
||||
<Stack align="center" mt="xl">
|
||||
{/* 🔍 Search + Zoom + Fullscreen controls */}
|
||||
<Group mb="md" justify="center" gap="sm" align="center">
|
||||
<TextInput
|
||||
placeholder="Cari nama atau jabatan..."
|
||||
leftSection={<IconSearch size={16} />}
|
||||
onChange={(e) => debouncedSearch(e.target.value)}
|
||||
/>
|
||||
|
||||
<Button variant="light" size="sm" onClick={handleZoomOut}>
|
||||
<IconZoomOut size={16} />
|
||||
</Button>
|
||||
|
||||
{/* 🔍 Tambahkan indikator zoom di sini */}
|
||||
{/* Floating Zoom Indicator */}
|
||||
<Box
|
||||
bg="#C3D0E8"
|
||||
c="blue"
|
||||
px={9}
|
||||
py={8}
|
||||
style={{
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
borderRadius: '5px',
|
||||
}}
|
||||
>
|
||||
{Math.round(scale * 100)}%
|
||||
</Box>
|
||||
|
||||
|
||||
<Button variant="light" size="sm" onClick={handleZoomIn}>
|
||||
<IconZoomIn size={16} />
|
||||
</Button>
|
||||
|
||||
<Button variant="light" size="sm" onClick={resetZoom}>
|
||||
Reset
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="light"
|
||||
size="sm"
|
||||
onClick={toggleFullscreen}
|
||||
leftSection={
|
||||
isFullscreen ? <IconArrowsMinimize size={16} /> : <IconArrowsMaximize size={16} />
|
||||
}
|
||||
>
|
||||
{isFullscreen ? 'Keluar' : 'Fullscreen'}
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
|
||||
{/* Chart Container */}
|
||||
<Box
|
||||
ref={chartContainerRef}
|
||||
{/* 🔍 Controls */}
|
||||
<Paper
|
||||
shadow="xs"
|
||||
p="md"
|
||||
radius="md"
|
||||
style={{
|
||||
overflow: 'auto',
|
||||
transform: `scale(${scale})`,
|
||||
transformOrigin: 'center top',
|
||||
transition: 'transform 0.25s ease',
|
||||
background: colors['blue-button']
|
||||
}}
|
||||
>
|
||||
<OrganizationChart
|
||||
value={chartData}
|
||||
nodeTemplate={(node) => nodeTemplate(node, router)}
|
||||
/>
|
||||
</Box>
|
||||
<Group gap="sm" wrap="wrap" justify="center">
|
||||
<TextInput
|
||||
placeholder="Cari nama atau jabatan..."
|
||||
leftSection={<IconSearch size={16} />}
|
||||
onChange={(e) => debouncedSearch(e.target.value)}
|
||||
styles={{
|
||||
input: {
|
||||
minWidth: 250,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Group gap="xs">
|
||||
<Button
|
||||
variant="light"
|
||||
bg={colors['blue-button-2']}
|
||||
size="sm"
|
||||
onClick={handleZoomOut}
|
||||
leftSection={<IconZoomOut size={16} />}
|
||||
c={colors['blue-button']}
|
||||
>
|
||||
Zoom Out
|
||||
</Button>
|
||||
|
||||
<Box
|
||||
bg={colors['blue-button-2']}
|
||||
c={colors['blue-button']}
|
||||
px={16}
|
||||
py={8}
|
||||
style={{
|
||||
fontSize: 14,
|
||||
fontWeight: 700,
|
||||
borderRadius: '8px',
|
||||
minWidth: 70,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
{Math.round(scale * 100)}%
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
bg={colors['blue-button-2']}
|
||||
c={colors['blue-button']}
|
||||
variant="light"
|
||||
size="sm"
|
||||
onClick={handleZoomIn}
|
||||
leftSection={<IconZoomIn size={16} />}
|
||||
>
|
||||
Zoom In
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
bg={colors['blue-button-2']}
|
||||
c={colors['blue-button']}
|
||||
variant="light"
|
||||
size="sm"
|
||||
onClick={resetZoom}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
bg={colors['blue-button-2']}
|
||||
c={colors['blue-button']}
|
||||
size="sm"
|
||||
onClick={toggleFullscreen}
|
||||
leftSection={
|
||||
isFullscreen ? (
|
||||
<IconArrowsMinimize size={16} />
|
||||
) : (
|
||||
<IconArrowsMaximize size={16} />
|
||||
)
|
||||
}
|
||||
>
|
||||
Fullscreen
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
</Paper>
|
||||
|
||||
{/* 🧩 Chart Container */}
|
||||
<Center style={{ width: '100%' }}>
|
||||
<Box
|
||||
ref={chartContainerRef}
|
||||
style={{
|
||||
overflowX: 'auto',
|
||||
overflowY: 'auto',
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
padding: '32px 16px',
|
||||
transition: 'transform 0.2s ease',
|
||||
transform: `scale(${scale})`,
|
||||
transformOrigin: 'center top',
|
||||
}}
|
||||
>
|
||||
<OrganizationChart
|
||||
value={chartData}
|
||||
nodeTemplate={(node) => <NodeCard node={node} router={router} />}
|
||||
className="p-organizationchart p-organizationchart-horizontal"
|
||||
/>
|
||||
</Box>
|
||||
</Center>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
function nodeTemplate(node: any, router: ReturnType<typeof useTransitionRouter>) {
|
||||
function NodeCard({ node, router }: any) {
|
||||
const imageSrc = node?.data?.image || '/img/default.png'
|
||||
const name = node?.data?.name || 'Tanpa Nama'
|
||||
const title = node?.data?.title || 'Tanpa Jabatan'
|
||||
const description = node?.data?.description || ''
|
||||
const hasId = Boolean(node?.data?.id)
|
||||
|
||||
return (
|
||||
<Transition mounted transition="pop" duration={240}>
|
||||
<Transition mounted transition="pop" duration={300}>
|
||||
{(styles) => (
|
||||
<Card
|
||||
radius="lg"
|
||||
shadow="md"
|
||||
radius="xl"
|
||||
withBorder
|
||||
style={{
|
||||
...styles,
|
||||
width: 260,
|
||||
padding: 16,
|
||||
background: 'rgba(28,110,164,0.3)',
|
||||
borderColor: 'rgba(255,255,255,0.15)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
textAlign: 'center',
|
||||
width: 240,
|
||||
minHeight: 280,
|
||||
padding: 20,
|
||||
background: 'linear-gradient(135deg, rgba(28,110,164,0.15) 0%, rgba(255,255,255,0.95) 100%)',
|
||||
borderColor: 'rgba(28, 110, 164, 0.3)',
|
||||
borderWidth: 2,
|
||||
transition: 'all 0.3s ease',
|
||||
cursor: hasId ? 'pointer' : 'default',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (hasId) {
|
||||
e.currentTarget.style.transform = 'translateY(-4px)'
|
||||
e.currentTarget.style.boxShadow = '0 8px 24px rgba(28, 110, 164, 0.25)'
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (hasId) {
|
||||
e.currentTarget.style.transform = 'translateY(0)'
|
||||
e.currentTarget.style.boxShadow = ''
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={imageSrc}
|
||||
alt={name}
|
||||
radius="md"
|
||||
width={60}
|
||||
height={60}
|
||||
fit="cover"
|
||||
style={{
|
||||
objectFit: 'cover',
|
||||
border: '2px solid rgba(255,255,255,0.2)',
|
||||
marginBottom: 12,
|
||||
}}
|
||||
loading="lazy"
|
||||
/>
|
||||
<Text fw={700}>{name}</Text>
|
||||
<Text size="sm" c="dimmed" mt={4}>
|
||||
{title}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" mt={8} lineClamp={3}>
|
||||
{description || 'Belum ada deskripsi.'}
|
||||
</Text>
|
||||
<Button
|
||||
variant="light"
|
||||
size="xs"
|
||||
mt="md"
|
||||
onClick={() => {
|
||||
const id = node?.data?.id
|
||||
router.push(`/darmasaba/ppid/struktur-ppid/${id}`)
|
||||
<Stack align="center" gap={12}>
|
||||
{/* Photo */}
|
||||
<Box
|
||||
style={{
|
||||
width: 96,
|
||||
height: 96,
|
||||
borderRadius: '50%',
|
||||
overflow: 'hidden',
|
||||
border: '3px solid rgba(28, 110, 164, 0.4)',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
|
||||
background: 'white',
|
||||
}}
|
||||
>
|
||||
Lihat Detail
|
||||
</Button>
|
||||
<Image
|
||||
src={imageSrc}
|
||||
alt={name}
|
||||
width={96}
|
||||
height={96}
|
||||
fit="cover"
|
||||
loading="lazy"
|
||||
style={{
|
||||
objectFit: 'cover',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Name */}
|
||||
<Text
|
||||
fw={700}
|
||||
size="sm"
|
||||
ta="center"
|
||||
c={colors['blue-button']}
|
||||
lineClamp={2}
|
||||
style={{
|
||||
minHeight: 40,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
wordBreak: 'break-word',
|
||||
lineHeight: 1.3,
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</Text>
|
||||
|
||||
{/* Title/Position */}
|
||||
<Text
|
||||
size="xs"
|
||||
c="dimmed"
|
||||
ta="center"
|
||||
fw={500}
|
||||
lineClamp={2}
|
||||
style={{
|
||||
minHeight: 32,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
wordBreak: 'break-word',
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
|
||||
{/* Detail Button */}
|
||||
{hasId && (
|
||||
<Button
|
||||
variant="gradient"
|
||||
gradient={{ from: 'blue', to: 'cyan' }}
|
||||
size="xs"
|
||||
fullWidth
|
||||
mt={8}
|
||||
radius="md"
|
||||
onClick={() =>
|
||||
router.push(`/darmasaba/ppid/struktur-ppid/${node.data.id}`)
|
||||
}
|
||||
style={{
|
||||
height: 32,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
Lihat Detail
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</Card>
|
||||
)}
|
||||
</Transition>
|
||||
|
||||
68
src/app/darmasaba/(pages)/ppid/struktur-ppid/struktur.css
Normal file
68
src/app/darmasaba/(pages)/ppid/struktur-ppid/struktur.css
Normal file
@@ -0,0 +1,68 @@
|
||||
/* ============================================
|
||||
STRUKTUR ORGANISASI PPID - STYLING
|
||||
============================================ */
|
||||
|
||||
/* Tabel chart selalu center */
|
||||
.p-organizationchart-table {
|
||||
margin: 0 auto !important;
|
||||
}
|
||||
|
||||
/* Jarak vertikal antar level - lebih lega */
|
||||
.p-organizationchart-line-down {
|
||||
height: 32px !important;
|
||||
}
|
||||
|
||||
/* Padding di dalam node - lebih rapi */
|
||||
.p-organizationchart-node-content {
|
||||
padding: 0 !important;
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
/* Garis connector antar node - lebih tebal dan jelas */
|
||||
.p-organizationchart-line-down,
|
||||
.p-organizationchart-line-left,
|
||||
.p-organizationchart-line-right,
|
||||
.p-organizationchart-line-top {
|
||||
border-color: rgba(28, 110, 164, 0.4) !important;
|
||||
border-width: 2px !important;
|
||||
}
|
||||
|
||||
/* Garis horizontal */
|
||||
.p-organizationchart-line-left,
|
||||
.p-organizationchart-line-right {
|
||||
border-top-width: 2px !important;
|
||||
}
|
||||
|
||||
/* Jarak horizontal antar node - lebih proporsional */
|
||||
.p-organizationchart-table > tbody > tr > td {
|
||||
padding: 0 24px !important;
|
||||
vertical-align: top !important;
|
||||
}
|
||||
|
||||
/* Node container spacing */
|
||||
.p-organizationchart-node {
|
||||
padding: 8px !important;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.p-organizationchart-table > tbody > tr > td {
|
||||
padding: 0 12px !important;
|
||||
}
|
||||
|
||||
.p-organizationchart-line-down {
|
||||
height: 24px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Smooth transitions untuk zoom */
|
||||
.p-organizationchart {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
/* Fullscreen mode adjustments */
|
||||
.p-organizationchart-table:fullscreen {
|
||||
background: rgba(230, 240, 255, 0.98);
|
||||
padding: 40px;
|
||||
}
|
||||
@@ -63,7 +63,7 @@ function Page() {
|
||||
<SimpleGrid px={{ base: 'md', md: 100 }} cols={{ base: 1, sm: 2, md: 3 }} spacing="xl">
|
||||
{data.map((v: any, k: number) => (
|
||||
<BackgroundImage key={k} src={v.image?.link || ''} h={360} radius="xl" pos="relative">
|
||||
<Box pos="absolute" inset={0} bg="rgba(0,0,0,0.45)" style={{ borderRadius: 16 }} />
|
||||
<Box pos="absolute" inset={0} bg="rgba(0,0,0,0.45)" style={{ borderRadius: 27 }} />
|
||||
<Stack justify="space-between" h="100%" p="lg" pos="relative">
|
||||
<Box>
|
||||
<Text fz="lg" fw={600} c="white" ta="center">
|
||||
|
||||
@@ -2,11 +2,9 @@
|
||||
'use client';
|
||||
import penghargaanState from "@/app/admin/(dashboard)/_state/desa/penghargaan";
|
||||
import colors from "@/con/colors";
|
||||
import { Carousel } from "@mantine/carousel";
|
||||
import { Box, Button, Container, Group, Paper, Skeleton, Stack, Text, useMantineTheme } from "@mantine/core";
|
||||
import { Box, Button, Container, Group, Paper, Skeleton, Stack, Text } from "@mantine/core";
|
||||
import { useMediaQuery } from "@mantine/hooks";
|
||||
import { IconArrowRight, IconAward } from "@tabler/icons-react";
|
||||
import Autoplay from "embla-carousel-autoplay";
|
||||
import { useTransitionRouter } from "next-view-transitions";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useProxy } from "valtio/utils";
|
||||
@@ -19,7 +17,6 @@ export default function Page() {
|
||||
<BackButton />
|
||||
</Box>
|
||||
<Container w={{ base: "100%", md: "90%", lg: "60%" }}>
|
||||
|
||||
<Stack align="center" gap="sm">
|
||||
<Group gap="xs">
|
||||
<IconAward size={40} color={colors["blue-button"]} />
|
||||
@@ -30,21 +27,35 @@ export default function Page() {
|
||||
<Text fz="lg" c="dimmed" ta="center">
|
||||
Desa Darmasaba berhasil meraih beragam penghargaan bergengsi yang mencerminkan dedikasi dan kerja keras masyarakat dalam membangun desa yang maju dan berkelanjutan.
|
||||
</Text>
|
||||
<Slider />
|
||||
</Stack>
|
||||
</Container>
|
||||
<Slider />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function Slider() {
|
||||
const theme = useMantineTheme();
|
||||
const mobile = useMediaQuery(`(max-width: ${theme.breakpoints.sm})`);
|
||||
const tablet = useMediaQuery(`(max-width: ${theme.breakpoints.md})`);
|
||||
const autoplay = useRef(Autoplay({ delay: 3000, stopOnInteraction: false }));
|
||||
const mobile = useMediaQuery("(max-width: 768px)", false);
|
||||
const state = useProxy(penghargaanState);
|
||||
const router = useTransitionRouter();
|
||||
|
||||
// Refs for smooth animation
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const scrollPositionRef = useRef(0);
|
||||
const animationFrameRef = useRef<number>(0);
|
||||
const isHoveredRef = useRef(false);
|
||||
|
||||
// Refs for drag functionality
|
||||
const isDraggingRef = useRef(false);
|
||||
const startXRef = useRef(0);
|
||||
const scrollLeftRef = useRef(0);
|
||||
const velocityRef = useRef(0);
|
||||
const lastScrollTimeRef = useRef(0);
|
||||
|
||||
// Speed configuration
|
||||
const normalSpeed = 1.0; // pixels per frame
|
||||
const hoverSpeed = 0.5; // slower speed on hover
|
||||
|
||||
useEffect(() => {
|
||||
state.findMany.load();
|
||||
}, []);
|
||||
@@ -52,12 +63,128 @@ function Slider() {
|
||||
const data = state.findMany.data || [];
|
||||
const loading = state.findMany.loading;
|
||||
|
||||
// Duplicate slides for seamless infinite loop
|
||||
const slidesData = [...data, ...data, ...data];
|
||||
|
||||
useEffect(() => {
|
||||
if (loading || !containerRef.current || slidesData.length === 0) return;
|
||||
|
||||
const container = containerRef.current;
|
||||
const slideWidth = container.scrollWidth / slidesData.length;
|
||||
const originalDataLength = data.length;
|
||||
|
||||
// Start from the middle set of slides
|
||||
scrollPositionRef.current = slideWidth * originalDataLength;
|
||||
container.scrollLeft = scrollPositionRef.current;
|
||||
|
||||
const animate = () => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const container = containerRef.current;
|
||||
const slideWidth = container.scrollWidth / slidesData.length;
|
||||
|
||||
// Check if user recently scrolled manually
|
||||
const timeSinceLastScroll = Date.now() - lastScrollTimeRef.current;
|
||||
const isUserScrolling = timeSinceLastScroll < 100;
|
||||
|
||||
// Only auto-scroll if user is not actively scrolling or dragging
|
||||
if (!isDraggingRef.current && !isUserScrolling) {
|
||||
const currentSpeed = isHoveredRef.current ? hoverSpeed : normalSpeed;
|
||||
scrollPositionRef.current += currentSpeed;
|
||||
|
||||
// Reset position for infinite loop
|
||||
if (scrollPositionRef.current >= slideWidth * (originalDataLength * 2)) {
|
||||
scrollPositionRef.current -= slideWidth * originalDataLength;
|
||||
}
|
||||
|
||||
if (scrollPositionRef.current <= 0) {
|
||||
scrollPositionRef.current += slideWidth * originalDataLength;
|
||||
}
|
||||
|
||||
container.scrollLeft = scrollPositionRef.current;
|
||||
} else {
|
||||
// Sync scroll position when user is scrolling
|
||||
scrollPositionRef.current = container.scrollLeft;
|
||||
|
||||
// Apply momentum/velocity for smooth drag release
|
||||
if (!isDraggingRef.current && Math.abs(velocityRef.current) > 0.1) {
|
||||
scrollPositionRef.current += velocityRef.current;
|
||||
velocityRef.current *= 0.95; // Decay velocity
|
||||
container.scrollLeft = scrollPositionRef.current;
|
||||
}
|
||||
}
|
||||
|
||||
animationFrameRef.current = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
animationFrameRef.current = requestAnimationFrame(animate);
|
||||
|
||||
return () => {
|
||||
if (animationFrameRef.current) {
|
||||
cancelAnimationFrame(animationFrameRef.current);
|
||||
}
|
||||
};
|
||||
}, [loading, slidesData.length, data.length, mobile]);
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
isHoveredRef.current = true;
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
isHoveredRef.current = false;
|
||||
isDraggingRef.current = false;
|
||||
};
|
||||
|
||||
// Mouse drag handlers
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
isDraggingRef.current = true;
|
||||
startXRef.current = e.pageX - containerRef.current.offsetLeft;
|
||||
scrollLeftRef.current = containerRef.current.scrollLeft;
|
||||
velocityRef.current = 0;
|
||||
|
||||
containerRef.current.style.cursor = 'grabbing';
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: React.MouseEvent) => {
|
||||
if (!isDraggingRef.current || !containerRef.current) return;
|
||||
|
||||
e.preventDefault();
|
||||
const x = e.pageX - containerRef.current.offsetLeft;
|
||||
const walk = (x - startXRef.current) * 2;
|
||||
const newScrollLeft = scrollLeftRef.current - walk;
|
||||
|
||||
velocityRef.current = containerRef.current.scrollLeft - newScrollLeft;
|
||||
|
||||
containerRef.current.scrollLeft = newScrollLeft;
|
||||
scrollPositionRef.current = newScrollLeft;
|
||||
lastScrollTimeRef.current = Date.now();
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
isDraggingRef.current = false;
|
||||
containerRef.current.style.cursor = 'grab';
|
||||
};
|
||||
|
||||
// Wheel scroll handler
|
||||
const handleWheel = (e: React.WheelEvent) => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
e.preventDefault();
|
||||
containerRef.current.scrollLeft += e.deltaY;
|
||||
scrollPositionRef.current = containerRef.current.scrollLeft;
|
||||
lastScrollTimeRef.current = Date.now();
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Group justify="center" py="xl" gap="md">
|
||||
<Skeleton w={300} h={200} radius="lg" />
|
||||
<Skeleton w={300} h={200} radius="lg" visibleFrom="sm" />
|
||||
<Skeleton w={300} h={200} radius="lg" visibleFrom="md" />
|
||||
<Skeleton w={300} h={400} radius="lg" />
|
||||
<Skeleton w={300} h={400} radius="lg" visibleFrom="sm" />
|
||||
<Skeleton w={300} h={400} radius="lg" visibleFrom="md" />
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
@@ -73,138 +200,129 @@ function Slider() {
|
||||
);
|
||||
}
|
||||
|
||||
const slides = data.map((item) => (
|
||||
<Carousel.Slide key={item.id}>
|
||||
<Paper
|
||||
radius="lg"
|
||||
shadow="md"
|
||||
pos="relative"
|
||||
style={{
|
||||
height: "100%",
|
||||
backgroundImage: `url(${item.image?.link})`,
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
transition: "transform 0.3s ease, box-shadow 0.3s ease",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.transform = "translateY(-4px)";
|
||||
e.currentTarget.style.boxShadow = "0 8px 20px rgba(0,0,0,0.2)";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.transform = "translateY(0)";
|
||||
e.currentTarget.style.boxShadow = "none";
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
pos="absolute"
|
||||
inset={0}
|
||||
bg="linear-gradient(to top, rgba(0,0,0,0.8), rgba(0,0,0,0.2))"
|
||||
style={{ borderRadius: 16 }}
|
||||
/>
|
||||
<Stack justify="flex-end" h="100%" gap="sm" p="lg" pos="relative">
|
||||
<Text
|
||||
fz={{ base: "md", sm: "lg", md: "xl" }}
|
||||
fw={700}
|
||||
ta="center"
|
||||
c="white"
|
||||
lineClamp={3}
|
||||
style={{ textShadow: "0 2px 4px rgba(0,0,0,0.6)" }}
|
||||
>
|
||||
{item.name}
|
||||
</Text>
|
||||
<Group justify="center">
|
||||
<Button
|
||||
onClick={() =>
|
||||
router.push(`/darmasaba/penghargaan/${item.id}`)
|
||||
}
|
||||
size="md"
|
||||
radius="xl"
|
||||
rightSection={<IconArrowRight size={18} />}
|
||||
variant="gradient"
|
||||
gradient={{ from: "#1C6EA4", to: "#69BFF8" }}
|
||||
>
|
||||
Lihat Detail
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Carousel.Slide>
|
||||
));
|
||||
|
||||
return (
|
||||
<Box
|
||||
pos="relative"
|
||||
w="100%"
|
||||
mx="auto"
|
||||
px={{ base: "md", sm: "xl", md: "2rem", lg: "3rem" }}
|
||||
ref={containerRef}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseUp={handleMouseUp}
|
||||
onWheel={handleWheel}
|
||||
py="xl"
|
||||
style={{
|
||||
maxWidth: 1300,
|
||||
overflow: "hidden",
|
||||
cursor: "grab",
|
||||
userSelect: "none",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<Carousel
|
||||
py="xl"
|
||||
w="100%"
|
||||
h={{ base: 320, sm: 380, md: 420, lg: 450 }}
|
||||
slideSize={{
|
||||
base: "100%", // Mobile: 1
|
||||
sm: "50%", // Tablet kecil (≥768): 2
|
||||
md: "50%", // 1024px: tetap 2
|
||||
lg: "33.333%", // Desktop besar: 3
|
||||
{/* Blur edges effect */}
|
||||
<Box
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "120px",
|
||||
height: "100%",
|
||||
background: "linear-gradient(to right, rgba(249, 250, 251, 1), rgba(249, 250, 251, 0))",
|
||||
zIndex: 10,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
slideGap={{ base: "md", sm: "md", md: "lg" }}
|
||||
loop
|
||||
align="start"
|
||||
slidesToScroll={mobile ? 1 : tablet ? 2 : 3}
|
||||
plugins={[autoplay.current]}
|
||||
onMouseEnter={autoplay.current.stop}
|
||||
onMouseLeave={autoplay.current.reset}
|
||||
withControls={data.length > 3}
|
||||
draggable={data.length > 1}
|
||||
styles={{
|
||||
root: {
|
||||
position: "relative",
|
||||
},
|
||||
viewport: {
|
||||
overflow: "hidden",
|
||||
},
|
||||
container: {
|
||||
alignItems: "stretch",
|
||||
},
|
||||
control: {
|
||||
zIndex: 20,
|
||||
backgroundColor: "rgba(255,255,255,0.95)",
|
||||
color: colors["blue-button"],
|
||||
border: `2px solid ${colors["blue-button"]}`,
|
||||
width: 46,
|
||||
height: 46,
|
||||
borderRadius: "50%",
|
||||
boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
|
||||
transition: "all 0.2s ease",
|
||||
'&:hover': {
|
||||
backgroundColor: colors["blue-button"],
|
||||
color: "white",
|
||||
transform: "scale(1.1)",
|
||||
},
|
||||
'&[data-inactive]': {
|
||||
opacity: 0,
|
||||
cursor: 'default',
|
||||
},
|
||||
},
|
||||
controls: {
|
||||
position: "absolute",
|
||||
top: mobile ? "70%" : tablet ? "65%" : "60%",
|
||||
transform: "translateY(-50%)",
|
||||
width: mobile ? "100%" : tablet ? "calc(100% + 60px)" : "calc(100% + 100px)",
|
||||
left: mobile ? "0" : tablet ? "-30px" : "-50px",
|
||||
right: mobile ? "0" : tablet ? "-30px" : "-50px",
|
||||
padding: "0",
|
||||
justifyContent: "space-between",
|
||||
zIndex: 30,
|
||||
},
|
||||
/>
|
||||
<Box
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
right: 0,
|
||||
width: "120px",
|
||||
height: "100%",
|
||||
background: "linear-gradient(to left, rgba(249, 250, 251, 1), rgba(249, 250, 251, 0))",
|
||||
zIndex: 10,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
/>
|
||||
|
||||
<Box
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: mobile ? "1rem" : "1.5rem",
|
||||
paddingLeft: mobile ? "1rem" : "2rem",
|
||||
paddingRight: mobile ? "1rem" : "2rem",
|
||||
}}
|
||||
>
|
||||
{slides}
|
||||
</Carousel>
|
||||
{slidesData.map((item, index) => (
|
||||
<Box
|
||||
key={`${item.id}-${index}`}
|
||||
style={{
|
||||
flex: `0 0 ${mobile ? "90%" : "calc(33.333% - 1rem)"}`,
|
||||
minWidth: mobile ? "90%" : "calc(33.333% - 1rem)",
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
radius="lg"
|
||||
shadow="md"
|
||||
pos="relative"
|
||||
style={{
|
||||
height: mobile ? "380px" : "450px",
|
||||
backgroundImage: `url(${item.image?.link})`,
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
transition: "transform 0.3s ease, box-shadow 0.3s ease",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.transform = "translateY(-8px) scale(1.02)";
|
||||
e.currentTarget.style.boxShadow = "0 12px 28px rgba(0,0,0,0.25)";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.transform = "translateY(0) scale(1)";
|
||||
e.currentTarget.style.boxShadow = "0 4px 12px rgba(0,0,0,0.15)";
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
pos="absolute"
|
||||
inset={0}
|
||||
bg="linear-gradient(to top, rgba(0,0,0,0.85), rgba(0,0,0,0.3))"
|
||||
style={{ borderRadius: 16 }}
|
||||
/>
|
||||
<Stack justify="flex-end" h="100%" gap="sm" p="lg" pos="relative">
|
||||
<Text
|
||||
fz={{ base: "lg", sm: "xl", md: "1.5rem" }}
|
||||
fw={700}
|
||||
ta="center"
|
||||
c="white"
|
||||
lineClamp={3}
|
||||
style={{ textShadow: "0 2px 8px rgba(0,0,0,0.8)" }}
|
||||
>
|
||||
{item.name}
|
||||
</Text>
|
||||
<Group justify="center">
|
||||
<Button
|
||||
onClick={() => router.push(`/darmasaba/penghargaan/${item.id}`)}
|
||||
size="md"
|
||||
radius="xl"
|
||||
rightSection={<IconArrowRight size={18} />}
|
||||
variant="gradient"
|
||||
gradient={{ from: "#1C6EA4", to: "#69BFF8" }}
|
||||
style={{
|
||||
transition: "transform 0.2s ease",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.transform = "scale(1.05)";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.transform = "scale(1)";
|
||||
}}
|
||||
>
|
||||
Lihat Detail
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
'use client'
|
||||
import { Box, Center, Container, Image, LoadingOverlay, Paper, SimpleGrid, Stack, Text, Title, Tooltip } from '@mantine/core';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { IconMoodSad } from '@tabler/icons-react';
|
||||
import BackButton from '../../(pages)/desa/layanan/_com/BackButto';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Center, Container, Image, LoadingOverlay, Paper, SimpleGrid, Stack, Text, Title } from '@mantine/core';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { IconMoodSad } from '@tabler/icons-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import BackButton from '../../(pages)/desa/layanan/_com/BackButto';
|
||||
|
||||
function Page() {
|
||||
const [sdgsDesa, setSdgsDesa] = useState<Prisma.SdgsDesaGetPayload<{ include: { image: true } }>[]>([]);
|
||||
@@ -114,11 +114,9 @@ function Page() {
|
||||
/>
|
||||
</Box>
|
||||
<Stack gap="xs" align="center" style={{ width: '100%' }}>
|
||||
<Tooltip label={item.name} position="top" withArrow>
|
||||
<Title order={4} ta="center" c="dark" fw={600} lineClamp={2} style={{ minHeight: '3rem' }}>
|
||||
{item.name}
|
||||
</Title>
|
||||
</Tooltip>
|
||||
<Text
|
||||
ta="center"
|
||||
fw={700}
|
||||
|
||||
@@ -116,7 +116,7 @@ function NavbarMobile({ listNavbar }: { listNavbar: MenuItem[] }) {
|
||||
radius="md"
|
||||
p="sm"
|
||||
withBorder
|
||||
bg={active ? "blue.0" : "gray.0"}
|
||||
bg={active ? colors["blue-button-2"] : "gray.0"}
|
||||
onClick={() => {
|
||||
if (item.href) {
|
||||
router.push(item.href);
|
||||
@@ -126,21 +126,21 @@ function NavbarMobile({ listNavbar }: { listNavbar: MenuItem[] }) {
|
||||
style={{
|
||||
cursor: item.href ? "pointer" : "default",
|
||||
transition: "background 0.15s ease",
|
||||
borderLeft: active ? "4px solid #1e66f5" : "4px solid transparent",
|
||||
borderLeft: active ? `4px solid ${colors['blue-button']}` : "4px solid transparent",
|
||||
}}
|
||||
>
|
||||
<Group justify="space-between" align="center" wrap="nowrap">
|
||||
<Text
|
||||
fw={active ? 700 : 600}
|
||||
fz="md"
|
||||
c={active ? "blue.7" : "dark.9"}
|
||||
c={active ? colors['blue-button'] : "dark.9"}
|
||||
>
|
||||
{item.name}
|
||||
</Text>
|
||||
{item.href && (
|
||||
<IconSquareArrowRight
|
||||
size={18}
|
||||
color={active ? "#1e66f5" : "inherit"}
|
||||
color={active ? colors['blue-button'] : "inherit"}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
@@ -167,21 +167,21 @@ function NavbarMobile({ listNavbar }: { listNavbar: MenuItem[] }) {
|
||||
cursor: child.href ? "pointer" : "default",
|
||||
opacity: child.href ? 1 : 0.8,
|
||||
borderRadius: "0.5rem",
|
||||
backgroundColor: childActive ? "#e7f0ff" : "transparent",
|
||||
borderLeft: childActive ? "3px solid #1e66f5" : "3px solid transparent",
|
||||
backgroundColor: childActive ? colors["blue-button-2"] : "transparent",
|
||||
borderLeft: childActive ? `3px solid ${colors['blue-button']}` : "3px solid transparent",
|
||||
transition: "background 0.15s ease",
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
fz="sm"
|
||||
fw={childActive ? 600 : 400}
|
||||
c={childActive ? "blue.7" : "dark.8"}
|
||||
c={childActive ? colors['blue-button'] : "dark.8"}
|
||||
>
|
||||
{child.name}
|
||||
</Text>
|
||||
<IconSquareArrowRight
|
||||
size={14}
|
||||
color={childActive ? "#1e66f5" : "inherit"}
|
||||
color={childActive ? colors['blue-button'] : "inherit"}
|
||||
/>
|
||||
</Group>
|
||||
);
|
||||
|
||||
@@ -112,7 +112,7 @@ function MenuItemCom({ item, isActive = false }: { item: MenuItem, isActive?: bo
|
||||
<MenuTarget>
|
||||
<Button
|
||||
variant="subtle"
|
||||
color={isActive ? 'blue' : 'gray'}
|
||||
color={isActive ? colors['blue-button'] : 'gray'}
|
||||
onClick={() => {
|
||||
if (item.href) {
|
||||
router.push(item.href);
|
||||
|
||||
@@ -49,12 +49,12 @@ export function NavbarSubMenu({ item }: { item: MenuItem[] | null }) {
|
||||
rightSection={<IconArrowRight size={18} />}
|
||||
styles={(theme) => ({
|
||||
root: {
|
||||
background: link.href && pathname.startsWith(link.href) ? theme.colors.blue[0] : 'transparent',
|
||||
color: link.href && pathname.startsWith(link.href) ? theme.colors.blue[7] : colors['blue-button'],
|
||||
background: link.href && pathname.startsWith(link.href) ? colors['blue-button-2'] : 'transparent',
|
||||
color: link.href && pathname.startsWith(link.href) ? colors['blue-button'] : 'gray',
|
||||
fontWeight: link.href && pathname.startsWith(link.href) ? 600 : 500,
|
||||
transition: "all 0.2s ease",
|
||||
"&:hover": {
|
||||
background: link.href && pathname.startsWith(link.href) ? theme.colors.blue[1] : theme.colors.gray[0],
|
||||
background: link.href && pathname.startsWith(link.href) ? colors['blue-button-2'] : theme.colors.gray[0],
|
||||
}
|
||||
},
|
||||
})}
|
||||
|
||||
96
src/app/darmasaba/_com/NewsReader.tsx
Normal file
96
src/app/darmasaba/_com/NewsReader.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
'use client';
|
||||
import { Button } from '@mantine/core';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
const NewsReader = () => {
|
||||
const [isSpeaking, setIsSpeaking] = useState(false);
|
||||
const [isAllowed, setIsAllowed] = useState(false);
|
||||
const utteranceRef = useRef<SpeechSynthesisUtterance | null>(null);
|
||||
|
||||
// Fungsi untuk membaca teks
|
||||
const speakText = () => {
|
||||
if (typeof window === 'undefined' || !window.speechSynthesis) {
|
||||
console.warn('Browser tidak mendukung SpeechSynthesis.');
|
||||
return;
|
||||
}
|
||||
|
||||
const contentElement = document.getElementById('news-content');
|
||||
const rawText = contentElement?.innerText || '';
|
||||
if (!rawText.trim()) return;
|
||||
|
||||
// Hentikan semua suara sebelumnya
|
||||
window.speechSynthesis.cancel();
|
||||
|
||||
const utterance = new SpeechSynthesisUtterance(rawText);
|
||||
utterance.lang = 'id-ID';
|
||||
utterance.rate = 1;
|
||||
utterance.pitch = 1;
|
||||
|
||||
utterance.onstart = () => setIsSpeaking(true);
|
||||
utterance.onend = () => setIsSpeaking(false);
|
||||
|
||||
utteranceRef.current = utterance;
|
||||
|
||||
try {
|
||||
window.speechSynthesis.speak(utterance);
|
||||
} catch (err) {
|
||||
console.warn('Autoplay gagal karena kebijakan browser:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// Auto play jika sudah pernah diizinkan
|
||||
useEffect(() => {
|
||||
const hasPermission = localStorage.getItem('ttsAllowed') === 'true';
|
||||
setIsAllowed(hasPermission);
|
||||
|
||||
if (hasPermission) {
|
||||
const trySpeak = setInterval(() => {
|
||||
const contentElement = document.getElementById('news-content');
|
||||
if (contentElement && contentElement.innerText.trim()) {
|
||||
speakText();
|
||||
clearInterval(trySpeak);
|
||||
}
|
||||
}, 1000);
|
||||
return () => clearInterval(trySpeak);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Hentikan suara saat user keluar halaman / komponen unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (typeof window !== 'undefined' && window.speechSynthesis) {
|
||||
window.speechSynthesis.cancel();
|
||||
setIsSpeaking(false);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Handle tombol manual
|
||||
const handleToggle = () => {
|
||||
if (isSpeaking) {
|
||||
window.speechSynthesis.cancel();
|
||||
setIsSpeaking(false);
|
||||
} else {
|
||||
if (!isAllowed) {
|
||||
localStorage.setItem('ttsAllowed', 'true');
|
||||
setIsAllowed(true);
|
||||
}
|
||||
speakText();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={handleToggle}
|
||||
color="#0B4F78"
|
||||
variant="filled"
|
||||
radius="xl"
|
||||
size="md"
|
||||
mt="md"
|
||||
>
|
||||
{isSpeaking ? '🔇 Hentikan Suara' : '🔊 Dengarkan Berita'}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewsReader;
|
||||
185
src/app/darmasaba/_com/RunningText.tsx
Normal file
185
src/app/darmasaba/_com/RunningText.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
"use client";
|
||||
|
||||
import { Box } from "@mantine/core";
|
||||
import { IconBell } from "@tabler/icons-react";
|
||||
import { useMemo, useState, useEffect } from "react";
|
||||
|
||||
interface RunningTextProps {
|
||||
news?: string[];
|
||||
speed?: number; // dalam detik (jika mau manual)
|
||||
autoSpeed?: boolean; // otomatis sesuaikan speed dengan panjang text
|
||||
bgColor?: string;
|
||||
textColor?: string;
|
||||
maxLength?: number; // max karakter per item
|
||||
}
|
||||
|
||||
// Utility function untuk strip HTML (works on both server and client)
|
||||
function stripHtmlTags(html: string): string {
|
||||
const text = html
|
||||
.replace(/<style[^>]*>.*?<\/style>/gi, '')
|
||||
.replace(/<script[^>]*>.*?<\/script>/gi, '')
|
||||
.replace(/<[^>]+>/g, '')
|
||||
.replace(/ /gi, ' ')
|
||||
.replace(/&/gi, '&')
|
||||
.replace(/</gi, '<')
|
||||
.replace(/>/gi, '>')
|
||||
.replace(/"/gi, '"')
|
||||
.replace(/'/gi, "'")
|
||||
.replace(/’/gi, "'")
|
||||
.replace(/—/gi, '—')
|
||||
.replace(/–/gi, '–')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
export default function RunningText({
|
||||
news = [
|
||||
"Selamat datang di Portal Desa Darmasaba",
|
||||
"Jam operasional kantor: Senin - Jumat 08:00 - 17:00",
|
||||
],
|
||||
speed = 20,
|
||||
autoSpeed = true,
|
||||
bgColor = "#1e5a7e",
|
||||
textColor = "white",
|
||||
maxLength = 200 // default max 200 karakter per item
|
||||
}: RunningTextProps) {
|
||||
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
}, []);
|
||||
|
||||
// Process news data
|
||||
const processedNews = useMemo(() => {
|
||||
return news
|
||||
.filter(item => item && item.trim() !== "")
|
||||
.map(item => {
|
||||
let text = stripHtmlTags(item);
|
||||
// Limit panjang per item
|
||||
if (text.length > maxLength) {
|
||||
text = text.substring(0, maxLength) + "...";
|
||||
}
|
||||
return text;
|
||||
})
|
||||
.filter(item => item.length > 0);
|
||||
}, [news, maxLength]);
|
||||
|
||||
const allNews = processedNews.length > 0
|
||||
? processedNews.join(" • ")
|
||||
: "Tidak ada pengumuman";
|
||||
|
||||
// Hitung speed berdasarkan mode
|
||||
const calculatedSpeed = useMemo(() => {
|
||||
if (!autoSpeed) {
|
||||
return speed; // Gunakan speed manual
|
||||
}
|
||||
|
||||
// Auto speed: berdasarkan panjang text
|
||||
const textLength = allNews.length;
|
||||
|
||||
// Formula yang lebih natural:
|
||||
// - Text pendek (< 100 char): 15 detik
|
||||
// - Text sedang (100-300 char): 20-30 detik
|
||||
// - Text panjang (> 300 char): 30-45 detik
|
||||
let calculatedTime;
|
||||
|
||||
if (textLength < 100) {
|
||||
calculatedTime = 15;
|
||||
} else if (textLength < 300) {
|
||||
calculatedTime = 15 + ((textLength - 100) / 200) * 15; // 15-30 detik
|
||||
} else {
|
||||
calculatedTime = 30 + Math.min(((textLength - 300) / 500) * 15, 15); // 30-45 detik max
|
||||
}
|
||||
|
||||
return Math.round(calculatedTime);
|
||||
}, [allNews, speed, autoSpeed]);
|
||||
|
||||
// Prevent hydration mismatch
|
||||
if (!isMounted) {
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
backgroundColor: bgColor,
|
||||
overflow: "hidden",
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
padding: "12px 0",
|
||||
borderBottom: "2px solid rgba(255, 255, 255, 0.1)"
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: "8px",
|
||||
whiteSpace: "nowrap"
|
||||
}}>
|
||||
<IconBell size={20} color={textColor} style={{ flexShrink: 0 }} />
|
||||
<span style={{
|
||||
color: textColor,
|
||||
fontSize: "15px",
|
||||
fontWeight: 500,
|
||||
whiteSpace: "nowrap"
|
||||
}}>
|
||||
Memuat pengumuman...
|
||||
</span>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
backgroundColor: bgColor,
|
||||
overflow: "hidden",
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
padding: "12px 0",
|
||||
borderBottom: "2px solid rgba(255, 255, 255, 0.1)"
|
||||
}}
|
||||
>
|
||||
<style dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
@keyframes scrollText {
|
||||
0% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
}
|
||||
|
||||
.running-text-wrapper {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
white-space: nowrap;
|
||||
animation: scrollText ${calculatedSpeed}s linear infinite;
|
||||
}
|
||||
|
||||
.running-text-wrapper:hover {
|
||||
animation-play-state: paused;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.running-text-content {
|
||||
color: ${textColor};
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
`
|
||||
}} />
|
||||
|
||||
<div className="running-text-wrapper">
|
||||
<IconBell size={20} color={textColor} style={{ flexShrink: 0 }} />
|
||||
<span className="running-text-content">
|
||||
{allNews}
|
||||
</span>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -35,7 +35,7 @@ function Apbdes() {
|
||||
|
||||
return (
|
||||
<Stack p="sm" gap="xl" bg={colors.Bg}>
|
||||
<Box>
|
||||
<Box mt={"xl"}>
|
||||
<Stack gap="sm">
|
||||
<Text c={colors["blue-button"]} ta={"center"} fw={"bold"} fz={{ base: "1.8rem", md: "3.4rem" }}>
|
||||
{textHeading.title}
|
||||
@@ -72,12 +72,7 @@ function Apbdes() {
|
||||
pos="relative"
|
||||
style={{ overflow: 'hidden' }}
|
||||
>
|
||||
<Box
|
||||
pos="absolute"
|
||||
inset={0}
|
||||
bg="rgba(0,0,0,0.55)"
|
||||
style={{ backdropFilter: 'blur(4px)' }}
|
||||
/>
|
||||
<Box pos="absolute" inset={0} bg="rgba(0,0,0,0.45)" style={{ borderRadius: 16 }} />
|
||||
<Stack justify="space-between" h="100%" p="xl" pos="relative">
|
||||
<Text
|
||||
c="white"
|
||||
@@ -117,7 +112,7 @@ function Apbdes() {
|
||||
)}
|
||||
</SimpleGrid>
|
||||
|
||||
<Group justify="center" pb={10}>
|
||||
<Group justify="center" pb={"xl"}>
|
||||
<Button
|
||||
component={Link}
|
||||
href="/darmasaba/apbdes"
|
||||
|
||||
@@ -29,7 +29,7 @@ function DesaAntiKorupsi() {
|
||||
|
||||
const data = (state.desaAntikorupsi.findMany.data || []).slice(0, 6);
|
||||
return (
|
||||
<Stack gap={"0"} bg={colors.Bg} p={"sm"}>
|
||||
<Stack gap={"0"} bg={colors.Bg} p={"sm"} my={"xs"}>
|
||||
<Container w={{ base: "100%", md: "80%" }} p={"md"} >
|
||||
<Center>
|
||||
<Text fw={"bold"} c={colors["blue-button"]} fz={{ base: "1.8rem", md: "3.4rem" }}>Desa Anti Korupsi</Text>
|
||||
|
||||
@@ -153,7 +153,7 @@ function Kepuasan() {
|
||||
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<Stack p="sm">
|
||||
<Stack p="sm" my={"xs"}>
|
||||
<Container w={{ base: "100%", md: "80%" }} p={"sm"}>
|
||||
<Center>
|
||||
<Text
|
||||
@@ -245,7 +245,7 @@ function Kepuasan() {
|
||||
{/* Chart Rating */}
|
||||
<Paper bg={colors['white-1']} p="md" radius="md">
|
||||
<Stack>
|
||||
<Title order={4}>Pilihan</Title>
|
||||
<Title order={4}>Ulasan</Title>
|
||||
{donutDataRating.every(item => item.value === 0) ? (
|
||||
<Text c="dimmed" ta="center" my="md">
|
||||
Belum ada data untuk ditampilkan dalam grafik
|
||||
@@ -421,7 +421,7 @@ function Kepuasan() {
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Stack p={"sm"}>
|
||||
<Stack p={"sm"} my={"xs"}>
|
||||
<Container size="lg" px="sm">
|
||||
<Center>
|
||||
<Text
|
||||
@@ -517,7 +517,7 @@ function Kepuasan() {
|
||||
{/* Chart Rating */}
|
||||
<Paper bg={colors['white-1']} p="md" radius="md">
|
||||
<Stack>
|
||||
<Title order={4}>Pilihan</Title>
|
||||
<Title order={4}>Ulasan</Title>
|
||||
{donutDataRating.every(item => item.value === 0) ? (
|
||||
<Text c="dimmed" ta="center" my="md">
|
||||
Belum ada data untuk ditampilkan dalam grafik
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user