- QC User & Admin Menu Lingkungan
- Fix SubMenu : Edukasi Lingkungan & Konservasi Adat Bali dibagian User - Fix SUbMenu : Gotong Royong User ( Tabs kategori menyesuaikan dengan data kategori kegiatan )
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
[
|
||||
{ "name": "Semua" },
|
||||
{ "name": "Pemerintahan" },
|
||||
{ "name": "Pembangunan" },
|
||||
{ "name": "Ekonomi" },
|
||||
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{ "nama": "Kebersihan" },
|
||||
{ "nama": "Infrastruktur" },
|
||||
{ "nama": "Sosial" },
|
||||
{ "nama": "Lingkungan" }
|
||||
]
|
||||
@@ -1820,7 +1820,7 @@ model KategoriKegiatan {
|
||||
isActive Boolean @default(true)
|
||||
KegiatanDesa KegiatanDesa[]
|
||||
}
|
||||
|
||||
|
||||
// ========================================= EDUKASI LINGKUNGAN ========================================= //
|
||||
model TujuanEdukasiLingkungan {
|
||||
id String @id @default(cuid())
|
||||
|
||||
@@ -33,11 +33,12 @@ import detailDataPengangguran from "./data/ekonomi/jumlah-pengangguran/detail-da
|
||||
import kategoriProduk from "./data/ekonomi/pasar-desa/kategori-produk.json";
|
||||
import pegawai from "./data/ekonomi/struktur-organisasi/pegawai-bumdes.json";
|
||||
import posisiOrganisasi from "./data/ekonomi/struktur-organisasi/posisi-organisasi-bumdes.json";
|
||||
import kategoriBerita from "./data/kategori-berita.json";
|
||||
import kategoriBerita from "./data/desa/berita/kategori-berita.json";
|
||||
import contohEdukasiLingkungan from "./data/lingkungan/edukasi-lingkungan/contoh-kegiatan-di-desa-darmasaba.json";
|
||||
import materiEdukasiLingkungan from "./data/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan.json";
|
||||
import tujuanEdukasiLingkungan from "./data/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan.json";
|
||||
import bentukKonservasiBerdasarkanAdat from "./data/lingkungan/konservasi-adat-bali/bentuk-konservasi.json";
|
||||
import kategoriKegiatanData from "./data/lingkungan/gotong-royong/kategori-gotong-royong.json";
|
||||
import filosofiTriHita from "./data/lingkungan/konservasi-adat-bali/filosofi-tri-hita.json";
|
||||
import nilaiKonservasiAdat from "./data/lingkungan/konservasi-adat-bali/nilai-konservasi-adat.json";
|
||||
import caraMemperolehInformasi from "./data/list-caraMemperolehInformasi.json";
|
||||
@@ -885,6 +886,30 @@ import { safeSeedUnique } from "./safeseedUnique";
|
||||
}
|
||||
console.log("📊 detailDataPengangguran success ...");
|
||||
|
||||
// =========== KATEGORI GOTONG ROYONG ===========
|
||||
// Add IDs to the kategoriKegiatan data
|
||||
const kategoriKegiatan = kategoriKegiatanData.map((k, index) => ({
|
||||
...k,
|
||||
id: `kategori-${index + 1}`
|
||||
}));
|
||||
|
||||
for (const k of kategoriKegiatan) {
|
||||
await prisma.kategoriKegiatan.upsert({
|
||||
where: {
|
||||
id: k.id,
|
||||
},
|
||||
update: {
|
||||
nama: k.nama,
|
||||
},
|
||||
create: {
|
||||
id: k.id,
|
||||
nama: k.nama,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("kategori kegiatan success ...");
|
||||
|
||||
for (const e of tujuanEdukasiLingkungan) {
|
||||
await prisma.tujuanEdukasiLingkungan.upsert({
|
||||
where: {
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Group,
|
||||
Pagination,
|
||||
Paper,
|
||||
Skeleton,
|
||||
@@ -16,11 +15,10 @@ import {
|
||||
TableThead,
|
||||
TableTr,
|
||||
Text,
|
||||
Title,
|
||||
Tooltip,
|
||||
Title
|
||||
} from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconDeviceImac, IconSearch, IconPlus } from '@tabler/icons-react';
|
||||
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
@@ -72,20 +70,7 @@ function ListAjukanIdeInovatif({ search }: { search: string }) {
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={4}>Daftar Ide Inovatif</Title>
|
||||
<Tooltip label="Ajukan Ide Baru" withArrow>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/inovasi/ajukan-ide-inovatif/create')}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
|
||||
<Title order={4}>Daftar Ide Inovatif</Title>
|
||||
<Box style={{ overflowX: "auto" }}>
|
||||
<Table highlightOnHover>
|
||||
<TableThead>
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
'use client'
|
||||
'use client';
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
||||
import desaDigitalState from '@/app/admin/(dashboard)/_state/inovasi/desa-digital';
|
||||
import colors from '@/con/colors';
|
||||
import ApiFetch from '@/lib/api-fetch';
|
||||
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Image,
|
||||
Paper,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { Dropzone } from '@mantine/dropzone';
|
||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
@@ -20,16 +31,14 @@ function EditDigitalSmartVillage() {
|
||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
|
||||
// ✅ hanya lokal state untuk form
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
deskripsi: '',
|
||||
imageId: '',
|
||||
});
|
||||
|
||||
// load data sekali saat mount
|
||||
useEffect(() => {
|
||||
const loadPenghargaan = async () => {
|
||||
const loadData = async () => {
|
||||
const id = params?.id as string;
|
||||
if (!id) return;
|
||||
|
||||
@@ -42,69 +51,67 @@ function EditDigitalSmartVillage() {
|
||||
imageId: data.imageId || '',
|
||||
});
|
||||
|
||||
if (data?.image?.link) {
|
||||
setPreviewImage(data.image.link);
|
||||
}
|
||||
if (data?.image?.link) setPreviewImage(data.image.link);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading desa digital smart village:", error);
|
||||
toast.error("Gagal memuat data desa digital smart village");
|
||||
console.error('Error loading data:', error);
|
||||
toast.error('Gagal memuat data desa digital smart village');
|
||||
}
|
||||
};
|
||||
|
||||
loadPenghargaan();
|
||||
loadData();
|
||||
}, [params?.id]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
// ✅ update global state hanya saat submit
|
||||
stateDesaDigital.edit.form = {
|
||||
...stateDesaDigital.edit.form,
|
||||
...formData,
|
||||
};
|
||||
stateDesaDigital.edit.form = { ...stateDesaDigital.edit.form, ...formData };
|
||||
|
||||
if (file) {
|
||||
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
|
||||
const uploaded = res.data?.data;
|
||||
|
||||
if (!uploaded?.id) {
|
||||
return toast.error("Gagal upload gambar");
|
||||
}
|
||||
if (!uploaded?.id) return toast.error('Gagal upload gambar');
|
||||
|
||||
stateDesaDigital.edit.form.imageId = uploaded.id;
|
||||
}
|
||||
|
||||
await stateDesaDigital.edit.update();
|
||||
toast.success("Desa digital smart village berhasil diperbarui!");
|
||||
router.push("/admin/inovasi/desa-digital-smart-village");
|
||||
toast.success('Desa digital smart village berhasil diperbarui!');
|
||||
router.push('/admin/inovasi/desa-digital-smart-village');
|
||||
} catch (error) {
|
||||
console.error("Error updating desa digital smart village:", error);
|
||||
toast.error("Terjadi kesalahan saat memperbarui desa digital smart village");
|
||||
console.error('Error updating desa digital:', error);
|
||||
toast.error('Terjadi kesalahan saat memperbarui data');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button variant="subtle" onClick={() => router.back()}>
|
||||
<IconArrowBack color={colors["blue-button"]} size={30} />
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
|
||||
<Stack gap={"xs"}>
|
||||
<Title order={3}>Edit Desa Digital Smart Village</Title>
|
||||
|
||||
{/* ✅ controlled input */}
|
||||
<TextInput
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
|
||||
placeholder="masukkan judul"
|
||||
/>
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
{/* Header */}
|
||||
<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>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Edit Desa Digital Smart Village
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
{/* Form Card */}
|
||||
<Paper
|
||||
w={{ base: '100%', md: '55%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
{/* Dropzone Upload */}
|
||||
<Box>
|
||||
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
Gambar Desa Digital
|
||||
</Text>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const selectedFile = files[0];
|
||||
@@ -113,43 +120,43 @@ function EditDigitalSmartVillage() {
|
||||
setPreviewImage(URL.createObjectURL(selectedFile));
|
||||
}
|
||||
}}
|
||||
onReject={() => toast.error('File tidak valid.')}
|
||||
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
||||
maxSize={5 * 1024 ** 2}
|
||||
accept={{ 'image/*': [] }}
|
||||
radius="md"
|
||||
p="xl"
|
||||
>
|
||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
||||
<Group justify="center" gap="xl" mih={180}>
|
||||
<Dropzone.Accept>
|
||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||
<IconX size={48} color="red" stroke={1.5} />
|
||||
</Dropzone.Reject>
|
||||
<Dropzone.Idle>
|
||||
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||
<IconPhoto size={48} color="#868e96" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
|
||||
<div>
|
||||
<Text size="xl" inline>
|
||||
Drag gambar ke sini atau klik untuk pilih file
|
||||
<Stack gap="xs" align="center">
|
||||
<Text size="md" fw={500}>
|
||||
Seret gambar atau klik untuk memilih file
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" inline mt={7}>
|
||||
Maksimal 5MB dan harus format gambar
|
||||
<Text size="sm" c="dimmed">
|
||||
Maksimal 5MB, format gambar wajib
|
||||
</Text>
|
||||
</div>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
|
||||
{previewImage && (
|
||||
<Box mt="sm">
|
||||
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<Image
|
||||
src={previewImage}
|
||||
alt="Preview"
|
||||
alt="Preview Gambar"
|
||||
radius="md"
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
maxHeight: '200px',
|
||||
maxHeight: 220,
|
||||
objectFit: 'contain',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #ddd',
|
||||
border: `1px solid ${colors['blue-button']}`,
|
||||
}}
|
||||
loading="lazy"
|
||||
/>
|
||||
@@ -157,18 +164,43 @@ function EditDigitalSmartVillage() {
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Input Judul */}
|
||||
<TextInput
|
||||
label="Judul"
|
||||
placeholder="Masukkan judul inovasi"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
required
|
||||
/>
|
||||
|
||||
{/* Editor Deskripsi */}
|
||||
<Box>
|
||||
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
|
||||
{/* ✅ controlled editor */}
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
Deskripsi
|
||||
</Text>
|
||||
<EditEditor
|
||||
value={formData.deskripsi}
|
||||
onChange={(htmlContent) => {
|
||||
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }));
|
||||
}}
|
||||
onChange={(htmlContent) =>
|
||||
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }))
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Button onClick={handleSubmit}>Simpan</Button>
|
||||
{/* Tombol Simpan */}
|
||||
<Group justify="right">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
size="md"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||
}}
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Image,
|
||||
Paper,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Text,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
|
||||
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
@@ -10,95 +20,136 @@ import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||
import desaDigitalState from '../../../_state/inovasi/desa-digital';
|
||||
|
||||
function DetailDesaDigital() {
|
||||
const stateDesaDigital = useProxy(desaDigitalState)
|
||||
const stateDesaDigital = useProxy(desaDigitalState);
|
||||
const [modalHapus, setModalHapus] = useState(false);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const router = useRouter()
|
||||
const params = useParams()
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
|
||||
useShallowEffect(() => {
|
||||
stateDesaDigital.findUnique.load(params?.id as string)
|
||||
}, [])
|
||||
stateDesaDigital.findUnique.load(params?.id as string);
|
||||
}, []);
|
||||
|
||||
const handleHapus = () => {
|
||||
if (selectedId) {
|
||||
stateDesaDigital.delete.byId(selectedId)
|
||||
setModalHapus(false)
|
||||
setSelectedId(null)
|
||||
router.push("/admin/inovasi/desa-digital-smart-village")
|
||||
stateDesaDigital.delete.byId(selectedId);
|
||||
setModalHapus(false);
|
||||
setSelectedId(null);
|
||||
router.push("/admin/inovasi/desa-digital-smart-village");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!stateDesaDigital.findUnique.data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton h={500} />
|
||||
<Skeleton height={500} radius="md" />
|
||||
</Stack>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const data = stateDesaDigital.findUnique.data;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button variant="subtle" onClick={() => router.back()}>
|
||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||
</Button>
|
||||
</Box>
|
||||
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
|
||||
<Stack>
|
||||
<Text fz={"xl"} fw={"bold"}>Detail Desa Digital Smart Village</Text>
|
||||
{stateDesaDigital.findUnique.data ? (
|
||||
<Paper key={stateDesaDigital.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
||||
<Stack gap={"xs"}>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Judul</Text>
|
||||
<Text fz={"lg"}>{stateDesaDigital.findUnique.data?.name}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
|
||||
<Text fz={"lg"} style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: stateDesaDigital.findUnique.data?.deskripsi }} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
|
||||
<Image w={{ base: 150, md: 150, lg: 150 }} src={stateDesaDigital.findUnique.data?.image?.link} alt="gambar" loading="lazy"/>
|
||||
</Box>
|
||||
<Flex gap={"xs"} mt={10}>
|
||||
<Box py={10}>
|
||||
{/* Tombol Kembali */}
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
|
||||
mb={15}
|
||||
>
|
||||
Kembali
|
||||
</Button>
|
||||
|
||||
{/* Card Utama */}
|
||||
<Paper
|
||||
withBorder
|
||||
w={{ base: "100%", md: "60%" }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
|
||||
Detail Desa Digital Smart Village
|
||||
</Text>
|
||||
|
||||
{/* Sub Card Detail */}
|
||||
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
|
||||
<Stack gap="sm">
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Judul</Text>
|
||||
<Text fz="md" c="dimmed">{data?.name || '-'}</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Deskripsi</Text>
|
||||
<Text
|
||||
fz="md"
|
||||
c="dimmed"
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
dangerouslySetInnerHTML={{ __html: data?.deskripsi || '-' }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Gambar</Text>
|
||||
{data?.image?.link ? (
|
||||
<Image
|
||||
src={data.image.link}
|
||||
alt={data.name || 'Gambar Desa Digital'}
|
||||
w={200}
|
||||
h={200}
|
||||
radius="md"
|
||||
fit="cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
) : (
|
||||
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Tombol Aksi */}
|
||||
<Group gap="sm">
|
||||
<Tooltip label="Hapus Data" withArrow position="top">
|
||||
<Button
|
||||
color="red"
|
||||
onClick={() => {
|
||||
if (stateDesaDigital.findUnique.data) {
|
||||
setSelectedId(stateDesaDigital.findUnique.data.id);
|
||||
setModalHapus(true);
|
||||
}
|
||||
setSelectedId(data.id);
|
||||
setModalHapus(true);
|
||||
}}
|
||||
disabled={stateDesaDigital.delete.loading || !stateDesaDigital.findUnique.data}
|
||||
color={"red"}
|
||||
variant="light"
|
||||
radius="md"
|
||||
size="md"
|
||||
>
|
||||
<IconX size={20} />
|
||||
<IconTrash size={20} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label="Edit Data" withArrow position="top">
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (stateDesaDigital.findUnique.data) {
|
||||
router.push(`/admin/inovasi/desa-digital-smart-village/${stateDesaDigital.findUnique.data.id}/edit`);
|
||||
}
|
||||
}}
|
||||
disabled={!stateDesaDigital.findUnique.data}
|
||||
color={"green"}
|
||||
color="green"
|
||||
onClick={() => router.push(`/admin/inovasi/desa-digital-smart-village/${data.id}/edit`)}
|
||||
variant="light"
|
||||
radius="md"
|
||||
size="md"
|
||||
>
|
||||
<IconEdit size={20} />
|
||||
</Button>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</Paper>
|
||||
) : null}
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Modal Konfirmasi Hapus */}
|
||||
{/* Modal Konfirmasi */}
|
||||
<ModalKonfirmasiHapus
|
||||
opened={modalHapus}
|
||||
onClose={() => setModalHapus(false)}
|
||||
onConfirm={handleHapus}
|
||||
text='Apakah anda yakin ingin menghapus desa digital smart village ini?'
|
||||
text="Apakah Anda yakin ingin menghapus desa digital smart village ini?"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -22,7 +22,7 @@ import CreateEditor from '../../../_com/createEditor';
|
||||
import desaDigitalState from '../../../_state/inovasi/desa-digital';
|
||||
import { Dropzone } from '@mantine/dropzone';
|
||||
|
||||
function CreateDesaDigital() {
|
||||
export default function CreateDesaDigital() {
|
||||
const stateDesaDigital = useProxy(desaDigitalState);
|
||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
@@ -44,7 +44,6 @@ function CreateDesaDigital() {
|
||||
}
|
||||
|
||||
try {
|
||||
// Upload gambar dulu
|
||||
const uploadRes = await ApiFetch.api.fileStorage.create.post({
|
||||
file,
|
||||
name: file.name,
|
||||
@@ -55,10 +54,8 @@ function CreateDesaDigital() {
|
||||
return toast.error('Gagal mengunggah gambar');
|
||||
}
|
||||
|
||||
// Set imageId ke form
|
||||
stateDesaDigital.create.form.imageId = uploaded.id;
|
||||
|
||||
// Submit form
|
||||
const success = await stateDesaDigital.create.create();
|
||||
if (success) {
|
||||
resetForm();
|
||||
@@ -72,10 +69,16 @@ function CreateDesaDigital() {
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
{/* Header */}
|
||||
<Group mb="md">
|
||||
{/* Header dengan tombol kembali */}
|
||||
<Group mb="md" align="center">
|
||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
p="xs"
|
||||
radius="md"
|
||||
style={{ transition: 'background 0.2s ease' }}
|
||||
>
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
@@ -84,28 +87,32 @@ function CreateDesaDigital() {
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
{/* Card */}
|
||||
{/* Card Form */}
|
||||
<Paper
|
||||
w={{ base: '100%', md: '60%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
p={{ base: 'md', md: 'xl' }}
|
||||
radius="lg"
|
||||
shadow="md"
|
||||
style={{
|
||||
border: '1px solid #eaeaea',
|
||||
transition: 'box-shadow 0.3s ease',
|
||||
}}
|
||||
>
|
||||
<Stack gap="md">
|
||||
{/* Nama */}
|
||||
<Stack gap="lg">
|
||||
{/* Input Nama */}
|
||||
<TextInput
|
||||
label="Nama Desa Digital Smart Village"
|
||||
placeholder="Masukkan nama desa digital smart village"
|
||||
label="Nama Desa Digital"
|
||||
placeholder="Masukkan nama desa digital"
|
||||
defaultValue={stateDesaDigital.create.form.name}
|
||||
onChange={(e) => (stateDesaDigital.create.form.name = e.target.value)}
|
||||
required
|
||||
radius="md"
|
||||
withAsterisk
|
||||
/>
|
||||
|
||||
{/* Deskripsi */}
|
||||
<Box>
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
<Text fw={500} fz="sm" mb={6}>
|
||||
Deskripsi
|
||||
</Text>
|
||||
<CreateEditor
|
||||
@@ -118,8 +125,8 @@ function CreateDesaDigital() {
|
||||
|
||||
{/* Upload Gambar */}
|
||||
<Box>
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
Gambar
|
||||
<Text fw={500} fz="sm" mb={6}>
|
||||
Gambar Desa Digital
|
||||
</Text>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
@@ -134,6 +141,11 @@ function CreateDesaDigital() {
|
||||
accept={{ 'image/*': [] }}
|
||||
radius="md"
|
||||
p="xl"
|
||||
style={{
|
||||
border: '2px dashed #cfd8dc',
|
||||
backgroundColor: '#fafafa',
|
||||
transition: 'background-color 0.2s ease, border-color 0.2s ease',
|
||||
}}
|
||||
>
|
||||
<Group justify="center" gap="xl" mih={180}>
|
||||
<Dropzone.Accept>
|
||||
@@ -153,15 +165,22 @@ function CreateDesaDigital() {
|
||||
|
||||
{/* Preview */}
|
||||
{previewImage && (
|
||||
<Box mt="sm" style={{ textAlign: 'center' }}>
|
||||
<Box
|
||||
mt="sm"
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={previewImage}
|
||||
alt="Preview"
|
||||
radius="md"
|
||||
style={{
|
||||
maxHeight: 200,
|
||||
objectFit: 'contain',
|
||||
border: '1px solid #ddd',
|
||||
maxHeight: 220,
|
||||
objectFit: 'cover',
|
||||
border: '1px solid #e0e0e0',
|
||||
}}
|
||||
loading="lazy"
|
||||
/>
|
||||
@@ -170,7 +189,7 @@ function CreateDesaDigital() {
|
||||
</Box>
|
||||
|
||||
{/* Tombol Submit */}
|
||||
<Group justify="right">
|
||||
<Group justify="flex-end" mt="sm">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
@@ -179,6 +198,17 @@ function CreateDesaDigital() {
|
||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||
transition: 'transform 0.2s ease, box-shadow 0.2s ease',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
(e.currentTarget.style.transform = 'translateY(-2px)');
|
||||
(e.currentTarget.style.boxShadow =
|
||||
'0 6px 20px rgba(79, 172, 254, 0.5)');
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
(e.currentTarget.style.transform = 'translateY(0)');
|
||||
(e.currentTarget.style.boxShadow =
|
||||
'0 4px 15px rgba(79, 172, 254, 0.4)');
|
||||
}}
|
||||
>
|
||||
Simpan
|
||||
@@ -189,5 +219,3 @@ function CreateDesaDigital() {
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateDesaDigital;
|
||||
|
||||
@@ -55,7 +55,7 @@ function DetailInfoTeknologiTepatGuna() {
|
||||
<Paper
|
||||
withBorder
|
||||
w={{ base: "100%", md: "70%", lg: "60%" }}
|
||||
bg={colors['white-1']}
|
||||
bg="#ECEEF8"
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
'use client'
|
||||
'use client';
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
||||
import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Paper,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
@@ -15,13 +25,11 @@ function EditJenisLayanan() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
|
||||
// state lokal untuk form
|
||||
const [formData, setFormData] = useState({
|
||||
nama: "",
|
||||
deskripsi: "",
|
||||
nama: '',
|
||||
deskripsi: '',
|
||||
});
|
||||
|
||||
// load data dari backend ke local state
|
||||
useEffect(() => {
|
||||
const loadJenisLayanan = async () => {
|
||||
const id = params?.id as string;
|
||||
@@ -31,20 +39,19 @@ function EditJenisLayanan() {
|
||||
const data = await state.edit.load(id);
|
||||
if (data) {
|
||||
setFormData({
|
||||
nama: data.nama ?? "",
|
||||
deskripsi: data.deskripsi ?? "",
|
||||
nama: data.nama ?? '',
|
||||
deskripsi: data.deskripsi ?? '',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading jenis layanan:", error);
|
||||
toast.error("Gagal memuat data jenis layanan");
|
||||
console.error('Error loading jenis layanan:', error);
|
||||
toast.error('Gagal memuat data jenis layanan');
|
||||
}
|
||||
};
|
||||
|
||||
loadJenisLayanan();
|
||||
}, [params?.id]);
|
||||
|
||||
// submit update → baru sync ke global state
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
state.edit.form = {
|
||||
@@ -53,48 +60,85 @@ function EditJenisLayanan() {
|
||||
};
|
||||
|
||||
await state.edit.update();
|
||||
toast.success("Jenis layanan berhasil diperbarui!");
|
||||
router.push("/admin/inovasi/layanan-online-desa/jenis-layanan");
|
||||
toast.success('Jenis layanan berhasil diperbarui!');
|
||||
router.push('/admin/inovasi/layanan-online-desa/jenis-layanan');
|
||||
} catch (error) {
|
||||
console.error("Error updating jenis layanan:", error);
|
||||
toast.error("Terjadi kesalahan saat memperbarui jenis layanan");
|
||||
console.error('Error updating jenis layanan:', error);
|
||||
toast.error('Terjadi kesalahan saat memperbarui jenis layanan');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button variant="subtle" onClick={() => router.back()}>
|
||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Paper bg={colors["white-1"]} p="md" w={{ base: "100%", md: "50%" }}>
|
||||
<Stack gap="xs">
|
||||
<Title order={3}>Edit Jenis Layanan</Title>
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
{/* Header */}
|
||||
<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>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Edit Jenis Layanan
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
{/* Form Container */}
|
||||
<Paper
|
||||
w={{ base: '100%', md: '60%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
{/* Input: Nama Jenis Layanan */}
|
||||
<TextInput
|
||||
label="Nama Jenis Layanan"
|
||||
placeholder="Masukkan nama jenis layanan"
|
||||
value={formData.nama}
|
||||
onChange={(e) =>
|
||||
setFormData((prev) => ({ ...prev, nama: e.target.value }))
|
||||
}
|
||||
label={<Text fz="sm" fw="bold">Nama Jenis Layanan</Text>}
|
||||
placeholder="masukkan nama jenis layanan"
|
||||
required
|
||||
/>
|
||||
|
||||
{/* Input: Deskripsi (Rich Text Editor) */}
|
||||
<Box>
|
||||
<Text fz="sm" fw="bold">Deskripsi</Text>
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
Deskripsi
|
||||
</Text>
|
||||
<EditEditor
|
||||
value={formData.deskripsi}
|
||||
onChange={(htmlContent) =>
|
||||
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }))
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
deskripsi: htmlContent,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>
|
||||
Simpan
|
||||
</Button>
|
||||
{/* Tombol Submit */}
|
||||
<Group justify="right" mt="sm">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
size="md"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||
}}
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
@@ -150,13 +150,13 @@ export default function EditKegiatanDesa() {
|
||||
onChange={(e) => setFormData({ ...formData, judul: e.target.value })}
|
||||
required
|
||||
/>
|
||||
<TextInput
|
||||
value={formData.deskripsiSingkat}
|
||||
label={<Text fz="sm" fw="bold">Deskripsi Singkat Kegiatan Desa</Text>}
|
||||
placeholder="masukkan deskripsi singkat kegiatan desa"
|
||||
onChange={(e) => setFormData({ ...formData, deskripsiSingkat: e.target.value })}
|
||||
required
|
||||
/>
|
||||
<Box>
|
||||
<Text fw="bold" fz="sm">Deskripsi Singkat Kegiatan Desa</Text>
|
||||
<EditEditor
|
||||
value={formData.deskripsiSingkat}
|
||||
onChange={(htmlContent) => setFormData(prev => ({ ...prev, deskripsiSingkat: htmlContent }))}
|
||||
/>
|
||||
</Box>
|
||||
<Select
|
||||
label="Kategori Kegiatan"
|
||||
data={gotongRoyongState.kategoriKegiatan.findMany.data?.map(k => ({
|
||||
|
||||
@@ -85,7 +85,7 @@ function DetailKegiatanDesa() {
|
||||
{/* Deskripsi Singkat */}
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Deskripsi Singkat</Text>
|
||||
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }}>{data.deskripsiSingkat || '-'}</Text>
|
||||
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.deskripsiSingkat || '-' }} />
|
||||
</Box>
|
||||
|
||||
{/* Deskripsi Lengkap */}
|
||||
|
||||
@@ -159,13 +159,15 @@ function CreateKegiatanDesa() {
|
||||
onChange={(e) => (stateKegiatanDesa.create.form.judul = e.target.value)}
|
||||
required
|
||||
/>
|
||||
<TextInput
|
||||
label="Deskripsi Singkat"
|
||||
placeholder="Masukkan deskripsi singkat"
|
||||
defaultValue={stateKegiatanDesa.create.form.deskripsiSingkat}
|
||||
onChange={(e) => (stateKegiatanDesa.create.form.deskripsiSingkat = e.target.value)}
|
||||
required
|
||||
/>
|
||||
<Box>
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
Deskripsi Singkat
|
||||
</Text>
|
||||
<CreateEditor
|
||||
value={stateKegiatanDesa.create.form.deskripsiSingkat}
|
||||
onChange={(val) => (stateKegiatanDesa.create.form.deskripsiSingkat = val)}
|
||||
/>
|
||||
</Box>
|
||||
<TextInput
|
||||
type="number"
|
||||
min={0}
|
||||
|
||||
@@ -1,11 +1,36 @@
|
||||
import LayoutTabs from "./(dashboard)/landing-page/profile/_lib/layoutTabs";
|
||||
import ProgramInovasi from "./(dashboard)/landing-page/profile/program-inovasi/page";
|
||||
'use client';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
// Dynamically import the components with SSR disabled to prevent hydration issues
|
||||
const LayoutTabs = dynamic(
|
||||
() => import('./(dashboard)/landing-page/profile/_lib/layoutTabs'),
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
const ProgramInovasi = dynamic(
|
||||
() => import('./(dashboard)/landing-page/profile/program-inovasi/page'),
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
export default function Page() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
// This ensures the component is only rendered on the client
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) {
|
||||
return null; // or return a loading state
|
||||
}
|
||||
|
||||
return (
|
||||
<LayoutTabs>
|
||||
<ProgramInovasi />
|
||||
</LayoutTabs>
|
||||
<div suppressHydrationWarning>
|
||||
<LayoutTabs>
|
||||
<ProgramInovasi />
|
||||
</LayoutTabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -9,7 +9,7 @@ export default async function KegiatanDesaFindFirst(context: Context) {
|
||||
|
||||
if (kategori) {
|
||||
where.kategoriKegiatan = {
|
||||
name: { equals: kategori, mode: 'insensitive' }
|
||||
nama: { equals: kategori, mode: 'insensitive' }
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -135,7 +135,7 @@ export default function Content({ kategori }: { kategori: string }) {
|
||||
{item.kategoriBerita?.name || kategori}
|
||||
</Badge>
|
||||
<Text fw={600} size="lg" mt="sm" lineClamp={2}>{item.judul}</Text>
|
||||
<Text size="sm" c="dimmed" lineClamp={3} mt="xs" dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||
<Text size="sm" c="dimmed" lineClamp={3} style={{wordBreak: "break-word", whiteSpace: "normal"}} mt="xs" dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||
<Group justify="apart" mt="md" gap="xs">
|
||||
<Text size="xs" c="dimmed">
|
||||
{new Date(item.createdAt).toLocaleDateString('id-ID', {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// src/app/darmasaba/(pages)/desa/berita/[kategori]/page.tsx
|
||||
import { Suspense } from "react";
|
||||
import Content from "./Content";
|
||||
|
||||
|
||||
@@ -1,24 +1,300 @@
|
||||
import colors from '@/con/colors';
|
||||
import { Stack, Box, Text, Image } from '@mantine/core';
|
||||
import React from 'react';
|
||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||
/* 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 {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Center,
|
||||
Container,
|
||||
Group,
|
||||
Image,
|
||||
Loader,
|
||||
Paper,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
Tooltip,
|
||||
Transition,
|
||||
} from '@mantine/core'
|
||||
import { IconRefresh, IconSearch, IconUsers } from '@tabler/icons-react'
|
||||
import { OrganizationChart } from 'primereact/organizationchart'
|
||||
import { useEffect } from 'react'
|
||||
import { useProxy } from 'valtio/utils'
|
||||
import BackButton from '../../desa/layanan/_com/BackButto'
|
||||
|
||||
function Page() {
|
||||
export default function Page() {
|
||||
return (
|
||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
<Text px={{ base: 'md', md: 100 }} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
|
||||
Struktur Organisasi dan SK Pengurus BUMDesa
|
||||
</Text>
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Stack gap={'lg'} justify='center'>
|
||||
<Image src={'/api/img/bpddarmasaba.png'} alt='' loading="lazy"/>
|
||||
<Box
|
||||
style={{
|
||||
minHeight: '100vh',
|
||||
background: colors['Bg'],
|
||||
color: '#E6F0FF',
|
||||
paddingBottom: 48,
|
||||
}}
|
||||
>
|
||||
<Container size="xl" py="xl">
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
<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
|
||||
</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>
|
||||
</Stack>
|
||||
);
|
||||
<Box mt="lg">
|
||||
<StrukturOrganisasiBumDes />
|
||||
</Box>
|
||||
</Container>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default Page;
|
||||
function StrukturOrganisasiBumDes() {
|
||||
const stateOrganisasi: any = useProxy(stateStrukturBumDes.pegawai)
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
!stateOrganisasi.findMany.data ||
|
||||
stateOrganisasi.findMany.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 BumDes. Silakan coba
|
||||
muat ulang atau periksa sumber data.
|
||||
</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>
|
||||
<Button
|
||||
leftSection={<IconSearch size={16} />}
|
||||
variant="subtle"
|
||||
onClick={() =>
|
||||
stateOrganisasi.findMany.load({ query: { q: '' } })
|
||||
}
|
||||
>
|
||||
Cari Pegawai
|
||||
</Button>
|
||||
</Group>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Center>
|
||||
)
|
||||
}
|
||||
|
||||
const posisiMap = new Map<string, any>()
|
||||
|
||||
const aktifPegawai = stateOrganisasi.findMany.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);
|
||||
}
|
||||
|
||||
// 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: []
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 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 {
|
||||
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',
|
||||
description: node.deskripsi || '',
|
||||
positionId: node.id || null,
|
||||
},
|
||||
children: node.children?.map(toOrgChartFormat) || [],
|
||||
}
|
||||
}
|
||||
|
||||
const chartData = root.map(toOrgChartFormat)
|
||||
|
||||
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}
|
||||
/>
|
||||
</Paper>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function nodeTemplate(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}>
|
||||
{(styles) => (
|
||||
<Card
|
||||
radius="lg"
|
||||
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',
|
||||
}}
|
||||
>
|
||||
<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' })
|
||||
}
|
||||
}}
|
||||
>
|
||||
Kembali
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Card>
|
||||
)}
|
||||
</Transition>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ function Page() {
|
||||
|
||||
useShallowEffect(() => {
|
||||
mitraState.findMany.load(page, 10);
|
||||
load(page, 10, search, selectedYear || '');
|
||||
load(page, 10, search, selectedYear ? `year:${selectedYear}` : '');
|
||||
}, [page, search, selectedYear]);
|
||||
|
||||
const mitraData = mitraState.findMany.data || [];
|
||||
|
||||
@@ -1,48 +1,31 @@
|
||||
'use client'
|
||||
import stateEdukasiLingkungan from '@/app/admin/(dashboard)/_state/lingkungan/edukasi-lingkungan';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, List, ListItem, Paper, SimpleGrid, Stack, Text, Tooltip } from '@mantine/core';
|
||||
import { Box, Paper, SimpleGrid, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconLeaf, IconPlant2, IconRecycle } from '@tabler/icons-react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||
|
||||
const data = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Tujuan Edukasi Lingkungan',
|
||||
icon: <IconLeaf size={28} color={colors['blue-button']} />,
|
||||
listDeskripsi: [
|
||||
'Meningkatkan kesadaran masyarakat akan pentingnya lingkungan bersih dan sehat',
|
||||
'Mendorong partisipasi warga dalam pengelolaan sampah, penghijauan, dan konservasi',
|
||||
'Mengurangi dampak negatif kegiatan manusia terhadap lingkungan',
|
||||
'Membentuk generasi muda peduli isu-isu lingkungan',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Materi Edukasi yang Diberikan',
|
||||
icon: <IconRecycle size={28} color={colors['blue-button']} />,
|
||||
listDeskripsi: [
|
||||
'Pengelolaan sampah: pilah organik & anorganik',
|
||||
'Pencegahan pencemaran lingkungan (air, udara, tanah)',
|
||||
'Pemanfaatan lahan hijau dan penghijauan desa',
|
||||
'Daur ulang dan kreativitas dari sampah',
|
||||
'Bahaya pembakaran sampah sembarangan',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Contoh Kegiatan di Desa Darmasaba',
|
||||
icon: <IconPlant2 size={28} color={colors['blue-button']} />,
|
||||
listDeskripsi: [
|
||||
'Pelatihan membuat kompos dari sampah rumah tangga',
|
||||
'Gerakan "Jumat Bersih" rutin',
|
||||
'Workshop pembuatan ecobrick',
|
||||
'Lomba kebersihan antar banjar',
|
||||
'Sosialisasi lingkungan di sekolah dan posyandu',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
function Page() {
|
||||
const tujuan = useProxy(stateEdukasiLingkungan.stateTujuanEdukasi.findById)
|
||||
const materi = useProxy(stateEdukasiLingkungan.stateMateriEdukasiLingkungan.findById)
|
||||
const contoh = useProxy(stateEdukasiLingkungan.stateContohEdukasiLingkungan.findById)
|
||||
|
||||
useShallowEffect(() => {
|
||||
tujuan.load('edit')
|
||||
materi.load('edit')
|
||||
contoh.load('edit')
|
||||
}, [])
|
||||
|
||||
if (tujuan.loading || !tujuan.data || materi.loading || !materi.data || contoh.loading || !contoh.data) {
|
||||
return (
|
||||
<Stack py={20}>
|
||||
<Skeleton radius="md" height={600} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
@@ -60,28 +43,84 @@ function Page() {
|
||||
</Box>
|
||||
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg">
|
||||
{data.map((item) => (
|
||||
<Paper key={item.id} p={20} bg={colors['white-trans-1']} shadow="md" radius="md">
|
||||
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg" style={{ alignItems: 'stretch' }}>
|
||||
{/* Tujuan Edukasi Lingkungan */}
|
||||
<Box style={{ display: 'flex', height: '100%' }}>
|
||||
<Paper p={20} bg={colors['white-trans-1']} shadow="md" radius="md" style={{ width: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<Stack gap="md">
|
||||
<Box>
|
||||
<Tooltip label={item.title} position="top" withArrow>
|
||||
<Tooltip label={tujuan.data?.judul} position="top" withArrow>
|
||||
<Stack gap={4} align="center">
|
||||
{item.icon}
|
||||
<IconLeaf size={28} color={colors['blue-button']} />
|
||||
<Text fz="h3" fw="bold" c={colors['blue-button']} ta="center">
|
||||
{item.title}
|
||||
{tujuan.data?.judul}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<List fz="h4" spacing="sm" withPadding>
|
||||
{item.listDeskripsi.map((desc, idx) => (
|
||||
<ListItem key={idx}>{desc}</ListItem>
|
||||
))}
|
||||
</List>
|
||||
<Text
|
||||
style={{
|
||||
wordBreak: "break-word",
|
||||
whiteSpace: "normal",
|
||||
flexGrow: 1
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: tujuan.data?.deskripsi || '' }}
|
||||
/>
|
||||
<Box style={{ flexGrow: 1 }} />
|
||||
</Stack>
|
||||
</Paper>
|
||||
))}
|
||||
</Box>
|
||||
{/* Materi Edukasi Lingkungan */}
|
||||
<Box style={{ display: 'flex', height: '100%' }}>
|
||||
<Paper p={20} bg={colors['white-trans-1']} shadow="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<Box>
|
||||
<Tooltip label={materi.data?.judul} position="top" withArrow>
|
||||
<Stack gap={4} align="center">
|
||||
<IconRecycle size={28} color={colors['blue-button']} />
|
||||
<Text fz="h3" fw="bold" c={colors['blue-button']} ta="center">
|
||||
{materi.data?.judul}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Text
|
||||
style={{
|
||||
wordBreak: "break-word",
|
||||
whiteSpace: "normal",
|
||||
flexGrow: 1
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: materi.data?.deskripsi || '' }}
|
||||
/>
|
||||
<Box style={{ flexGrow: 1 }} />
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
{/* Contoh Edukasi Lingkungan */}
|
||||
<Box style={{ display: 'flex', height: '100%' }}>
|
||||
<Paper p={20} bg={colors['white-trans-1']} shadow="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<Box>
|
||||
<Tooltip label={contoh.data?.judul} position="top" withArrow>
|
||||
<Stack gap={4} align="center">
|
||||
<IconPlant2 size={28} color={colors['blue-button']} />
|
||||
<Text fz="h3" fw="bold" c={colors['blue-button']} ta="center">
|
||||
{contoh.data?.judul}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Text
|
||||
style={{
|
||||
wordBreak: "break-word",
|
||||
whiteSpace: "normal",
|
||||
flexGrow: 1
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: contoh.data?.deskripsi || '' }}
|
||||
/>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
@@ -75,7 +75,7 @@ export default function Content({ kategori }: { kategori: string }) {
|
||||
{featured.kategoriKegiatan?.nama || kategori}
|
||||
</Badge>
|
||||
<Title order={2} mb="md">{featured.judul}</Title>
|
||||
<Text color="dimmed" lineClamp={3} mb="md">{featured.deskripsiLengkap}</Text>
|
||||
<Text c="dimmed" lineClamp={3} mb="md" dangerouslySetInnerHTML={{ __html: featured.deskripsiLengkap }} />
|
||||
</div>
|
||||
<Group justify="apart" mt="auto">
|
||||
<Group gap="xs">
|
||||
@@ -135,9 +135,9 @@ export default function Content({ kategori }: { kategori: string }) {
|
||||
{item.kategoriKegiatan?.nama || kategori}
|
||||
</Badge>
|
||||
<Text fw={600} size="lg" mt="sm" lineClamp={2}>{item.judul}</Text>
|
||||
<Text size="sm" color="dimmed" lineClamp={3} style={{wordBreak: "break-word", whiteSpace: "normal"}} mt="xs" dangerouslySetInnerHTML={{ __html: item.deskripsiLengkap }} />
|
||||
<Text size="sm" c="dimmed" lineClamp={3} style={{wordBreak: "break-word", whiteSpace: "normal"}} mt="xs" dangerouslySetInnerHTML={{ __html: item.deskripsiLengkap }} />
|
||||
<Group justify="apart" mt="md" gap="xs">
|
||||
<Text size="xs" color="dimmed">
|
||||
<Text size="xs" c="dimmed">
|
||||
{new Date(item.createdAt).toLocaleDateString('id-ID', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// src/app/darmasaba/(pages)/desa/berita/[kategori]/page.tsx
|
||||
import { Suspense } from "react";
|
||||
import Content from "./content";
|
||||
|
||||
|
||||
@@ -1,113 +1,391 @@
|
||||
// 'use client'
|
||||
// import colors from '@/con/colors';
|
||||
// import { Box, Container, Grid, GridCol, Stack, Tabs, TabsList, TabsTab, Text, TextInput } from '@mantine/core';
|
||||
// import { IconSearch } from '@tabler/icons-react';
|
||||
// import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||||
// import React, { useEffect, useState } from 'react';
|
||||
// import BackButton from '../../../desa/layanan/_com/BackButto';
|
||||
|
||||
|
||||
// type HeaderSearchProps = {
|
||||
// placeholder?: string;
|
||||
// searchIcon?: React.ReactNode;
|
||||
// value?: string;
|
||||
// onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
// children?: React.ReactNode;
|
||||
// };
|
||||
|
||||
// function LayoutTabsGotongRoyong({
|
||||
// children,
|
||||
// placeholder = "pencarian",
|
||||
// searchIcon = <IconSearch size={20} />
|
||||
// }: HeaderSearchProps) {
|
||||
// const router = useRouter();
|
||||
// const pathname = usePathname();
|
||||
// const searchParams = useSearchParams();
|
||||
|
||||
// // Get active tab from URL path
|
||||
// const activeTab = pathname.split('/').pop() || 'semua';
|
||||
|
||||
// // Get initial search value from URL
|
||||
// const initialSearch = searchParams.get('search') || '';
|
||||
// const [searchValue, setSearchValue] = useState(initialSearch);
|
||||
// const [searchTimeout, setSearchTimeout] = useState<number | null>(null);
|
||||
|
||||
// // Update active tab state when pathname changes
|
||||
// const [activeTabState, setActiveTabState] = useState(activeTab);
|
||||
// useEffect(() => {
|
||||
// setActiveTabState(activeTab);
|
||||
// }, [activeTab]);
|
||||
|
||||
// // Clean up timeouts on unmount
|
||||
// useEffect(() => {
|
||||
// return () => {
|
||||
// if (searchTimeout !== null) {
|
||||
// clearTimeout(searchTimeout);
|
||||
// }
|
||||
// };
|
||||
// }, [searchTimeout]);
|
||||
|
||||
// // Handle search input change with debounce
|
||||
// const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
// const value = event.target.value;
|
||||
// setSearchValue(value);
|
||||
|
||||
// // Clear previous timeout
|
||||
// if (searchTimeout !== null) {
|
||||
// clearTimeout(searchTimeout);
|
||||
// }
|
||||
|
||||
// // Set new timeout
|
||||
// const newTimeout = window.setTimeout(() => {
|
||||
// const params = new URLSearchParams(searchParams.toString());
|
||||
|
||||
// if (value) {
|
||||
// params.set('search', value);
|
||||
// } else {
|
||||
// params.delete('search');
|
||||
// }
|
||||
|
||||
// // Only update URL if the search value has actually changed
|
||||
// if (params.toString() !== searchParams.toString()) {
|
||||
// router.push(`/darmasaba/lingkungan/gotong-royong/${activeTab}?${params.toString()}`);
|
||||
// }
|
||||
// }, 500); // 500ms debounce delay
|
||||
|
||||
// setSearchTimeout(newTimeout);
|
||||
// };
|
||||
// const tabs = [
|
||||
// {
|
||||
// label: "Semua",
|
||||
// value: "semua",
|
||||
// href: "/darmasaba/lingkungan/gotong-royong/semua"
|
||||
// },
|
||||
// {
|
||||
// label: "Kebersihan",
|
||||
// value: "kebersihan",
|
||||
// href: "/darmasaba/lingkungan/gotong-royong/kebersihan"
|
||||
// },
|
||||
// {
|
||||
// label: "Infrastruktur",
|
||||
// value: "infrastruktur",
|
||||
// href: "/darmasaba/lingkungan/gotong-royong/infrastruktur"
|
||||
// },
|
||||
// {
|
||||
// label: "Sosial",
|
||||
// value: "sosial",
|
||||
// href: "/darmasaba/lingkungan/gotong-royong/sosial"
|
||||
// },
|
||||
// {
|
||||
// label: "Lingkungan",
|
||||
// value: "lingkungan",
|
||||
// href: "/darmasaba/lingkungan/gotong-royong/lingkungan"
|
||||
// }
|
||||
// ];
|
||||
// const handleTabChange = (value: string | null) => {
|
||||
// if (!value) return;
|
||||
// const tab = tabs.find(t => t.value === value);
|
||||
// if (tab) {
|
||||
// const params = new URLSearchParams(searchParams.toString());
|
||||
// router.push(`/darmasaba/lingkungan/gotong-royong/${value}${params.toString() ? `?${params.toString()}` : ''}`);
|
||||
// }
|
||||
// };
|
||||
|
||||
// return (
|
||||
// <Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
|
||||
// {/* Header */}
|
||||
// <Box px={{ base: "md", md: 100 }}>
|
||||
// <BackButton />
|
||||
// </Box>
|
||||
// <Container size="lg" px="md">
|
||||
// <Stack align="center" gap="0" >
|
||||
// <Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" ta="center">
|
||||
// Gotong Royong Desa Darmasaba
|
||||
// </Text>
|
||||
// <Text ta="center" px="md">
|
||||
// Gotong royong rutin dilakukan oleh warga desa untuk meningkatkan kualitas hidup dan kesejahteraan masyarakat Desa Darmasaba
|
||||
// </Text>
|
||||
// </Stack>
|
||||
// </Container>
|
||||
|
||||
// <Tabs
|
||||
// color={colors['blue-button']}
|
||||
// variant="pills"
|
||||
// value={activeTabState}
|
||||
// onChange={handleTabChange}
|
||||
// >
|
||||
// <Box px={{ base: "md", md: 100 }} py="md" bg={colors['BG-trans']}>
|
||||
// <Grid>
|
||||
// <GridCol span={{ base: 12, md: 9, lg: 8, xl: 9 }}>
|
||||
// <TabsList>
|
||||
// {tabs.map((tab, index) => (
|
||||
// <TabsTab
|
||||
// key={index}
|
||||
// value={tab.value}
|
||||
// onClick={() => router.push(tab.href)}
|
||||
// >
|
||||
// {tab.label}
|
||||
// </TabsTab>
|
||||
// ))}
|
||||
// </TabsList>
|
||||
// </GridCol>
|
||||
// <GridCol span={{ base: 12, md: 3, lg: 4, xl: 3 }}>
|
||||
// <TextInput
|
||||
// radius="lg"
|
||||
// placeholder={placeholder}
|
||||
// leftSection={searchIcon}
|
||||
// w="100%"
|
||||
// value={searchValue}
|
||||
// onChange={handleSearchChange}
|
||||
// />
|
||||
// </GridCol>
|
||||
// </Grid>
|
||||
// </Box>
|
||||
|
||||
// {children}
|
||||
// </Tabs>
|
||||
// </Stack>
|
||||
// );
|
||||
// }
|
||||
|
||||
// export default LayoutTabsGotongRoyong;
|
||||
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
// 'use client'
|
||||
// import colors from '@/con/colors';
|
||||
// import { Box, Group, Stack, Tabs, TabsList, TabsTab, Text, TextInput } from '@mantine/core';
|
||||
// import { IconSearch } from '@tabler/icons-react';
|
||||
// import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||||
// import React, { useEffect, useState } from 'react';
|
||||
// import BackButton from '../../layanan/_com/BackButto';
|
||||
|
||||
// type HeaderSearchProps = {
|
||||
// placeholder?: string;
|
||||
// searchIcon?: React.ReactNode;
|
||||
// value?: string;
|
||||
// onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
// children?: React.ReactNode;
|
||||
// };
|
||||
|
||||
// function LayoutTabsBerita({
|
||||
// children,
|
||||
// placeholder = "pencarian",
|
||||
// searchIcon = <IconSearch size={20} />
|
||||
// }: HeaderSearchProps) {
|
||||
// const router = useRouter();
|
||||
// const pathname = usePathname();
|
||||
// const searchParams = useSearchParams();
|
||||
|
||||
// const activeTab = pathname.split('/').pop() || 'semua';
|
||||
// const initialSearch = searchParams.get('search') || '';
|
||||
// const [searchValue, setSearchValue] = useState(initialSearch);
|
||||
// const [searchTimeout, setSearchTimeout] = useState<number | null>(null);
|
||||
|
||||
// const [activeTabState, setActiveTabState] = useState(activeTab);
|
||||
// useEffect(() => {
|
||||
// setActiveTabState(activeTab);
|
||||
// }, [activeTab]);
|
||||
|
||||
// useEffect(() => {
|
||||
// return () => {
|
||||
// if (searchTimeout !== null) clearTimeout(searchTimeout);
|
||||
// };
|
||||
// }, [searchTimeout]);
|
||||
|
||||
// const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
// const value = event.target.value;
|
||||
// setSearchValue(value);
|
||||
|
||||
// if (searchTimeout !== null) clearTimeout(searchTimeout);
|
||||
|
||||
// const newTimeout = window.setTimeout(() => {
|
||||
// const params = new URLSearchParams(searchParams.toString());
|
||||
|
||||
// if (value) params.set('search', value);
|
||||
// else params.delete('search');
|
||||
|
||||
// if (params.toString() !== searchParams.toString()) {
|
||||
// router.push(`/darmasaba/desa/berita/${activeTab}?${params.toString()}`);
|
||||
// }
|
||||
// }, 500);
|
||||
|
||||
// setSearchTimeout(newTimeout);
|
||||
// };
|
||||
|
||||
// const tabs = [
|
||||
// { label: "Semua", value: "semua", href: "/darmasaba/desa/berita/semua" },
|
||||
// { label: "Budaya", value: "budaya", href: "/darmasaba/desa/berita/budaya" },
|
||||
// { label: "Pemerintahan", value: "pemerintahan", href: "/darmasaba/desa/berita/pemerintahan" },
|
||||
// { label: "Ekonomi", value: "ekonomi", href: "/darmasaba/desa/berita/ekonomi" },
|
||||
// { label: "Pembangunan", value: "pembangunan", href: "/darmasaba/desa/berita/pembangunan" },
|
||||
// { label: "Sosial", value: "sosial", href: "/darmasaba/desa/berita/sosial" },
|
||||
// { label: "Teknologi", value: "teknologi", href: "/darmasaba/desa/berita/teknologi" },
|
||||
// ];
|
||||
|
||||
// const handleTabChange = (value: string | null) => {
|
||||
// if (!value) return;
|
||||
// const tab = tabs.find(t => t.value === value);
|
||||
// if (tab) {
|
||||
// const params = new URLSearchParams(searchParams.toString());
|
||||
// router.push(`/darmasaba/desa/berita/${value}${params.toString() ? `?${params.toString()}` : ''}`);
|
||||
// }
|
||||
// };
|
||||
|
||||
// return (
|
||||
// <Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
|
||||
// {/* Header */}
|
||||
// <Box px={{ base: "md", md: 100 }}>
|
||||
// <BackButton />
|
||||
// </Box>
|
||||
|
||||
// <Box px={{ base: 'md', md: 100 }}>
|
||||
// <Group justify='space-between' align="center">
|
||||
// <Stack gap="0">
|
||||
// <Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" >
|
||||
// Portal Berita Darmasaba
|
||||
// </Text>
|
||||
// <Text>
|
||||
// Temukan berbagai potensi dan keunggulan yang dimiliki Desa Darmasaba
|
||||
// </Text>
|
||||
// </Stack>
|
||||
// <Box>
|
||||
// <TextInput
|
||||
// radius="lg"
|
||||
// placeholder={placeholder}
|
||||
// leftSection={searchIcon}
|
||||
// w="100%"
|
||||
// value={searchValue}
|
||||
// onChange={handleSearchChange}
|
||||
// />
|
||||
// </Box>
|
||||
// </Group>
|
||||
// </Box>
|
||||
|
||||
// <Tabs
|
||||
// color={colors['blue-button']}
|
||||
// variant="pills"
|
||||
// value={activeTabState}
|
||||
// onChange={handleTabChange}
|
||||
// >
|
||||
// <Box px={{ base: "md", md: 100 }} py="md" bg={colors['BG-trans']}>
|
||||
// {/* SCROLLABLE TABS */}
|
||||
// <Box style={{ overflowX: 'auto', whiteSpace: 'nowrap' }}>
|
||||
// <TabsList style={{ display: 'flex', flexWrap: 'nowrap', gap: '0.5rem' }}>
|
||||
// {tabs.map((tab, index) => (
|
||||
// <TabsTab
|
||||
// key={index}
|
||||
// value={tab.value}
|
||||
// onClick={() => router.push(tab.href)}
|
||||
// style={{
|
||||
// flex: '0 0 auto', // Prevent shrinking
|
||||
// minWidth: 100, // optional: makes them touch-friendly
|
||||
// textAlign: 'center'
|
||||
// }}
|
||||
// >
|
||||
// {tab.label}
|
||||
// </TabsTab>
|
||||
// ))}
|
||||
// </TabsList>
|
||||
// </Box>
|
||||
// </Box>
|
||||
|
||||
// {children}
|
||||
// </Tabs>
|
||||
// </Stack>
|
||||
// );
|
||||
// }
|
||||
|
||||
// export default LayoutTabsBerita;
|
||||
|
||||
|
||||
'use client'
|
||||
import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Container, Grid, GridCol, Stack, Tabs, TabsList, TabsTab, Text, TextInput } from '@mantine/core';
|
||||
import { Box, Group, Stack, Tabs, TabsList, TabsTab, Text, TextInput } from '@mantine/core';
|
||||
import { IconSearch } from '@tabler/icons-react';
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import BackButton from '../../../desa/layanan/_com/BackButto';
|
||||
|
||||
|
||||
type HeaderSearchProps = {
|
||||
placeholder?: string;
|
||||
searchIcon?: React.ReactNode;
|
||||
value?: string;
|
||||
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
function LayoutTabsGotongRoyong({
|
||||
children,
|
||||
placeholder = "pencarian",
|
||||
searchIcon = <IconSearch size={20} />
|
||||
}: HeaderSearchProps) {
|
||||
function LayoutTabsGotongRoyong({ children }: { children: React.ReactNode }) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
// Get active tab from URL path
|
||||
|
||||
const kategoriState = useProxy(gotongRoyongState.kategoriKegiatan);
|
||||
|
||||
// tab aktif dari url
|
||||
const activeTab = pathname.split('/').pop() || 'semua';
|
||||
|
||||
// Get initial search value from URL
|
||||
const initialSearch = searchParams.get('search') || '';
|
||||
const [searchValue, setSearchValue] = useState(initialSearch);
|
||||
const [searchTimeout, setSearchTimeout] = useState<number | null>(null);
|
||||
|
||||
// Update active tab state when pathname changes
|
||||
const [activeTabState, setActiveTabState] = useState(activeTab);
|
||||
|
||||
useEffect(() => {
|
||||
kategoriState.findMany.load(); // ambil kategori dari DB
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveTabState(activeTab);
|
||||
}, [activeTab]);
|
||||
|
||||
// Clean up timeouts on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (searchTimeout !== null) {
|
||||
clearTimeout(searchTimeout);
|
||||
}
|
||||
};
|
||||
}, [searchTimeout]);
|
||||
// search
|
||||
const initialSearch = searchParams.get('search') || '';
|
||||
const [searchValue, setSearchValue] = useState(initialSearch);
|
||||
const [searchTimeout, setSearchTimeout] = useState<number | null>(null);
|
||||
|
||||
// Handle search input change with debounce
|
||||
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.target.value;
|
||||
setSearchValue(value);
|
||||
|
||||
// Clear previous timeout
|
||||
if (searchTimeout !== null) {
|
||||
clearTimeout(searchTimeout);
|
||||
}
|
||||
|
||||
// Set new timeout
|
||||
|
||||
if (searchTimeout !== null) clearTimeout(searchTimeout);
|
||||
|
||||
const newTimeout = window.setTimeout(() => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
|
||||
if (value) {
|
||||
params.set('search', value);
|
||||
} else {
|
||||
params.delete('search');
|
||||
}
|
||||
|
||||
// Only update URL if the search value has actually changed
|
||||
if (params.toString() !== searchParams.toString()) {
|
||||
router.push(`/darmasaba/lingkungan/gotong-royong/${activeTab}?${params.toString()}`);
|
||||
}
|
||||
}, 500); // 500ms debounce delay
|
||||
|
||||
if (value) params.set('search', value);
|
||||
else params.delete('search');
|
||||
|
||||
router.push(`/darmasaba/lingkungan/gotong-royong/${activeTab}${params.toString() ? `?${params.toString()}` : ''}`);
|
||||
}, 500);
|
||||
|
||||
setSearchTimeout(newTimeout);
|
||||
};
|
||||
|
||||
// --- tabs dinamis ---
|
||||
const tabs = [
|
||||
{
|
||||
label: "Semua",
|
||||
value: "semua",
|
||||
href: "/darmasaba/lingkungan/gotong-royong/semua"
|
||||
},
|
||||
{
|
||||
label: "Kebersihan",
|
||||
value: "kebersihan",
|
||||
href: "/darmasaba/lingkungan/gotong-royong/kebersihan"
|
||||
},
|
||||
{
|
||||
label: "Infrastruktur",
|
||||
value: "infrastruktur",
|
||||
href: "/darmasaba/lingkungan/gotong-royong/infrastruktur"
|
||||
},
|
||||
{
|
||||
label: "Sosial",
|
||||
value: "sosial",
|
||||
href: "/darmasaba/lingkungan/gotong-royong/sosial"
|
||||
},
|
||||
{
|
||||
label: "Lingkungan",
|
||||
value: "lingkungan",
|
||||
href: "/darmasaba/lingkungan/gotong-royong/lingkungan"
|
||||
}
|
||||
{ label: "Semua", value: "semua", href: "/darmasaba/lingkungan/gotong-royong/semua" },
|
||||
...(kategoriState.findMany.data || []).map((kat: any) => ({
|
||||
label: kat.nama,
|
||||
value: kat.nama.toLowerCase(),
|
||||
href: `/darmasaba/lingkungan/gotong-royong/${kat.nama.toLowerCase()}`
|
||||
}))
|
||||
];
|
||||
|
||||
const handleTabChange = (value: string | null) => {
|
||||
if (!value) return;
|
||||
const tab = tabs.find(t => t.value === value);
|
||||
if (tab) {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
router.push(`/darmasaba/lingkungan/gotong-royong/${value}${params.toString() ? `?${params.toString()}` : ''}`);
|
||||
router.push(`${tab.href}${params.toString() ? `?${params.toString()}` : ''}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -117,17 +395,29 @@ function LayoutTabsGotongRoyong({
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
<Container size="lg" px="md">
|
||||
<Stack align="center" gap="0" >
|
||||
<Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" ta="center">
|
||||
Gotong Royong Desa Darmasaba
|
||||
</Text>
|
||||
<Text ta="center" px="md">
|
||||
Gotong royong rutin dilakukan oleh warga desa untuk meningkatkan kualitas hidup dan kesejahteraan masyarakat Desa Darmasaba
|
||||
</Text>
|
||||
</Stack>
|
||||
</Container>
|
||||
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Group justify='space-between' align="center">
|
||||
<Stack gap="0">
|
||||
<Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold">
|
||||
Portal Gotong royong Darmasaba
|
||||
</Text>
|
||||
<Text>Temukan berbagai kegiatan lingkungan yang dimiliki Desa Darmasaba</Text>
|
||||
</Stack>
|
||||
<Box>
|
||||
<TextInput
|
||||
radius="lg"
|
||||
placeholder="pencarian"
|
||||
leftSection={<IconSearch size={20} />}
|
||||
w="100%"
|
||||
value={searchValue}
|
||||
onChange={handleSearchChange}
|
||||
/>
|
||||
</Box>
|
||||
</Group>
|
||||
</Box>
|
||||
|
||||
{/* TABS */}
|
||||
<Tabs
|
||||
color={colors['blue-button']}
|
||||
variant="pills"
|
||||
@@ -135,31 +425,24 @@ function LayoutTabsGotongRoyong({
|
||||
onChange={handleTabChange}
|
||||
>
|
||||
<Box px={{ base: "md", md: 100 }} py="md" bg={colors['BG-trans']}>
|
||||
<Grid>
|
||||
<GridCol span={{ base: 12, md: 9, lg: 8, xl: 9 }}>
|
||||
<TabsList>
|
||||
{tabs.map((tab, index) => (
|
||||
<TabsTab
|
||||
key={index}
|
||||
value={tab.value}
|
||||
onClick={() => router.push(tab.href)}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTab>
|
||||
))}
|
||||
</TabsList>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 3, lg: 4, xl: 3 }}>
|
||||
<TextInput
|
||||
radius="lg"
|
||||
placeholder={placeholder}
|
||||
leftSection={searchIcon}
|
||||
w="100%"
|
||||
value={searchValue}
|
||||
onChange={handleSearchChange}
|
||||
/>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
<Box style={{ overflowX: 'auto', whiteSpace: 'nowrap' }}>
|
||||
<TabsList style={{ display: 'flex', flexWrap: 'nowrap', gap: '0.5rem' }}>
|
||||
{tabs.map((tab, index) => (
|
||||
<TabsTab
|
||||
key={index}
|
||||
value={tab.value}
|
||||
onClick={() => router.push(tab.href)}
|
||||
style={{
|
||||
flex: '0 0 auto',
|
||||
minWidth: 100,
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTab>
|
||||
))}
|
||||
</TabsList>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{children}
|
||||
@@ -168,4 +451,4 @@ function LayoutTabsGotongRoyong({
|
||||
);
|
||||
}
|
||||
|
||||
export default LayoutTabsGotongRoyong;
|
||||
export default LayoutTabsGotongRoyong;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Badge, Box, Button, Card, Center, Container, Divider, Flex, Grid, GridC
|
||||
import { IconArrowRight, IconCalendar } from '@tabler/icons-react';
|
||||
import { useTransitionRouter } from 'next-view-transitions';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
function Page() {
|
||||
@@ -14,8 +14,7 @@ function Page() {
|
||||
|
||||
// Parameter URL
|
||||
const search = searchParams.get('search') || '';
|
||||
const currentPage = parseInt(searchParams.get('page') || '1');
|
||||
const [page, setPage] = useState(currentPage);
|
||||
const page = parseInt(searchParams.get('page') || '1');
|
||||
|
||||
// Gunakan proxy untuk state
|
||||
const state = useProxy(gotongRoyongState.kegiatanDesa);
|
||||
@@ -37,12 +36,14 @@ function Page() {
|
||||
}, [page, search]);
|
||||
|
||||
// Update URL saat page berubah
|
||||
useEffect(() => {
|
||||
const url = new URLSearchParams();
|
||||
const handlePageChange = (newPage: number) => {
|
||||
const url = new URLSearchParams(searchParams.toString());
|
||||
if (search) url.set('search', search);
|
||||
if (page > 1) url.set('page', page.toString());
|
||||
if (newPage > 1) url.set('page', newPage.toString());
|
||||
else url.delete('page'); // biar page=1 ga muncul di URL
|
||||
|
||||
router.replace(`?${url.toString()}`);
|
||||
}, [page, search]);
|
||||
};
|
||||
|
||||
const featuredData = featured.data;
|
||||
const paginatedNews = state.findMany.data || [];
|
||||
@@ -77,9 +78,7 @@ function Page() {
|
||||
{featuredData.kategoriKegiatan?.nama || 'Gotong royong'}
|
||||
</Badge>
|
||||
<Title order={2} mb="md">{featuredData.judul}</Title>
|
||||
<Text c="dimmed" lineClamp={3} mb="md">
|
||||
{featuredData.deskripsiSingkat}
|
||||
</Text>
|
||||
<Text c="dimmed" lineClamp={3} mb="md" dangerouslySetInnerHTML={{ __html: featuredData.deskripsiSingkat }} />
|
||||
</div>
|
||||
<Group justify="apart" mt="auto">
|
||||
<Group gap="xs">
|
||||
@@ -146,7 +145,7 @@ function Page() {
|
||||
|
||||
<Text fw={600} size="lg" mt="sm" lineClamp={2}>{item.judul}</Text>
|
||||
|
||||
<Text size="sm" c="dimmed" lineClamp={3} mt="xs">{item.deskripsiSingkat}</Text>
|
||||
<Text size="sm" c="dimmed" lineClamp={3} mt="xs" dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat }} />
|
||||
|
||||
<Flex align="center" justify="apart" mt="md" gap="xs">
|
||||
<Text size="xs" c="dimmed">
|
||||
@@ -169,7 +168,7 @@ function Page() {
|
||||
<Pagination
|
||||
total={totalPages}
|
||||
value={page}
|
||||
onChange={setPage}
|
||||
onChange={handlePageChange}
|
||||
siblings={1}
|
||||
boundaries={1}
|
||||
withEdges
|
||||
|
||||
@@ -1,47 +1,30 @@
|
||||
'use client'
|
||||
import stateKonservasiAdatBali from '@/app/admin/(dashboard)/_state/lingkungan/konservasi-adat-bali';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Center, List, ListItem, Paper, SimpleGrid, Stack, Text } from '@mantine/core';
|
||||
import { Box, Center, Paper, SimpleGrid, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||
|
||||
const data = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Filosofi Tri Hita Karana',
|
||||
listDeskripsi: (
|
||||
<List fz={'lg'} spacing="sm" ta={'justify'}>
|
||||
<ListItem>Parahyangan: Hubungan manusia dengan Tuhan yang dijaga penuh kesadaran spiritual</ListItem>
|
||||
<ListItem>Pawongan: Harmoni dan kerja sama antar manusia dalam masyarakat</ListItem>
|
||||
<ListItem>Palemahan: Pelestarian lingkungan dan hubungan manusia dengan alam</ListItem>
|
||||
</List>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Bentuk Konservasi Berdasarkan Adat',
|
||||
listDeskripsi: (
|
||||
<List fz={'lg'} spacing="sm" ta={'justify'}>
|
||||
<ListItem>Pelestarian Hutan Adat seperti Alas Pala Sangeh dan Wana Kerthi</ListItem>
|
||||
<ListItem>Subak: Sistem irigasi tradisional yang menekankan kebersamaan dan keberlanjutan</ListItem>
|
||||
<ListItem>Hari Raya Tumpek Uduh: Perayaan untuk menghormati pohon dan tumbuhan</ListItem>
|
||||
<ListItem>Perarem & Awig-Awig: Aturan adat untuk menjaga lingkungan dari kerusakan</ListItem>
|
||||
<ListItem>Ritual penyucian alam seperti Melasti dan Piodalan Segara</ListItem>
|
||||
</List>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Nilai Konservasi Adat',
|
||||
listDeskripsi: (
|
||||
<List fz={'lg'} spacing="sm" ta={'justify'}>
|
||||
<ListItem>Menjaga keseimbangan ekosistem dan lingkungan hidup</ListItem>
|
||||
<ListItem>Melestarikan spiritualitas lokal dan kesucian alam</ListItem>
|
||||
<ListItem>Meningkatkan kesadaran kolektif untuk hidup selaras dengan alam</ListItem>
|
||||
<ListItem>Menjamin keberlanjutan sumber daya alam untuk generasi mendatang</ListItem>
|
||||
</List>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
function Page() {
|
||||
const filosofi = useProxy(stateKonservasiAdatBali.stateFilosofiTriHita.findById)
|
||||
const nilai = useProxy(stateKonservasiAdatBali.stateNilaiKonservasiAdat.findById)
|
||||
const bentuk = useProxy(stateKonservasiAdatBali.stateBentukKonservasiBerdasarkanAdat.findById)
|
||||
|
||||
useShallowEffect(() => {
|
||||
filosofi.load('edit')
|
||||
nilai.load('edit')
|
||||
bentuk.load('edit')
|
||||
}, [])
|
||||
|
||||
if (filosofi.loading || !filosofi.data || nilai.loading || !nilai.data || bentuk.loading || !bentuk.data) {
|
||||
return (
|
||||
<Stack py={20} align="center">
|
||||
<Skeleton radius="md" height={600} width="100%" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap="24">
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
@@ -56,24 +39,99 @@ function Page() {
|
||||
</Text>
|
||||
</Box>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg">
|
||||
{data.map((item) => (
|
||||
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg" style={{ alignItems: 'stretch' }}>
|
||||
{/* Filsosofi */}
|
||||
<Box style={{ display: 'flex', height: '100%' }}>
|
||||
<Paper
|
||||
key={item.id}
|
||||
p="lg"
|
||||
bg="linear-gradient(145deg, #DFE3E8FF 0%, #EFF1F4FF 100%)"
|
||||
style={{ borderRadius: 16, boxShadow: '0 0 20px rgba(28, 110, 164, 0.5)' }}
|
||||
style={{
|
||||
borderRadius: 16,
|
||||
boxShadow: '0 0 20px rgba(28, 110, 164, 0.5)',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}}
|
||||
>
|
||||
<Stack gap="md" px={20}>
|
||||
<Stack gap="md" px={20} style={{ height: '100%' }}>
|
||||
<Center>
|
||||
<Text fz="xl" fw="bold" c="black">
|
||||
{item.title}
|
||||
</Text>
|
||||
{filosofi.data?.judul}
|
||||
</Text>
|
||||
</Center>
|
||||
{item.listDeskripsi}
|
||||
<div
|
||||
style={{
|
||||
wordBreak: "break-word",
|
||||
whiteSpace: "normal",
|
||||
flexGrow: 1
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: filosofi.data?.deskripsi || '' }}
|
||||
/>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))}
|
||||
</Box>
|
||||
{/* Nilai */}
|
||||
<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'
|
||||
}}
|
||||
>
|
||||
<Stack gap="md" px={20} style={{ height: '100%' }}>
|
||||
<Center>
|
||||
<Text fz="xl" fw="bold" c="black">
|
||||
{nilai.data?.judul}
|
||||
</Text>
|
||||
</Center>
|
||||
<div
|
||||
style={{
|
||||
wordBreak: "break-word",
|
||||
whiteSpace: "normal",
|
||||
flexGrow: 1,
|
||||
minHeight: 0
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: nilai.data?.deskripsi || '' }}
|
||||
/>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
{/* Bentuk */}
|
||||
<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'
|
||||
}}
|
||||
>
|
||||
<Stack gap="md" px={20} style={{ height: '100%' }}>
|
||||
<Center>
|
||||
<Text fz="xl" fw="bold" c="black">
|
||||
{bentuk.data?.judul}
|
||||
</Text>
|
||||
</Center>
|
||||
<div
|
||||
style={{
|
||||
wordBreak: "break-word",
|
||||
whiteSpace: "normal",
|
||||
flexGrow: 1,
|
||||
minHeight: 0
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: bentuk.data?.deskripsi || '' }}
|
||||
/>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
'use client'
|
||||
import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Flex, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { Box, Center, Flex, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
|
||||
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||
import { Icon, IconChartLine, IconClipboardTextFilled, IconLeaf, IconRecycle, IconScale, IconSearch, IconTent, IconTrashFilled, IconTrophy, IconTruckFilled } from '@tabler/icons-react';
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||
import dynamic from 'next/dynamic';
|
||||
@@ -20,20 +20,26 @@ function Page() {
|
||||
const state = useProxy(pengelolaanSampahState.pengelolaanSampah)
|
||||
const state2 = useProxy(pengelolaanSampahState.keteranganSampah)
|
||||
|
||||
const [search, setSearch] = useState('')
|
||||
const [debouncedSearch] = useDebouncedValue(search, 500);
|
||||
|
||||
const {
|
||||
data,
|
||||
load
|
||||
load,
|
||||
|
||||
} = state.findMany
|
||||
|
||||
const {
|
||||
data: data2,
|
||||
load: load2
|
||||
load: load2,
|
||||
page,
|
||||
totalPages,
|
||||
} = state2.findMany
|
||||
|
||||
useShallowEffect(() => {
|
||||
load()
|
||||
load2()
|
||||
}, [])
|
||||
load2(page, 3, debouncedSearch)
|
||||
}, [page, debouncedSearch])
|
||||
|
||||
const iconMap: Record<string, Icon> = {
|
||||
ekowisata: IconLeaf,
|
||||
@@ -104,8 +110,10 @@ function Page() {
|
||||
px={{ base: 70, md: 150 }}
|
||||
leftSection={<IconSearch size={20} />}
|
||||
placeholder='Cari Bank Sampah Terdekat'
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
|
||||
|
||||
<SimpleGrid cols={{ base: 1, md: 2 }} spacing="lg">
|
||||
{/* Left side - List of bank locations */}
|
||||
<Box>
|
||||
@@ -131,9 +139,17 @@ function Page() {
|
||||
</Paper>
|
||||
))}
|
||||
</Stack>
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => load(newPage)} // ini penting!
|
||||
total={totalPages}
|
||||
my="md"
|
||||
/>
|
||||
</Center>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
|
||||
{/* Right side - Single map showing all locations */}
|
||||
<Box style={{ position: 'sticky', top: '20px' }}>
|
||||
<Paper p="md" bg={colors['white-trans-1']} radius="lg" h="100%">
|
||||
|
||||
Reference in New Issue
Block a user