nico/27-okt-25 #1

Merged
nicoarya20 merged 277 commits from nico/27-okt-25 into main 2025-10-27 22:18:01 +08:00
1638 changed files with 159178 additions and 928 deletions
Showing only changes of commit b5c044df6e - Show all commits

View File

@@ -354,14 +354,39 @@ const kategoriKegiatan = proxy({
id: string;
nama: string;
}> | null,
async load() {
const res = await ApiFetch.api.lingkungan.kategorikegiatan[
"find-many"
].get();
if (res.status === 200) {
kategoriKegiatan.findMany.data = res.data?.data ?? [];
}
},
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
kategoriKegiatan.findMany.loading = true; // ✅ Akses langsung via nama path
kategoriKegiatan.findMany.page = page;
kategoriKegiatan.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res =
await ApiFetch.api.lingkungan.kategorikegiatan[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
kategoriKegiatan.findMany.data = res.data.data ?? [];
kategoriKegiatan.findMany.totalPages = res.data.totalPages ?? 1;
} else {
kategoriKegiatan.findMany.data = [];
kategoriKegiatan.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch kategori kegiatan paginated:", err);
kategoriKegiatan.findMany.data = [];
kategoriKegiatan.findMany.totalPages = 1;
} finally {
kategoriKegiatan.findMany.loading = false;
}
},
},
findUnique: {
data: null as Prisma.KategoriKegiatanGetPayload<{

View File

@@ -3,7 +3,17 @@
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import dataLingkunganDesaState from '@/app/admin/(dashboard)/_state/lingkungan/data-lingkungan-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';
@@ -19,34 +29,34 @@ interface FormDataLingkunganDesa {
}
type IconKey =
'ekowisata' |
'kompetisi' |
'wisata' |
'ekonomi' |
'sampah' |
'truck' |
'scale' |
'clipboard' |
'trash' |
'lingkunganSehat' |
'sumberOksigen' |
'ekonomiBerkelanjutan' |
'mencegahBencana' |
'rumah' |
'pohon' |
'air';
| 'ekowisata'
| 'kompetisi'
| 'wisata'
| 'ekonomi'
| 'sampah'
| 'truck'
| 'scale'
| 'clipboard'
| 'trash'
| 'lingkunganSehat'
| 'sumberOksigen'
| 'ekonomiBerkelanjutan'
| 'mencegahBencana'
| 'rumah'
| 'pohon'
| 'air';
function EditDataLingkunganDesa() {
const stateDataLingkunganDesa = useProxy(dataLingkunganDesaState)
const params = useParams()
const stateDataLingkunganDesa = useProxy(dataLingkunganDesaState);
const params = useParams();
const router = useRouter();
const [formData, setFormData] = useState<FormDataLingkunganDesa>({
name: '',
deskripsi: '',
jumlah: '',
icon: '',
})
});
useEffect(() => {
const loadProgramKreatif = async () => {
@@ -56,7 +66,6 @@ function EditDataLingkunganDesa() {
try {
const data = await stateDataLingkunganDesa.update.load(id);
if (data) {
// ⬇️ FIX PENTING: tambahkan ini
stateDataLingkunganDesa.update.id = id;
stateDataLingkunganDesa.update.form = {
@@ -74,16 +83,14 @@ function EditDataLingkunganDesa() {
});
}
} catch (error) {
console.error("Error loading data lingkungan desa:", error);
toast.error("Gagal memuat data data lingkungan desa");
console.error('Error loading data lingkungan desa:', error);
toast.error('Gagal memuat data lingkungan desa');
}
}
};
loadProgramKreatif();
}, [params?.id]);
const handleSubmit = async () => {
try {
stateDataLingkunganDesa.update.form = {
@@ -92,49 +99,68 @@ function EditDataLingkunganDesa() {
deskripsi: formData.deskripsi.trim(),
jumlah: formData.jumlah.trim(),
icon: formData.icon.trim(),
}
};
await stateDataLingkunganDesa.update.submit();
router.push("/admin/lingkungan/data-lingkungan-desa");
toast.success('Data lingkungan desa berhasil diperbarui!');
router.push('/admin/lingkungan/data-lingkungan-desa');
} catch (error) {
console.error("Error updating data lingkungan desa:", error);
toast.error("Gagal memuat data data lingkungan desa");
console.error('Error updating data lingkungan desa:', error);
toast.error('Terjadi kesalahan saat memperbarui data lingkungan desa');
}
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
};
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={3}>Edit Data Lingkungan Desa</Title>
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>
<Title order={4} ml="sm" c="dark">
Edit Data Lingkungan Desa
</Title>
</Group>
<Paper
w={{ base: '100%', md: '60%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
value={formData.name}
label={<Text fz={"sm"} fw={"bold"}>Nama Data Lingkungan Desa</Text>}
placeholder="masukkan nama data lingkungan desa"
onChange={(val) => {
label={<Text fz="sm" fw="bold">Nama Data Lingkungan Desa</Text>}
placeholder="Masukkan nama data lingkungan desa"
onChange={(val) =>
setFormData({
...formData,
name: val.target.value
name: val.target.value,
})
}}
}
required
/>
<TextInput
value={formData.jumlah}
label={<Text fz={"sm"} fw={"bold"}>Jumlah Data Lingkungan Desa</Text>}
placeholder="masukkan jumlah data lingkungan desa"
onChange={(val) => {
label={<Text fz="sm" fw="bold">Jumlah Data Lingkungan Desa</Text>}
placeholder="Masukkan jumlah data lingkungan desa"
onChange={(val) =>
setFormData({
...formData,
jumlah: val.target.value
jumlah: val.target.value,
})
}}
}
required
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<Text fz="sm" fw="bold" mb={6}>
Deskripsi
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
@@ -143,16 +169,34 @@ function EditDataLingkunganDesa() {
}}
/>
</Box>
<Box>
<Text fz={"sm"} fw={"bold"}>Ikon Data Lingkungan Desa</Text>
<Text fz="sm" fw="bold" mb={6}>
Ikon Data Lingkungan Desa
</Text>
<SelectIconProgramEdit
value={formData.icon as IconKey}
onChange={(value) => {
setFormData((prev) => ({ ...prev, icon: value }));
stateDataLingkunganDesa.update.form.icon = value;
}} />
}}
/>
</Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
<Group justify="flex-end">
<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>

View File

@@ -1,23 +1,40 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconChartLine, IconChristmasTreeFilled, IconClipboard, IconDroplet, IconEdit, IconHome, IconHomeEco, IconLeaf, IconRecycle, IconScale, IconShieldFilled, IconTent, IconTrash, IconTree, IconTrendingUp, IconTrophy, IconTruck, IconX } from '@tabler/icons-react';
import {
IconArrowBack,
IconChartLine,
IconChristmasTreeFilled,
IconClipboard,
IconDroplet,
IconEdit,
IconHome,
IconHomeEco,
IconLeaf,
IconRecycle,
IconScale,
IconShieldFilled,
IconTent,
IconTrash,
IconTree,
IconTrendingUp,
IconTrophy,
IconTruck,
} from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import React, { useState } from 'react';
import { useProxy } from 'valtio/utils';
import dataLingkunganDesaState from '../../../_state/lingkungan/data-lingkungan-desa';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import dataLingkunganDesaState from '../../../_state/lingkungan/data-lingkungan-desa';
function DetailDataLingkunganDesa() {
const [modalHapus, setModalHapus] = useState(false)
const stateDataLingkungan = useProxy(dataLingkunganDesaState)
const router = useRouter()
const params = useParams()
const [selectedId, setSelectedId] = useState<string | null>(null)
const [modalHapus, setModalHapus] = useState(false);
const stateDataLingkungan = useProxy(dataLingkunganDesaState);
const router = useRouter();
const params = useParams();
const [selectedId, setSelectedId] = useState<string | null>(null);
const iconMap: Record<string, React.FC<any>> = {
ekowisata: IconLeaf,
@@ -35,90 +52,117 @@ function DetailDataLingkunganDesa() {
mencegahBencana: IconShieldFilled,
rumah: IconHome,
pohon: IconTree,
air: IconDroplet
air: IconDroplet,
};
useShallowEffect(() => {
stateDataLingkungan.findUnique.load(params?.id as string)
}, [params?.id])
stateDataLingkungan.findUnique.load(params?.id as string);
}, [params?.id]);
const handleHapus = () => {
if (selectedId) {
stateDataLingkungan.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/lingkungan/data-lingkungan-desa")
stateDataLingkungan.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
router.push('/admin/lingkungan/data-lingkungan-desa');
}
}
};
if (!stateDataLingkungan.findUnique.data) {
return (
<Stack>
<Skeleton h={500} />
<Stack py={10}>
<Skeleton height={500} radius="md" />
</Stack>
)
);
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Data Lingkungan Desa</Text>
const data = stateDataLingkungan.findUnique.data;
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
return (
<Box py={10}>
{/* Back Button */}
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15}
>
Kembali
</Button>
{/* Main Card */}
<Paper
withBorder
w={{ base: '100%', md: '60%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
{/* Title */}
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Data Lingkungan Desa
</Text>
{/* Content Card */}
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Data Lingkungan Desa</Text>
<Text fz={"lg"}>{stateDataLingkungan.findUnique.data?.name}</Text>
<Text fz="lg" fw="bold">Nama Data Lingkungan Desa</Text>
<Text fz="md" c="dimmed">{data?.name || '-'}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Jumlah Data Lingkungan Desa</Text>
<Text fz={"lg"}>{stateDataLingkungan.findUnique.data?.jumlah}</Text>
<Text fz="lg" fw="bold">Jumlah Data Lingkungan Desa</Text>
<Text fz="md" c="dimmed">{data?.jumlah || '-'}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Ikon Data Lingkungan Desa</Text>
{iconMap[stateDataLingkungan.findUnique.data?.icon] && (
<Box title={stateDataLingkungan.findUnique.data?.icon}>
{React.createElement(iconMap[stateDataLingkungan.findUnique.data?.icon], { size: 24 })}
<Text fz="lg" fw="bold">Ikon Data Lingkungan Desa</Text>
{iconMap[data?.icon] ? (
<Box title={data?.icon}>
{React.createElement(iconMap[data.icon], { size: 28, color: colors['blue-button'] })}
</Box>
) : (
<Text fz="sm" c="dimmed">Tidak ada ikon</Text>
)}
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: stateDataLingkungan.findUnique.data?.deskripsi }}></Text>
<Text fz="lg" fw="bold">Deskripsi</Text>
<Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data?.deskripsi || '-' }} />
</Box>
<Box>
<Flex gap={"xs"} mt={10}>
{/* Action Buttons */}
<Group gap="sm" mt="sm">
<Tooltip label="Hapus Data Lingkungan Desa" withArrow position="top">
<Button
color="red"
onClick={() => {
if (stateDataLingkungan.findUnique.data) {
setSelectedId(stateDataLingkungan.findUnique.data.id);
setModalHapus(true);
}
setSelectedId(data.id);
setModalHapus(true);
}}
disabled={stateDataLingkungan.delete.loading || !stateDataLingkungan.findUnique.data}
color={"red"}
variant="light"
radius="md"
size="md"
>
<IconX size={20} />
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Data Lingkungan Desa" withArrow position="top">
<Button
onClick={() => {
if (stateDataLingkungan.findUnique.data) {
router.push(`/admin/lingkungan/data-lingkungan-desa/${stateDataLingkungan.findUnique.data.id}/edit`);
}
}}
disabled={!stateDataLingkungan.findUnique.data}
color={"green"}
color="green"
onClick={() => router.push(`/admin/lingkungan/data-lingkungan-desa/${data.id}/edit`)}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Tooltip>
</Group>
</Stack>
</Paper>
</Stack>
@@ -130,7 +174,7 @@ function DetailDataLingkunganDesa() {
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus data lingkungan desa ini?"
/>
/>
</Box>
);
}

View File

@@ -1,6 +1,16 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, 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 { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
@@ -8,60 +18,105 @@ import CreateEditor from '../../../_com/createEditor';
import SelectIconProgram from '../../../_com/selectIcon';
import dataLingkunganDesaState from '../../../_state/lingkungan/data-lingkungan-desa';
function CreateDataLingkunganDesa() {
const stateCreate = useProxy(dataLingkunganDesaState)
const stateCreate = useProxy(dataLingkunganDesaState);
const router = useRouter();
const resetForm = () => {
stateCreate.create.form = {
name: "",
deskripsi: "",
jumlah: "",
icon: "",
}
}
name: '',
deskripsi: '',
jumlah: '',
icon: '',
};
};
const handleSubmit = async () => {
await stateCreate.create.create();
resetForm();
router.push("/admin/lingkungan/data-lingkungan-desa")
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
router.push('/admin/lingkungan/data-lingkungan-desa');
};
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={3}>Create Data Lingkungan Desa</Title>
return (
<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={22} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Data Lingkungan Desa
</Title>
</Group>
{/* Card Form */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Nama Data Lingkungan Desa</Text>}
placeholder="masukkan nama data lingkungan desa"
onChange={(val) => stateCreate.create.form.name = val.target.value}
label={<Text fw="bold" fz="sm">Nama Data Lingkungan Desa</Text>}
placeholder="Masukkan nama data lingkungan desa"
value={stateCreate.create.form.name || ''}
onChange={(val) => (stateCreate.create.form.name = val.target.value)}
required
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Ikon Data Lingkungan Desa</Text>
<SelectIconProgram onChange={(value) => stateCreate.create.form.icon = value} />
</Box>
<TextInput
type='number'
onChange={(e) => stateCreate.create.form.jumlah = e.currentTarget.value}
label={<Text fw={"bold"} fz={"sm"}>Jmlah data lingkungan desa</Text>}
placeholder='Masukkan jumlah data lingkungan desa'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi data lingkungan desa</Text>
<CreateEditor
value={stateCreate.create.form.deskripsi}
onChange={(htmlContent) => stateCreate.create.form.deskripsi = htmlContent}
<Text fz="sm" fw="bold" mb={6}>
Ikon Data Lingkungan Desa
</Text>
<SelectIconProgram
onChange={(value) => (stateCreate.create.form.icon = value)}
/>
</Box>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
<TextInput
label={<Text fw="bold" fz="sm">Jumlah Data Lingkungan Desa</Text>}
placeholder="Masukkan jumlah data lingkungan desa"
value={stateCreate.create.form.jumlah || ''}
onChange={(e) => (stateCreate.create.form.jumlah = e.currentTarget.value)}
required
/>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi Data Lingkungan Desa
</Text>
<CreateEditor
value={stateCreate.create.form.deskripsi}
onChange={(htmlContent) =>
(stateCreate.create.form.deskripsi = htmlContent)
}
/>
</Box>
{/* Submit Button */}
<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>

View File

@@ -2,21 +2,23 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import {
IconChartLine, IconChristmasTreeFilled, IconClipboardTextFilled, IconDeviceImac, IconDroplet, IconHome, IconHomeEco, IconLeaf,
Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack,
Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text,
Title,
Tooltip
} from '@mantine/core';
import {
IconChartLine, IconChristmasTreeFilled, IconClipboardTextFilled,
IconDeviceImacCog, IconDroplet, IconHome, IconHomeEco, IconLeaf,
IconPlus,
IconRecycle, IconScale, IconSearch, IconShieldFilled, IconTent,
IconTrashFilled,
IconTree,
IconTrendingUp,
IconTrophy,
IconTruckFilled
IconTrashFilled, IconTree, IconTrendingUp, IconTrophy, IconTruckFilled
} from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import dataLingkunganDesaState from '../../_state/lingkungan/data-lingkungan-desa';
@@ -26,7 +28,7 @@ function DataLingkunganDesa() {
<Box>
<HeaderSearch
title='Data Lingkungan Desa'
placeholder='pencarian'
placeholder='Cari data lingkungan...'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -64,76 +66,98 @@ function ListDataLingkunganDesa({ search }: { search: string }) {
rumah: IconHome,
pohon: IconTree,
air: IconDroplet
};
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={650} />
<Skeleton height={600} radius="md" />
</Stack>
);
}
if (data.length === 0) {
return (
<Box py={10}>
<Paper p="md" >
<Paper withBorder shadow="md" radius="md" p="lg" bg={colors['white-1']}>
<Stack>
<JudulList
title='List Data Lingkungan Desa'
href='/admin/lingkungan/data-lingkungan-desa/create'
/>
<Table striped withTableBorder withRowBorders>
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Data Lingkungan Desa</Title>
<Tooltip label="Tambah Data Lingkungan Desa" withArrow>
<Button leftSection={<IconPlus size={18} />} color="blue" variant="light" onClick={() => router.push('/admin/lingkungan/data-lingkungan-desa/create')}>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Nama Data Lingkungan Desa</TableTh>
<TableTh>Jumlah Data Lingkungan Desa</TableTh>
<TableTh>Jumlah</TableTh>
<TableTh>Ikon</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
</Table>
<Text ta="center">Tidak ada data lingkungan desa yang tersedia</Text>
<Center py={20}>
<Text c="dimmed">Tidak ada data lingkungan desa yang tersedia</Text>
</Center>
</Stack>
</Paper>
</Box >
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'} h={{ base: 'auto', md: 650 }}>
<JudulList
title='List Data Lingkungan Desa'
href='/admin/lingkungan/data-lingkungan-desa/create'
/>
<Box style={{ overflowY: 'auto' }}>
<Table striped withTableBorder withRowBorders>
<Paper withBorder shadow="md" radius="md" bg={colors['white-1']} p="lg">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Data Lingkungan Desa</Title>
<Tooltip label="Tambah Data Lingkungan Desa" withArrow>
<Button leftSection={<IconPlus size={18} />} color="blue" variant="light" onClick={() => router.push('/admin/lingkungan/data-lingkungan/create')}>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh>
<TableTh style={{ width: '20%' }}>Nama Data Lingkungan Desa</TableTh>
<TableTh style={{ width: '35%' }}>Jumlah Data Lingkungan Desa</TableTh>
<TableTh style={{ width: '10%' }}>Ikon</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Detail</TableTh>
<TableTh style={{ width: '25%' }}>Nama Data</TableTh>
<TableTh style={{ width: '35%' }}>Jumlah</TableTh>
<TableTh style={{ width: '15%' }}>Ikon</TableTh>
<TableTh style={{ width: '20%', textAlign: 'center' }}>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd style={{ width: '5%', textAlign: 'center' }}>{index + 1}</TableTd>
<TableTd style={{ width: '20%', wordWrap: 'break-word' }}>{item.name}</TableTd>
<TableTd style={{ width: '35%', wordWrap: 'break-word' }}>± {item.jumlah}</TableTd>
<TableTd style={{ width: '10%' }}>
<TableTd style={{ textAlign: 'center' }}>{index + 1}</TableTd>
<TableTd>
<Text fw={500} truncate="end" lineClamp={1}>{item.name}</Text>
</TableTd>
<TableTd>
<Text fz="sm" c="dimmed">± {item.jumlah}</Text>
</TableTd>
<TableTd>
{iconMap[item.icon] && (
<Box title={item.icon}>
{React.createElement(iconMap[item.icon], { size: 24 })}
{React.createElement(iconMap[item.icon], { size: 22 })}
</Box>
)}
</TableTd>
<TableTd style={{ width: '15%', textAlign: 'center' }}>
<Button onClick={() => router.push(`/admin/lingkungan/data-lingkungan-desa/${item.id}`)}>
<IconDeviceImac size={25} />
<TableTd style={{ textAlign: 'center' }}>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() => router.push(`/admin/lingkungan/data-lingkungan-desa/${item.id}`)}
>
Detail
</Button>
</TableTd>
</TableTr>
@@ -147,11 +171,13 @@ function ListDataLingkunganDesa({ search }: { search: string }) {
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
</Box>

View File

@@ -1,67 +1,116 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { Box, ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { IconBook, IconLeaf, IconSchool } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
function LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter()
const pathname = usePathname()
const tabs = [
{
label: "Tujuan Edukasi Lingkungan",
value: "tujuanedukasilingkungan",
href: "/admin/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan"
href: "/admin/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan",
tooltip: "Lihat tujuan edukasi lingkungan",
icon: <IconLeaf size={18} stroke={1.8} />
},
{
label: "Materi Edukasi Yang Diberikan",
value: "materiedukasiyangdiberikan",
href: "/admin/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan"
href: "/admin/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan",
tooltip: "Kelola materi edukasi yang diberikan",
icon: <IconBook size={18} stroke={1.8} />
},
{
label: "Contoh Kegiatan Di Desa Darmasaba",
value: "contohkegiatan",
href: "/admin/lingkungan/edukasi-lingkungan/contoh-kegiatan-desa-darmasaba"
href: "/admin/lingkungan/edukasi-lingkungan/contoh-kegiatan-desa-darmasaba",
tooltip: "Lihat contoh kegiatan desa Darmasaba",
icon: <IconSchool size={18} stroke={1.8} />
},
];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
const currentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value)
if (tab) {
router.push(tab.href)
}
if (tab) router.push(tab.href)
setActiveTab(value)
}
useEffect(() => {
const match = tabs.find(tab => tab.href === pathname)
if (match) {
setActiveTab(match.value)
}
if (match) setActiveTab(match.value)
}, [pathname])
return (
<Stack>
<Title order={3}>Jumlah Penduduk Usia Kerja yang Menganggur</Title>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
{tabs.map((e, i) => (
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
))}
</TabsList>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value}>
{/* Konten dummy, bisa diganti tergantung routing */}
<></>
<Stack gap="md">
<Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
Edukasi Lingkungan
</Title>
<Tabs
color={colors['blue-button']}
variant="pills"
value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem",
}}
>
{tabs.map((tab, i) => (
<Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
whiteSpace: "nowrap",
transition: "all 0.2s ease",
}}
>
<span style={{
display: "inline-block",
maxWidth: "200px",
overflow: "hidden",
textOverflow: "ellipsis"
}}>
{tab.label}
</span>
</TabsTab>
</Tooltip>
))}
</TabsList>
</ScrollArea>
{tabs.map((tab, i) => (
<TabsPanel key={i} value={tab.value}>
<Box p="md">
{children}
</Box>
</TabsPanel>
))}
</Tabs>
{children}
</Stack>
);
)
}
export default LayoutTabs;
export default LayoutTabs;

View File

@@ -9,77 +9,105 @@ import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
const EdukasiLingkunganTextEditor = dynamic(() => import('../../_lib/edukasiLingkunganTextEditor').then(mod => mod.EdukasiLingkunganTextEditor), {
ssr: false,
});
const EdukasiLingkunganTextEditor = dynamic(
() => import('../../_lib/edukasiLingkunganTextEditor').then(mod => mod.EdukasiLingkunganTextEditor),
{ ssr: false }
);
function EditContohKegiatanDesaDarmasaba() {
const router = useRouter()
const contohEdukasiState = useProxy(stateEdukasiLingkungan.stateContohEdukasiLingkungan)
const router = useRouter();
const contohEdukasiState = useProxy(stateEdukasiLingkungan.stateContohEdukasiLingkungan);
const [judul, setJudul] = useState('');
const [content, setContent] = useState('');
useShallowEffect(() => {
if (!contohEdukasiState.findById.data) {
contohEdukasiState.findById.initialize(); // biar masuk ke `findFirst` route kamu
contohEdukasiState.findById.initialize();
}
}, []);
useEffect(() => {
if (contohEdukasiState.findById.data) {
setJudul(contohEdukasiState.findById.data.judul ?? '')
setContent(contohEdukasiState.findById.data.deskripsi ?? '')
setJudul(contohEdukasiState.findById.data.judul ?? '');
setContent(contohEdukasiState.findById.data.deskripsi ?? '');
}
}, [contohEdukasiState.findById.data])
}, [contohEdukasiState.findById.data]);
const submit = () => {
if (contohEdukasiState.findById.data) {
contohEdukasiState.findById.data.judul = judul;
contohEdukasiState.findById.data.deskripsi = content;
contohEdukasiState.update.save(contohEdukasiState.findById.data)
contohEdukasiState.update.save(contohEdukasiState.findById.data);
}
router.push('/admin/lingkungan/edukasi-lingkungan/contoh-kegiatan-desa-darmasaba')
}
router.push('/admin/lingkungan/edukasi-lingkungan/contoh-kegiatan-desa-darmasaba');
};
return (
<Box>
<Stack gap={'xs'}>
<Box>
<Button
variant={'subtle'}
onClick={() => router.back()}
>
<IconArrowBack color={colors['blue-button']} size={20} />
</Button>
</Box>
<Box>
<Paper bg={colors['white-1']} p={'md'} radius={10} w={{ base: '100%', md: '50%' }}>
<Stack gap={'xs'}>
<Title order={3}>Edit Contoh Kegiatan Di Desa Darmasaba</Title>
<Text fw={"bold"}>Judul</Text>
<EdukasiLingkunganTextEditor
showSubmit={false}
onChange={setJudul}
initialContent={judul}
/>
<Text fw={"bold"}>Deskripsi</Text>
<EdukasiLingkunganTextEditor
showSubmit={false}
onChange={setContent}
initialContent={content}
/>
<Group>
<Button
bg={colors['blue-button']}
onClick={submit}
loading={contohEdukasiState.update.loading}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
</Stack>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */}
<Group mb="md">
<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 Contoh Kegiatan di Desa Darmasaba
</Title>
</Group>
{/* Form Paper */}
<Paper
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
w={{ base: '100%', md: '50%' }}
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<Box>
<Text fw="bold" mb={6}>
Judul
</Text>
<EdukasiLingkunganTextEditor
showSubmit={false}
onChange={setJudul}
initialContent={judul}
/>
</Box>
<Box>
<Text fw="bold" mb={6}>
Deskripsi
</Text>
<EdukasiLingkunganTextEditor
showSubmit={false}
onChange={setContent}
initialContent={content}
/>
</Box>
<Group justify="right" mt="md">
<Button
onClick={submit}
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)',
}}
loading={contohEdukasiState.update.loading}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}

View File

@@ -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';
@@ -8,47 +7,72 @@ import { useProxy } from 'valtio/utils';
import stateEdukasiLingkungan from '../../../_state/lingkungan/edukasi-lingkungan';
function Page() {
const router = useRouter()
const listContohEdukasi = useProxy(stateEdukasiLingkungan.stateContohEdukasiLingkungan)
const router = useRouter();
const listContohEdukasi = useProxy(stateEdukasiLingkungan.stateContohEdukasiLingkungan);
useShallowEffect(() => {
listContohEdukasi.findById.load('edit')
}, [])
listContohEdukasi.findById.load('edit');
}, []);
if (!listContohEdukasi.findById.data) {
return (
<Stack>
<Skeleton radius={10} h={800} />
<Stack py={20}>
<Skeleton radius="md" height={600} />
</Stack>
)
);
}
return (
<Paper bg={colors['white-1']} p={'md'} radius={10}>
<Stack gap={"22"}>
<Grid>
<Box p="md">
<Paper withBorder p={{ base: 'md', md: 'lg' }} radius="md">
<Grid align="center" mb={{ base: 'md', md: 'lg' }}>
<GridCol span={{ base: 12, md: 11 }}>
<Title order={2}>Preview Contoh Kegiatan Di Desa Darmasaba</Title>
<Title order={3} fw={600}>
Preview Contoh Kegiatan Di Desa Darmasaba
</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button bg={colors['blue-button']} onClick={() => router.push('/admin/lingkungan/edukasi-lingkungan/contoh-kegiatan-desa-darmasaba/edit')}>
<IconEdit size={16} />
<GridCol span={{ base: 12, md: 1 }} style={{ textAlign: 'right' }}>
<Button
size="sm"
variant="light"
color="green"
radius="md"
leftSection={<IconEdit size={16} />}
onClick={() =>
router.push(
'/admin/lingkungan/edukasi-lingkungan/contoh-kegiatan-desa-darmasaba/edit'
)
}
>
Edit
</Button>
</GridCol>
</Grid>
<Box>
<Stack gap={'lg'}>
<Paper p={"xl"} bg={colors['BG-trans']}>
<Box px={{ base: 0, md: 30 }}>
<Text fz={{ base: "h3", md: "h2" }} fw={"bold"} dangerouslySetInnerHTML={{ __html: listContohEdukasi.findById.data.judul }} />
</Box>
<Box px={{ base: 0, md: 30 }}>
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: listContohEdukasi.findById.data.deskripsi }} />
</Box>
</Paper>
</Stack>
</Box>
</Stack>
</Paper>
)
<Stack gap="md">
<Paper p={{ base: 'md', md: 'xl' }} bg="#ECEEF8" radius="md">
<Box mb="md">
<Text
fz={{ base: 'xl', md: '2xl' }}
fw={600}
c="black"
dangerouslySetInnerHTML={{ __html: listContohEdukasi.findById.data.judul }}
/>
</Box>
<Box>
<Text
fz={{ base: 'md', md: 'lg' }}
ta="justify"
c="dimmed"
lineClamp={10}
dangerouslySetInnerHTML={{ __html: listContohEdukasi.findById.data.deskripsi }}
/>
</Box>
</Paper>
</Stack>
</Paper>
</Box>
);
}
export default Page;

View File

@@ -9,77 +9,90 @@ import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
const EdukasiLingkunganTextEditor = dynamic(() => import('../../_lib/edukasiLingkunganTextEditor').then(mod => mod.EdukasiLingkunganTextEditor), {
ssr: false,
});
const EdukasiLingkunganTextEditor = dynamic(
() => import('../../_lib/edukasiLingkunganTextEditor').then(mod => mod.EdukasiLingkunganTextEditor),
{ ssr: false }
);
function EditMateriEdukasiYangDiberikan() {
const router = useRouter()
const materiEdukasiState = useProxy(stateEdukasiLingkungan.stateMateriEdukasiLingkungan)
const router = useRouter();
const materiEdukasiState = useProxy(stateEdukasiLingkungan.stateMateriEdukasiLingkungan);
const [judul, setJudul] = useState('');
const [content, setContent] = useState('');
useShallowEffect(() => {
if (!materiEdukasiState.findById.data) {
materiEdukasiState.findById.initialize(); // biar masuk ke `findFirst` route kamu
materiEdukasiState.findById.initialize();
}
}, []);
useEffect(() => {
if (materiEdukasiState.findById.data) {
setJudul(materiEdukasiState.findById.data.judul ?? '')
setContent(materiEdukasiState.findById.data.deskripsi ?? '')
setJudul(materiEdukasiState.findById.data.judul ?? '');
setContent(materiEdukasiState.findById.data.deskripsi ?? '');
}
}, [materiEdukasiState.findById.data])
}, [materiEdukasiState.findById.data]);
const submit = () => {
if (materiEdukasiState.findById.data) {
materiEdukasiState.findById.data.judul = judul;
materiEdukasiState.findById.data.deskripsi = content;
materiEdukasiState.update.save(materiEdukasiState.findById.data)
materiEdukasiState.update.save(materiEdukasiState.findById.data);
}
router.push('/admin/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan')
}
router.push('/admin/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan');
};
return (
<Box>
<Stack gap={'xs'}>
<Box>
<Button
variant={'subtle'}
onClick={() => router.back()}
>
<IconArrowBack color={colors['blue-button']} size={20} />
</Button>
</Box>
<Box>
<Paper bg={colors['white-1']} p={'md'} radius={10} w={{ base: '100%', md: '50%' }}>
<Stack gap={'xs'}>
<Title order={3}>Edit Materi Edukasi Yang Diberikan</Title>
<Text fw={"bold"}>Judul</Text>
<EdukasiLingkunganTextEditor
showSubmit={false}
onChange={setJudul}
initialContent={judul}
/>
<Text fw={"bold"}>Content</Text>
<EdukasiLingkunganTextEditor
showSubmit={false}
onChange={setContent}
initialContent={content}
/>
<Group>
<Button
bg={colors['blue-button']}
onClick={submit}
loading={materiEdukasiState.update.loading}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
</Stack>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<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 Materi Edukasi Yang Diberikan
</Title>
</Group>
<Paper
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
w={{ base: '100%', md: '50%' }}
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<Box>
<Text fw="bold" mb={6}>
Judul
</Text>
<EdukasiLingkunganTextEditor showSubmit={false} onChange={setJudul} initialContent={judul} />
</Box>
<Box>
<Text fw="bold" mb={6}>
Konten
</Text>
<EdukasiLingkunganTextEditor showSubmit={false} onChange={setContent} initialContent={content} />
</Box>
<Group justify="right" mt="md">
<Button
onClick={submit}
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)',
}}
loading={materiEdukasiState.update.loading}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}

View File

@@ -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';
@@ -10,44 +9,65 @@ import stateEdukasiLingkungan from '../../../_state/lingkungan/edukasi-lingkunga
function Page() {
const router = useRouter()
const listMateriEdukasi = useProxy(stateEdukasiLingkungan.stateMateriEdukasiLingkungan)
useShallowEffect(() => {
listMateriEdukasi.findById.load('edit')
}, [])
if (!listMateriEdukasi.findById.data) {
return (
<Stack>
<Skeleton radius={10} h={800} />
<Stack py={20}>
<Skeleton radius="md" height={600} />
</Stack>
)
}
return (
<Paper bg={colors['white-1']} p={'md'} radius={10}>
<Stack gap={"22"}>
<Grid>
<Box p="md">
<Paper withBorder p={{ base: 'md', md: 'lg' }} radius="md">
<Grid align="center" mb={{ base: 'md', md: 'lg' }}>
<GridCol span={{ base: 12, md: 11 }}>
<Title order={2}>Preview Materi Edukasi Yang Diberikan</Title>
<Title order={3} fw={600}>Preview Materi Edukasi Yang Diberikan</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button bg={colors['blue-button']} onClick={() => router.push('/admin/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan/edit')}>
<IconEdit size={16} />
<GridCol span={{ base: 12, md: 1 }} style={{ textAlign: 'right' }}>
<Button
size="sm"
variant="light"
color="green"
radius="md"
leftSection={<IconEdit size={16} />}
onClick={() =>
router.push('/admin/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan/edit')
}
>
Edit
</Button>
</GridCol>
</Grid>
<Box>
<Stack gap={'lg'}>
<Paper p={"xl"} bg={colors['BG-trans']}>
<Box px={{ base: 0, md: 30 }}>
<Text fz={{ base: "h3", md: "h2" }} fw={"bold"} dangerouslySetInnerHTML={{ __html: listMateriEdukasi.findById.data.judul }} />
</Box>
<Box px={{ base: 0, md: 30 }}>
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: listMateriEdukasi.findById.data.deskripsi }} />
</Box>
</Paper>
</Stack>
</Box>
</Stack>
</Paper>
<Stack gap="md">
<Paper p={{ base: 'md', md: 'xl' }} bg="#ECEEF8" radius="md">
<Box mb="md">
<Text
fz={{ base: 'xl', md: '2xl' }}
fw={600}
c="black"
dangerouslySetInnerHTML={{ __html: listMateriEdukasi.findById.data.judul }}
/>
</Box>
<Box>
<Text
fz={{ base: 'md', md: 'lg' }}
ta="justify"
c="dimmed"
lineClamp={10}
dangerouslySetInnerHTML={{ __html: listMateriEdukasi.findById.data.deskripsi }}
/>
</Box>
</Paper>
</Stack>
</Paper>
</Box>
)
}

View File

@@ -9,77 +9,103 @@ import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
const EdukasiLingkunganTextEditor = dynamic(() => import('../../_lib/edukasiLingkunganTextEditor').then(mod => mod.EdukasiLingkunganTextEditor), {
ssr: false,
});
const EdukasiLingkunganTextEditor = dynamic(
() => import('../../_lib/edukasiLingkunganTextEditor').then(mod => mod.EdukasiLingkunganTextEditor),
{ ssr: false }
);
function EditTujuanEdukasiLingkungan() {
const router = useRouter()
const tujuanEdukasiState = useProxy(stateEdukasiLingkungan.stateTujuanEdukasi)
const router = useRouter();
const tujuanEdukasiState = useProxy(stateEdukasiLingkungan.stateTujuanEdukasi);
const [judul, setJudul] = useState('');
const [content, setContent] = useState('');
useShallowEffect(() => {
if (!tujuanEdukasiState.findById.data) {
tujuanEdukasiState.findById.initialize(); // biar masuk ke `findFirst` route kamu
tujuanEdukasiState.findById.initialize();
}
}, []);
useEffect(() => {
if (tujuanEdukasiState.findById.data) {
setJudul(tujuanEdukasiState.findById.data.judul ?? '')
setContent(tujuanEdukasiState.findById.data.deskripsi ?? '')
setJudul(tujuanEdukasiState.findById.data.judul ?? '');
setContent(tujuanEdukasiState.findById.data.deskripsi ?? '');
}
}, [tujuanEdukasiState.findById.data])
}, [tujuanEdukasiState.findById.data]);
const submit = () => {
if (tujuanEdukasiState.findById.data) {
tujuanEdukasiState.findById.data.judul = judul;
tujuanEdukasiState.findById.data.deskripsi = content;
tujuanEdukasiState.update.save(tujuanEdukasiState.findById.data)
tujuanEdukasiState.update.save(tujuanEdukasiState.findById.data);
}
router.push('/admin/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan')
}
router.push('/admin/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan');
};
return (
<Box>
<Stack gap={'xs'}>
<Box>
<Button
variant={'subtle'}
onClick={() => router.back()}
>
<IconArrowBack color={colors['blue-button']} size={20} />
</Button>
</Box>
<Box>
<Paper bg={colors['white-1']} p={'md'} radius={10} w={{ base: '100%', md: '50%' }}>
<Stack gap={'xs'}>
<Title order={3}>Edit Tujuan Edukasi Lingkungan</Title>
<Text fw={"bold"}>Judul</Text>
<EdukasiLingkunganTextEditor
showSubmit={false}
onChange={setJudul}
initialContent={judul}
/>
<Text fw={"bold"}>Content</Text>
<EdukasiLingkunganTextEditor
showSubmit={false}
onChange={setContent}
initialContent={content}
/>
<Group>
<Button
bg={colors['blue-button']}
onClick={submit}
loading={tujuanEdukasiState.update.loading}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
</Stack>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<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 Tujuan Edukasi Lingkungan
</Title>
</Group>
<Paper
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
w={{ base: '100%', md: '50%' }}
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<Box>
<Text fw="bold" mb={6}>
Judul
</Text>
<EdukasiLingkunganTextEditor
showSubmit={false}
onChange={setJudul}
initialContent={judul}
/>
</Box>
<Box>
<Text fw="bold" mb={6}>
Konten
</Text>
<EdukasiLingkunganTextEditor
showSubmit={false}
onChange={setContent}
initialContent={content}
/>
</Box>
<Group justify="right" mt="md">
<Button
onClick={submit}
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)',
}}
loading={tujuanEdukasiState.update.loading}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}

View File

@@ -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';
@@ -8,47 +7,68 @@ import { useProxy } from 'valtio/utils';
import stateEdukasiLingkungan from '../../../_state/lingkungan/edukasi-lingkungan';
function Page() {
const router = useRouter()
const listTujuanEdukasi = useProxy(stateEdukasiLingkungan.stateTujuanEdukasi)
const router = useRouter();
const listTujuanEdukasi = useProxy(stateEdukasiLingkungan.stateTujuanEdukasi);
useShallowEffect(() => {
listTujuanEdukasi.findById.load('edit')
}, [])
listTujuanEdukasi.findById.load('edit');
}, []);
if (!listTujuanEdukasi.findById.data) {
return (
<Stack>
<Skeleton radius={10} h={800} />
<Stack py={20}>
<Skeleton radius="md" height={600} />
</Stack>
)
);
}
return (
<Paper bg={colors['white-1']} p={'md'} radius={10}>
<Stack gap={"22"}>
<Grid>
<Box p="md">
<Paper withBorder p={{ base: 'md', md: 'lg' }} radius="md">
<Grid align="center" mb={{ base: 'md', md: 'lg' }}>
<GridCol span={{ base: 12, md: 11 }}>
<Title order={2}>Preview Tujuan Edukasi Lingkungan</Title>
<Title order={3} fw={600}>Preview Tujuan Edukasi Lingkungan</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button bg={colors['blue-button']} onClick={() => router.push('/admin/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan/edit')}>
<IconEdit size={16} />
<GridCol span={{ base: 12, md: 1 }} style={{ textAlign: 'right' }}>
<Button
size="sm"
variant="light"
color="green"
radius="md"
leftSection={<IconEdit size={16} />}
onClick={() =>
router.push('/admin/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan/edit')
}
>
Edit
</Button>
</GridCol>
</Grid>
<Box>
<Stack gap={'lg'}>
<Paper p={"xl"} bg={colors['BG-trans']}>
<Box px={{ base: 0, md: 30 }}>
<Text fz={{ base: "h3", md: "h2" }} fw={"bold"} dangerouslySetInnerHTML={{ __html: listTujuanEdukasi.findById.data.judul }} />
</Box>
<Box px={{ base: 0, md: 30 }}>
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: listTujuanEdukasi.findById.data.deskripsi }} />
</Box>
</Paper>
</Stack>
</Box>
</Stack>
</Paper>
)
<Stack gap="md">
<Paper p={{ base: 'md', md: 'xl' }} bg="#ECEEF8" radius="md">
<Box mb="md">
<Text
fz={{ base: 'xl', md: '2xl' }}
fw={600}
c='black'
dangerouslySetInnerHTML={{ __html: listTujuanEdukasi.findById.data.judul }}
/>
</Box>
<Box>
<Text
fz={{ base: 'md', md: 'lg' }}
ta="justify"
c="dimmed"
lineClamp={10}
dangerouslySetInnerHTML={{ __html: listTujuanEdukasi.findById.data.deskripsi }}
/>
</Box>
</Paper>
</Stack>
</Paper>
</Box>
);
}
export default Page;

View File

@@ -1,62 +1,121 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import { IconClipboardList, IconTags } from '@tabler/icons-react';
function LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter()
const pathname = usePathname()
const tabs = [
{
label: "Kegiatan Desa",
value: "kegiatanDesa",
href: "/admin/lingkungan/gotong-royong/kegiatan-desa"
},
{
label: "Kategori Kegiatan",
value: "kategoriKegiatan",
href: "/admin/lingkungan/gotong-royong/kategori-kegiatan"
},
];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
const router = useRouter();
const pathname = usePathname();
const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value)
if (tab) {
router.push(tab.href)
}
setActiveTab(value)
const tabs = [
{
label: "Kegiatan Desa",
value: "kegiatanDesa",
href: "/admin/lingkungan/gotong-royong/kegiatan-desa",
icon: <IconClipboardList size={18} stroke={1.8} />,
tooltip: "Lihat dan kelola kegiatan desa",
},
{
label: "Kategori Kegiatan",
value: "kategoriKegiatan",
href: "/admin/lingkungan/gotong-royong/kategori-kegiatan",
icon: <IconTags size={18} stroke={1.8} />,
tooltip: "Kelola kategori kegiatan desa",
},
];
const currentTab = tabs.find(tab => tab.href === pathname);
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value);
if (tab) {
router.push(tab.href);
}
setActiveTab(value);
};
useEffect(() => {
const match = tabs.find(tab => tab.href === pathname)
if (match) {
setActiveTab(match.value)
}
}, [pathname])
useEffect(() => {
const match = tabs.find(tab => tab.href === pathname);
if (match) {
setActiveTab(match.value);
}
}, [pathname]);
return (
<Stack>
<Title order={3}>Gotong Royong</Title>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
{tabs.map((e, i) => (
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
))}
</TabsList>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value}>
{/* Konten dummy, bisa diganti tergantung routing */}
<></>
</TabsPanel>
))}
</Tabs>
return (
<Stack gap="lg">
{/* ✅ Title lebih tegas */}
<Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
Gotong Royong
</Title>
<Tabs
color={colors['blue-button']}
variant="pills"
value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
{/* ✅ Scroll horizontal */}
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem",
}}
>
{tabs.map((tab, i) => (
<Tooltip
key={i}
label={tab.tooltip}
position="bottom"
withArrow
transitionProps={{ transition: 'pop', duration: 200 }}
>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
</ScrollArea>
{/* ✅ Panel dengan gaya kartu */}
{tabs.map((tab, i) => (
<TabsPanel
key={i}
value={tab.value}
style={{
padding: "1.5rem",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
borderRadius: "1rem",
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
}}
>
{children}
</Stack>
);
</TabsPanel>
))}
</Tabs>
</Stack>
);
}
export default LayoutTabs;
export default LayoutTabs;

View File

@@ -2,7 +2,7 @@
'use client'
import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong';
import colors from '@/con/colors';
import { Box, Button, Group, 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';
@@ -16,7 +16,7 @@ function EditKategoriKegiatan() {
const stateKategori = useProxy(gotongRoyongState.kategoriKegiatan);
const [formData, setFormData] = useState({
nama: "",
nama: '',
});
useEffect(() => {
@@ -27,15 +27,14 @@ function EditKategoriKegiatan() {
const data = await stateKategori.edit.load(id);
if (data) {
// pastikan id-nya masuk ke state edit
stateKategori.edit.id = id;
setFormData({
nama: data.nama || '',
});
}
} catch (error) {
console.error("Error loading kategori kegiatan:", error);
toast.error("Gagal memuat data kategori kegiatan");
console.error('Error loading kategori kegiatan:', error);
toast.error('Gagal memuat data kategori kegiatan');
}
};
@@ -49,45 +48,63 @@ function EditKategoriKegiatan() {
return;
}
stateKategori.edit.form = {
nama: formData.nama.trim(),
};
// Safety check tambahan: pastikan ID tidak kosong
if (!stateKategori.edit.id) {
stateKategori.edit.id = id; // fallback
}
stateKategori.edit.form = { nama: formData.nama.trim() };
if (!stateKategori.edit.id) stateKategori.edit.id = id;
const success = await stateKategori.edit.update();
if (success) {
router.push("/admin/lingkungan/gotong-royong/kategori-kegiatan");
toast.success('Kategori kegiatan berhasil diperbarui!');
router.push('/admin/lingkungan/gotong-royong/kategori-kegiatan');
}
} catch (error) {
console.error("Error updating kategori kegiatan:", error);
// toast akan ditampilkan dari fungsi update
console.error('Error updating kategori kegiatan:', error);
}
};
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md" align="center">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" p="xs" radius="md" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Kategori Kegiatan
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Kategori kegiatan</Title>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
value={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori kegiatan</Text>}
placeholder='Masukkan nama kategori kegiatan'
label={<Text fw="bold" fz="sm">Nama Kategori Kegiatan</Text>}
placeholder="Masukkan nama kategori kegiatan"
required
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
<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>

View File

@@ -1,7 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, 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 { useRouter } from 'next/navigation';
import { useEffect } from 'react';
@@ -29,31 +29,53 @@ function CreateKategoriKegiatan() {
}
return (
<Box>
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */}
<Group mb="md" align="center">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" p="xs" radius="md" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Box>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Kategori Kegiatan
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Kategori Kegiatan</Title>
<TextInput
value={stateKategori.create.form.nama}
onChange={(val) => {
stateKategori.create.form.nama = val.target.value;
{/* Form */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
value={stateKategori.create.form.nama}
onChange={(val) => (stateKategori.create.form.nama = val.target.value)}
label={<Text fw="bold" fz="sm">Nama Kategori Kegiatan</Text>}
placeholder="Masukkan nama kategori kegiatan"
required
/>
<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)',
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Kegiatan</Text>}
placeholder='Masukkan nama kategori kegiatan'
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}

View File

@@ -1,24 +1,40 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import {
Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconX } from '@tabler/icons-react';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import gotongRoyongState from '../../../_state/lingkungan/gotong-royong';
function KategoriKegiatan() {
const [search, setSearch] = useState("")
return (
<Box>
<HeaderSearch
title='Kategori Kegiatan'
placeholder='pencarian'
placeholder='Cari kategori kegiatan...'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -34,6 +50,14 @@ function ListKategoriKegiatan({ search }: { search: string }) {
const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter()
const {
data,
page,
totalPages,
loading,
load,
} = stateKategori.findMany
const handleHapus = () => {
if (selectedId) {
stateKategori.delete.byId(selectedId)
@@ -43,61 +67,110 @@ function ListKategoriKegiatan({ search }: { search: string }) {
}
useShallowEffect(() => {
stateKategori.findMany.load()
}, [])
load(page, 10, search)
}, [page, search])
const filteredData = (stateKategori.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.nama.toLowerCase().includes(keyword)
);
});
const filteredData = data || []
if (!stateKategori.findMany.data) {
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton h={500} />
<Skeleton height={500} radius="md" />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Kategori Kegiatan'
href='/admin/lingkungan/gotong-royong/kategori-kegiatan/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Kategori</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.nama}</TableTd>
<TableTd>
<Button color="green" onClick={() => router.push(`/admin/lingkungan/gotong-royong/kategori-kegiatan/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button color="red" onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconX size={20} />
</Button>
</TableTd>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Kategori Kegiatan</Title>
<Tooltip label="Tambah Kategori Baru" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/lingkungan/gotong-royong/kategori-kegiatan/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '60%' }}>Nama Kategori</TableTh>
<TableTh style={{ width: '20%' }}>Edit</TableTh>
<TableTh style={{ width: '20%' }}>Delete</TableTh>
</TableTr>
))}
</TableTbody>
</Table>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text fw={500} truncate="end" lineClamp={1}>{item.nama}</Text>
</TableTd>
<TableTd>
<Button
size="xs"
radius="md"
variant="light"
color="green"
leftSection={<IconEdit size={16} />}
onClick={() => router.push(`/admin/lingkungan/gotong-royong/kategori-kegiatan/${item.id}`)}
>
Edit
</Button>
</TableTd>
<TableTd>
<Button
size="xs"
radius="md"
variant="light"
color="red"
leftSection={<IconTrash size={16} />}
onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}
>
Hapus
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={3}>
<Center py={20}>
<Text c="dimmed">Tidak ada kategori kegiatan yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}

View File

@@ -4,7 +4,7 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core';
import { Box, Button, Group, Image, Paper, Select, 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';
@@ -12,7 +12,6 @@ import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
interface FormKegiatanDesa {
judul: string;
deskripsiSingkat: string;
@@ -92,14 +91,11 @@ function EditGotongRoyong() {
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");
}
// Update imageId in global state
if (!uploaded?.id) return toast.error("Gagal upload gambar");
kegiatanDesaState.edit.form.imageId = uploaded.id;
}
await kegiatanDesaState.edit.update()
toast.success("Kegiatan desa berhasil diperbarui!")
router.push("/admin/lingkungan/gotong-royong/kegiatan-desa");
} catch (error) {
console.error("Error updating kegiatan desa:", error);
@@ -108,27 +104,40 @@ function EditGotongRoyong() {
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<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>
<Title order={4} ml="sm" c="dark">
Edit Kegiatan Desa
</Title>
</Group>
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}>
<Stack gap="xs">
<Title order={3}>Edit Kegiatan Desa</Title>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
value={formData.judul}
label={<Text fz="sm" fw="bold">Judul Kegiatan Desa</Text>}
placeholder="masukkan judul kegiatan desa"
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
/>
<Select
label="Kategori Kegiatan"
@@ -141,7 +150,7 @@ function EditGotongRoyong() {
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Lengkap Kegiatan Desa</Text>
<Text fw="bold" fz="sm">Deskripsi Lengkap Kegiatan Desa</Text>
<EditEditor
value={formData.deskripsiLengkap}
onChange={(htmlContent) => {
@@ -150,6 +159,7 @@ function EditGotongRoyong() {
}}
/>
</Box>
<TextInput
label={<Text fz="sm" fw="bold">Tanggal Kegiatan Desa</Text>}
placeholder="masukkan tanggal kegiatan desa"
@@ -169,71 +179,70 @@ function EditGotongRoyong() {
placeholder="masukkan partisipan kegiatan desa"
onChange={(e) => {
const val = Number(e.target.value);
if (!isNaN(val)) {
setFormData({ ...formData, partisipan: val });
}
if (!isNaN(val)) setFormData({ ...formData, partisipan: val });
}}
/>
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<Text fw="bold" fz="sm" mb={6}>Gambar Kegiatan Desa</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }}
radius="md"
p="xl"
>
<Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept>
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={48} color="red" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={48} color="#868e96" stroke={1.5} />
</Dropzone.Idle>
<Stack gap="xs" align="center">
<Text size="md" fw={500}>Drag gambar atau klik untuk pilih file</Text>
<Text size="sm" c="dimmed">Maksimal 5MB, format gambar wajib</Text>
</Stack>
</Group>
</Dropzone>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
loading="lazy"
/>
</Box>
)}
</Box>
{previewImage && (
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
<Image
src={previewImage}
alt="Preview"
radius="md"
style={{ maxHeight: 220, objectFit: 'contain', border: `1px solid ${colors['blue-button']}` }}
loading="lazy"
/>
</Box>
)}
</Box>
<Button bg={colors['blue-button']} onClick={handleSubmit} >
Simpan
</Button>
<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>

View File

@@ -1,121 +1,166 @@
'use client'
import { useProxy } from 'valtio/utils';
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import colors from '@/con/colors';
import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong';
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
function DetailKegiatanDesa() {
const kegiatanDesaState = useProxy(gotongRoyongState.kegiatanDesa)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
const kegiatanDesaState = useProxy(gotongRoyongState.kegiatanDesa);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const params = useParams();
const router = useRouter();
useShallowEffect(() => {
kegiatanDesaState.findUnique.load(params?.id as string)
}, [])
kegiatanDesaState.findUnique.load(params?.id as string);
}, []);
const handleHapus = () => {
if (selectedId) {
kegiatanDesaState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/lingkungan/gotong-royong/kegiatan-desa")
kegiatanDesaState.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
router.push("/admin/lingkungan/gotong-royong/kegiatan-desa");
}
}
};
if (!kegiatanDesaState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={40} />
<Skeleton height={500} radius="md" />
</Stack>
)
);
}
const data = kegiatanDesaState.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 Kegiatan Desa Inovasi</Text>
{kegiatanDesaState.findUnique.data ? (
<Paper key={kegiatanDesaState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Judul Kegiatan Desa Inovasi</Text>
<Text fz={"lg"}>{kegiatanDesaState.findUnique.data?.judul}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Tanggal</Text>
<Text fz={"lg"}>{kegiatanDesaState.findUnique.data?.tanggal
? new Date(kegiatanDesaState.findUnique.data.tanggal).toLocaleDateString()
: "-"}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi Singkat</Text>
<Text fz={"lg"} >{kegiatanDesaState.findUnique.data?.deskripsiSingkat}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: kegiatanDesaState.findUnique.data?.deskripsiLengkap }} />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Kategori Kegiatan</Text>
<Text fz={"lg"}>{kegiatanDesaState.findUnique.data?.kategoriKegiatan?.nama}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Partisipan</Text>
<Text fz={"lg"}>{kegiatanDesaState.findUnique.data?.partisipan}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Lokasi</Text>
<Text fz={"lg"}>{kegiatanDesaState.findUnique.data?.lokasi}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
<Image w={{ base: 150, md: 150, lg: 150 }} src={kegiatanDesaState.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>
{/* Container Detail */}
<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 Kegiatan Desa
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
{/* Judul */}
<Box>
<Text fz="lg" fw="bold">Judul Kegiatan Desa Inovasi</Text>
<Text fz="md" c="dimmed">{data.judul || '-'}</Text>
</Box>
{/* Tanggal */}
<Box>
<Text fz="lg" fw="bold">Tanggal</Text>
<Text fz="md" c="dimmed">
{data.tanggal ? new Date(data.tanggal).toLocaleDateString() : '-'}
</Text>
</Box>
{/* Deskripsi Singkat */}
<Box>
<Text fz="lg" fw="bold">Deskripsi Singkat</Text>
<Text fz="md" c="dimmed">{data.deskripsiSingkat || '-'}</Text>
</Box>
{/* Deskripsi Lengkap */}
<Box>
<Text fz="lg" fw="bold">Deskripsi</Text>
<Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.deskripsiLengkap || '-' }} />
</Box>
{/* Kategori */}
<Box>
<Text fz="lg" fw="bold">Kategori Kegiatan</Text>
<Text fz="md" c="dimmed">{data.kategoriKegiatan?.nama || '-'}</Text>
</Box>
{/* Partisipan */}
<Box>
<Text fz="lg" fw="bold">Partisipan</Text>
<Text fz="md" c="dimmed">{data.partisipan || '-'}</Text>
</Box>
{/* Lokasi */}
<Box>
<Text fz="lg" fw="bold">Lokasi</Text>
<Text fz="md" c="dimmed">{data.lokasi || '-'}</Text>
</Box>
{/* Gambar */}
<Box>
<Text fz="lg" fw="bold">Gambar</Text>
{data.image?.link ? (
<Image
src={data.image.link}
alt={data.judul || 'Gambar Kegiatan Desa'}
w={150}
h={150}
radius="md"
fit="cover"
loading="lazy"
/>
) : (
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
)}
</Box>
{/* Tombol Hapus & Edit */}
<Flex gap="sm" mt={10}>
<Tooltip label="Hapus Kegiatan Desa" withArrow position="top">
<Button
color="red"
onClick={() => {
if (kegiatanDesaState.findUnique.data) {
setSelectedId(kegiatanDesaState.findUnique.data.id);
setModalHapus(true);
}
setSelectedId(data.id);
setModalHapus(true);
}}
disabled={kegiatanDesaState.delete.loading || !kegiatanDesaState.findUnique.data}
color={"red"}
variant="light"
radius="md"
size="md"
>
<IconX size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Kegiatan Desa" withArrow position="top">
<Button
onClick={() => {
if (kegiatanDesaState.findUnique.data) {
router.push(`/admin/lingkungan/gotong-royong/kegiatan-desa/${kegiatanDesaState.findUnique.data.id}/edit`);
}
}}
disabled={!kegiatanDesaState.findUnique.data}
color={"green"}
color="green"
onClick={() => router.push(`/admin/lingkungan/gotong-royong/kegiatan-desa/${data.id}/edit`)}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
) : null}
</Tooltip>
</Flex>
</Stack>
</Paper>
</Stack>
</Paper>
@@ -130,4 +175,4 @@ function DetailKegiatanDesa() {
);
}
export default DetailKegiatanDesa;
export default DetailKegiatanDesa;

View File

@@ -4,7 +4,19 @@ import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core';
import {
Box,
Button,
Group,
Image,
Paper,
Select,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
@@ -12,10 +24,9 @@ import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function CreateKegiatanDesa() {
const router = useRouter();
const stateKegiatanDesa = useProxy(gotongRoyongState.kegiatanDesa)
const stateKegiatanDesa = useProxy(gotongRoyongState.kegiatanDesa);
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
@@ -26,32 +37,33 @@ function CreateKegiatanDesa() {
const resetForm = () => {
stateKegiatanDesa.create.form = {
judul: "",
deskripsiSingkat: "",
deskripsiLengkap: "",
judul: '',
deskripsiSingkat: '',
deskripsiLengkap: '',
tanggal: new Date(),
lokasi: "",
lokasi: '',
partisipan: 0,
imageId: "",
kategoriKegiatanId: "",
imageId: '',
kategoriKegiatanId: '',
};
setPreviewImage(null);
setFile(null);
};
const handleSubmit = async () => {
if (!file) {
return toast.warn("Pilih file gambar terlebih dahulu");
return toast.warn('Silakan pilih file gambar terlebih dahulu');
}
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
})
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal mengupload file");
return toast.error('Gagal mengunggah gambar, silakan coba lagi');
}
stateKegiatanDesa.create.form.imageId = uploaded.id;
@@ -59,153 +71,176 @@ function CreateKegiatanDesa() {
await stateKegiatanDesa.create.create();
resetForm();
router.push("/admin/lingkungan/gotong-royong/kegiatan-desa")
}
router.push('/admin/lingkungan/gotong-royong/kegiatan-desa');
};
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<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">
Tambah Kegiatan Desa
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Kegiatan Desa</Title>
{/* Form */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Upload Gambar */}
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<Text fw="bold" fz="sm" mb={6}>
Gambar Kegiatan
</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
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={180}>
<Dropzone.Accept>
<IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
</Group>
<Text ta="center" mt="sm" size="sm" color="dimmed">
Seret gambar atau klik untuk memilih file (maks 5MB)
</Text>
</Dropzone>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
loading="lazy"
/>
</Box>
)}
</Box>
{previewImage && (
<Box mt="sm" style={{ textAlign: 'center' }}>
<Image
src={previewImage}
alt="Preview Gambar"
radius="md"
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
loading="lazy"
/>
</Box>
)}
</Box>
{/* Input Form */}
<TextInput
label="Judul Kegiatan"
placeholder="Masukkan judul kegiatan"
value={stateKegiatanDesa.create.form.judul}
onChange={(val) => {
stateKegiatanDesa.create.form.judul = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Judul Kegiatan</Text>}
placeholder='Masukkan judul kegiatan'
onChange={(e) => (stateKegiatanDesa.create.form.judul = e.target.value)}
required
/>
<TextInput
label="Deskripsi Singkat"
placeholder="Masukkan deskripsi singkat"
value={stateKegiatanDesa.create.form.deskripsiSingkat}
onChange={(val) => {
stateKegiatanDesa.create.form.deskripsiSingkat = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Deskripsi Singkat</Text>}
placeholder='Masukkan deskripsi singkat'
onChange={(e) => (stateKegiatanDesa.create.form.deskripsiSingkat = e.target.value)}
required
/>
<TextInput
type="number"
min={0}
max={5}
step={0.1} // bisa pakai 0.1 biar support desimal
value={stateKegiatanDesa.create.form.partisipan}
onChange={(val) => {
const value = Number(val.target.value);
// Validasi manual juga boleh (jaga-jaga)
if (value >= 0 && value <= 1000) {
onChange={(e) => {
const value = Number(e.target.value);
if (value >= 0) {
stateKegiatanDesa.create.form.partisipan = value;
}
}}
label={<Text fw={"bold"} fz={"sm"}>Partisipan</Text>}
placeholder='Masukkan partisipan'
label="Partisipan"
placeholder="Masukkan jumlah partisipan"
required
/>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Tanggal</Text>}
label="Tanggal"
type="date"
placeholder="Contoh: 2022-01-01"
value={stateKegiatanDesa.create.form.tanggal ? stateKegiatanDesa.create.form.tanggal.toISOString().split('T')[0] : ''}
value={
stateKegiatanDesa.create.form.tanggal
? stateKegiatanDesa.create.form.tanggal.toISOString().split('T')[0]
: ''
}
onChange={(e) => {
const dateValue = e.currentTarget.value;
stateKegiatanDesa.create.form.tanggal = dateValue ? new Date(dateValue) : new Date();
}}
required
/>
<TextInput
label="Lokasi"
placeholder="Masukkan lokasi kegiatan"
value={stateKegiatanDesa.create.form.lokasi}
onChange={(val) => {
stateKegiatanDesa.create.form.lokasi = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Lokasi</Text>}
placeholder='Masukkan lokasi'
onChange={(e) => (stateKegiatanDesa.create.form.lokasi = e.target.value)}
required
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi Lengkap</Text>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi Lengkap
</Text>
<CreateEditor
value={stateKegiatanDesa.create.form.deskripsiLengkap}
onChange={(val) => {
stateKegiatanDesa.create.form.deskripsiLengkap = val;
}}
onChange={(val) => (stateKegiatanDesa.create.form.deskripsiLengkap = val)}
/>
</Box>
<Select
label="Kategori Kegiatan"
placeholder="Pilih kategori kegiatan"
value={stateKegiatanDesa.create.form.kategoriKegiatanId}
onChange={(val) => {
stateKegiatanDesa.create.form.kategoriKegiatanId = val ?? "";
}}
label={<Text fw={"bold"} fz={"sm"}>Kategori Kegiatan</Text>}
placeholder="Pilih kategori produk"
onChange={(val) => (stateKegiatanDesa.create.form.kategoriKegiatanId = val ?? '')}
data={
gotongRoyongState.kategoriKegiatan.findMany.data?.map((v) => ({
value: v.id,
label: v.nama,
})) || []
}
required
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
{/* Submit */}
<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>

View File

@@ -1,13 +1,29 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import {
Box,
Button,
Center,
Group,
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';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import gotongRoyongState from '../../../_state/lingkungan/gotong-royong';
function KegiatanDesa() {
@@ -15,8 +31,8 @@ function KegiatanDesa() {
return (
<Box>
<HeaderSearch
title='Kegiatan Desa'
placeholder='pencarian'
title="Kegiatan Desa"
placeholder="Cari judul, lokasi, atau kategori kegiatan..."
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -27,71 +43,123 @@ function KegiatanDesa() {
}
function ListKegiatanDesa({ search }: { search: string }) {
const listState = useProxy(gotongRoyongState.kegiatanDesa)
const listState = useProxy(gotongRoyongState.kegiatanDesa);
const router = useRouter();
useEffect(() => {
listState.findMany.load()
}, [])
const filteredData = (listState.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.judul.toLowerCase().includes(keyword) ||
item.lokasi.toLowerCase().includes(keyword) ||
item.kategoriKegiatan?.nama?.toLowerCase().includes(keyword)
);
});
const {
data,
page,
totalPages,
loading,
load,
} = listState.findMany;
if (!listState.findMany.data) {
useShallowEffect(() => {
load(page, 10, search)
}, [page, search])
const filteredData = data || []
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton h={500} />
<Skeleton height={600} radius="md" />
</Stack>
)
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<JudulList
title='List Kegiatan Desa'
href='/admin/lingkungan/gotong-royong/kegiatan-desa/create'
/>
<Box style={{ overflowX: 'auto' }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<TableThead>
<TableTr>
<TableTh>Judul Kegiatan Desa</TableTh>
<TableTh>Kategori Kegiatan Desa</TableTh>
<TableTh>Lokasi Kegiatan Desa</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Kegiatan Desa</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push('/admin/lingkungan/gotong-royong/kegiatan-desa/create')
}
>
Tambah Baru
</Button>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '30%' }}>Judul</TableTh>
<TableTh style={{ width: '25%' }}>Kategori</TableTh>
<TableTh style={{ width: '25%' }}>Lokasi</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>{item.judul}</Text>
</Box>
<Text fw={500} truncate="end" lineClamp={1}>
{item.judul}
</Text>
</TableTd>
<TableTd>{item.kategoriKegiatan?.nama}</TableTd>
<TableTd>{item.lokasi}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/lingkungan/gotong-royong/kegiatan-desa/${item.id}`)}>
<IconDeviceImacCog size={25} />
<Text fz="sm" c="dimmed">
{item.kategoriKegiatan?.nama || '-'}
</Text>
</TableTd>
<TableTd>
<Text fz="sm">{item.lokasi}</Text>
</TableTd>
<TableTd>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() =>
router.push(
`/admin/lingkungan/gotong-royong/kegiatan-desa/${item.id}`
)
}
>
Detail
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</Stack>
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text c="dimmed">
Tidak ada kegiatan desa yang cocok dengan pencarian
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
</Box>
)
);
}
export default KegiatanDesa;

View File

@@ -1,67 +1,116 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { Box, ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { IconBook, IconLeaf, IconSchool } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
function LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter()
const pathname = usePathname()
const tabs = [
{
label: "Filosofi Tri Hita",
value: "filosofi-tri-hita",
href: "/admin/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana"
href: "/admin/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana",
tooltip: "Lihat filosofi Tri Hita Karana",
icon: <IconLeaf size={18} stroke={1.8} />
},
{
label: "Nilai Konservasi Adat",
value: "nilai-konservasi-adat",
href: "/admin/lingkungan/konservasi-adat-bali/nilai-konservasi-adat"
href: "/admin/lingkungan/konservasi-adat-bali/nilai-konservasi-adat",
tooltip: "Kelola nilai konservasi adat",
icon: <IconBook size={18} stroke={1.8} />
},
{
label: "Bentuk Konservasi Berdasarkan Adat",
value: "bentuk-konservasi-berdasarkan-adat",
href: "/admin/lingkungan/konservasi-adat-bali/bentuk-konservasi-berdasarkan-adat"
href: "/admin/lingkungan/konservasi-adat-bali/bentuk-konservasi-berdasarkan-adat",
tooltip: "Lihat bentuk konservasi berdasarkan adat",
icon: <IconSchool size={18} stroke={1.8} />
},
];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
const currentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value)
if (tab) {
router.push(tab.href)
}
if (tab) router.push(tab.href)
setActiveTab(value)
}
useEffect(() => {
const match = tabs.find(tab => tab.href === pathname)
if (match) {
setActiveTab(match.value)
}
if (match) setActiveTab(match.value)
}, [pathname])
return (
<Stack>
<Title order={3}>Konservasi Adat Bali</Title>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
{tabs.map((e, i) => (
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
))}
</TabsList>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value}>
{/* Konten dummy, bisa diganti tergantung routing */}
<></>
<Stack gap="md">
<Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
Konservasi Adat Bali
</Title>
<Tabs
color={colors['blue-button']}
variant="pills"
value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem",
}}
>
{tabs.map((tab, i) => (
<Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
whiteSpace: "nowrap",
transition: "all 0.2s ease",
}}
>
<span style={{
display: "inline-block",
maxWidth: "200px",
overflow: "hidden",
textOverflow: "ellipsis"
}}>
{tab.label}
</span>
</TabsTab>
</Tooltip>
))}
</TabsList>
</ScrollArea>
{tabs.map((tab, i) => (
<TabsPanel key={i} value={tab.value}>
<Box p="md">
{children}
</Box>
</TabsPanel>
))}
</Tabs>
{children}
</Stack>
);
}
export default LayoutTabs;
export default LayoutTabs;

View File

@@ -9,77 +9,105 @@ import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
const KonservasiAdatBaliTextEditor = dynamic(() => import('../../_lib/konservasiAdatBaliTextEditor').then(mod => mod.KonservasiAdatBaliTextEditor), {
ssr: false,
});
const KonservasiAdatBaliTextEditor = dynamic(
() => import('../../_lib/konservasiAdatBaliTextEditor').then(mod => mod.KonservasiAdatBaliTextEditor),
{ ssr: false }
);
function EditBentukKonservasiBerdasarkanAdat() {
const router = useRouter()
const bentukKonservasiState = useProxy(stateKonservasiAdatBali.stateBentukKonservasiBerdasarkanAdat)
const router = useRouter();
const bentukKonservasiState = useProxy(stateKonservasiAdatBali.stateBentukKonservasiBerdasarkanAdat);
const [judul, setJudul] = useState('');
const [content, setContent] = useState('');
useShallowEffect(() => {
if (!bentukKonservasiState.findById.data) {
bentukKonservasiState.findById.initialize(); // biar masuk ke `findFirst` route kamu
bentukKonservasiState.findById.initialize();
}
}, []);
useEffect(() => {
if (bentukKonservasiState.findById.data) {
setJudul(bentukKonservasiState.findById.data.judul ?? '')
setContent(bentukKonservasiState.findById.data.deskripsi ?? '')
setJudul(bentukKonservasiState.findById.data.judul ?? '');
setContent(bentukKonservasiState.findById.data.deskripsi ?? '');
}
}, [bentukKonservasiState.findById.data])
}, [bentukKonservasiState.findById.data]);
const submit = () => {
if (bentukKonservasiState.findById.data) {
bentukKonservasiState.findById.data.judul = judul;
bentukKonservasiState.findById.data.deskripsi = content;
bentukKonservasiState.update.save(bentukKonservasiState.findById.data)
bentukKonservasiState.update.save(bentukKonservasiState.findById.data);
}
router.push('/admin/lingkungan/konservasi-adat-bali/bentuk-konservasi-berdasarkan-adat')
}
router.push('/admin/lingkungan/konservasi-adat-bali/bentuk-konservasi-berdasarkan-adat');
};
return (
<Box>
<Stack gap={'xs'}>
<Box>
<Button
variant={'subtle'}
onClick={() => router.back()}
>
<IconArrowBack color={colors['blue-button']} size={20} />
</Button>
</Box>
<Box>
<Paper bg={colors['white-1']} p={'md'} radius={10} w={{ base: '100%', md: '50%' }}>
<Stack gap={'xs'}>
<Title order={3}>Edit Bentuk Konservasi Berdasarkan Adat</Title>
<Text fw={"bold"}>Judul</Text>
<KonservasiAdatBaliTextEditor
showSubmit={false}
onChange={setJudul}
initialContent={judul}
/>
<Text fw={"bold"}>Content</Text>
<KonservasiAdatBaliTextEditor
showSubmit={false}
onChange={setContent}
initialContent={content}
/>
<Group>
<Button
bg={colors['blue-button']}
onClick={submit}
loading={bentukKonservasiState.update.loading}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
</Stack>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */}
<Group mb="md">
<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 Bentuk Konservasi Berdasarkan Adat
</Title>
</Group>
{/* Form Paper */}
<Paper
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
w={{ base: '100%', md: '50%' }}
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<Box>
<Text fw="bold" mb={6}>
Judul
</Text>
<KonservasiAdatBaliTextEditor
showSubmit={false}
onChange={setJudul}
initialContent={judul}
/>
</Box>
<Box>
<Text fw="bold" mb={6}>
Deskripsi
</Text>
<KonservasiAdatBaliTextEditor
showSubmit={false}
onChange={setContent}
initialContent={content}
/>
</Box>
<Group justify="right" mt="md">
<Button
onClick={submit}
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)',
}}
loading={bentukKonservasiState.update.loading}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}

View File

@@ -8,47 +8,80 @@ import { useProxy } from 'valtio/utils';
import stateKonservasiAdatBali from '../../../_state/lingkungan/konservasi-adat-bali';
function Page() {
const router = useRouter()
const listBentukKonservasiBerdasarkanAdat = useProxy(stateKonservasiAdatBali.stateBentukKonservasiBerdasarkanAdat)
const router = useRouter();
const listBentukKonservasiBerdasarkanAdat = useProxy(
stateKonservasiAdatBali.stateBentukKonservasiBerdasarkanAdat
);
useShallowEffect(() => {
listBentukKonservasiBerdasarkanAdat.findById.load('edit')
}, [])
listBentukKonservasiBerdasarkanAdat.findById.load('edit');
}, []);
if (!listBentukKonservasiBerdasarkanAdat.findById.data) {
return (
<Stack>
<Skeleton radius={10} h={800} />
<Stack py={20} align="center">
<Skeleton radius="md" height={600} width="100%" />
</Stack>
)
);
}
return (
<Paper bg={colors['white-1']} p={'md'} radius={10}>
<Stack gap={"22"}>
<Grid>
<Box p={{ base: 'md', md: 'xl' }}>
<Paper withBorder radius="md" p={{ base: 'md', md: 'lg' }} bg={colors['white-1']}>
{/* Header */}
<Grid align="center" mb="lg">
<GridCol span={{ base: 12, md: 11 }}>
<Title order={2}>Preview Bentuk Konservasi Berdasarkan Adat</Title>
<Title order={3} fw={600} c="dark">
Preview Bentuk Konservasi Berdasarkan Adat
</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button bg={colors['blue-button']} onClick={() => router.push('/admin/lingkungan/konservasi-adat-bali/bentuk-konservasi-berdasarkan-adat/edit')}>
<IconEdit size={16} />
<GridCol span={{ base: 12, md: 1 }} style={{ textAlign: 'right' }}>
<Button
size="sm"
variant="light"
color="green"
radius="md"
leftSection={<IconEdit size={16} />}
onClick={() =>
router.push(
'/admin/lingkungan/konservasi-adat-bali/bentuk-konservasi-berdasarkan-adat/edit'
)
}
>
Edit
</Button>
</GridCol>
</Grid>
<Box>
<Stack gap={'lg'}>
<Paper p={"xl"} bg={colors['BG-trans']}>
<Box px={{ base: 0, md: 30 }}>
<Text fz={{ base: "h3", md: "h2" }} fw={"bold"} dangerouslySetInnerHTML={{ __html: listBentukKonservasiBerdasarkanAdat.findById.data.judul }} />
</Box>
<Box px={{ base: 0, md: 30 }}>
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: listBentukKonservasiBerdasarkanAdat.findById.data.deskripsi }} />
</Box>
</Paper>
</Stack>
</Box>
</Stack>
</Paper>
)
{/* Konten */}
<Stack gap="md">
<Paper radius="md" p={{ base: 'md', md: 'xl' }} bg={colors['BG-trans']} shadow="sm">
<Box mb="md">
<Text
fz={{ base: 'xl', md: '2xl' }}
fw={600}
c="dark"
dangerouslySetInnerHTML={{
__html: listBentukKonservasiBerdasarkanAdat.findById.data.judul,
}}
/>
</Box>
<Box>
<Text
fz={{ base: 'md', md: 'lg' }}
ta="justify"
c="dimmed"
lineClamp={10}
dangerouslySetInnerHTML={{
__html: listBentukKonservasiBerdasarkanAdat.findById.data.deskripsi,
}}
/>
</Box>
</Paper>
</Stack>
</Paper>
</Box>
);
}
export default Page;

View File

@@ -9,78 +9,108 @@ import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
const KonservasiAdatBaliTextEditor = dynamic(() => import('../../_lib/konservasiAdatBaliTextEditor').then(mod => mod.KonservasiAdatBaliTextEditor), {
ssr: false,
});
const KonservasiAdatBaliTextEditor = dynamic(
() =>
import('../../_lib/konservasiAdatBaliTextEditor').then(
(mod) => mod.KonservasiAdatBaliTextEditor
),
{ ssr: false }
);
function EditFilosofiTriHitaKarana() {
const router = useRouter()
const filosofiTriHitaState = useProxy(stateKonservasiAdatBali.stateFilosofiTriHita)
const router = useRouter();
const filosofiTriHitaState = useProxy(stateKonservasiAdatBali.stateFilosofiTriHita);
const [judul, setJudul] = useState('');
const [content, setContent] = useState('');
useShallowEffect(() => {
if (!filosofiTriHitaState.findById.data) {
filosofiTriHitaState.findById.initialize(); // biar masuk ke `findFirst` route kamu
filosofiTriHitaState.findById.initialize();
}
}, []);
useEffect(() => {
if (filosofiTriHitaState.findById.data) {
setJudul(filosofiTriHitaState.findById.data.judul ?? '')
setContent(filosofiTriHitaState.findById.data.deskripsi ?? '')
setJudul(filosofiTriHitaState.findById.data.judul ?? '');
setContent(filosofiTriHitaState.findById.data.deskripsi ?? '');
}
}, [filosofiTriHitaState.findById.data])
}, [filosofiTriHitaState.findById.data]);
const submit = () => {
if (filosofiTriHitaState.findById.data) {
filosofiTriHitaState.findById.data.judul = judul;
filosofiTriHitaState.findById.data.deskripsi = content;
filosofiTriHitaState.update.save(filosofiTriHitaState.findById.data)
filosofiTriHitaState.update.save(filosofiTriHitaState.findById.data);
}
router.push('/admin/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana')
}
router.push('/admin/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana');
};
return (
<Box>
<Stack gap={'xs'}>
<Box>
<Button
variant={'subtle'}
onClick={() => router.back()}
>
<IconArrowBack color={colors['blue-button']} size={20} />
</Button>
</Box>
<Box>
<Paper bg={colors['white-1']} p={'md'} radius={10} w={{ base: '100%', md: '50%' }}>
<Stack gap={'xs'}>
<Title order={3}>Edit Filosofi Tri Hita Karana</Title>
<Text fw={"bold"}>Judul</Text>
<KonservasiAdatBaliTextEditor
showSubmit={false}
onChange={setJudul}
initialContent={judul}
/>
<Text fw={"bold"}>Content</Text>
<KonservasiAdatBaliTextEditor
showSubmit={false}
onChange={setContent}
initialContent={content}
/>
<Group>
<Button
bg={colors['blue-button']}
onClick={submit}
loading={filosofiTriHitaState.update.loading}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
</Stack>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */}
<Group mb="md">
<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 Filosofi Tri Hita Karana
</Title>
</Group>
{/* Form Paper */}
<Paper
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
w={{ base: '100%', md: '50%' }}
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<Box>
<Text fw="bold" mb={6}>
Judul
</Text>
<KonservasiAdatBaliTextEditor
showSubmit={false}
onChange={setJudul}
initialContent={judul}
/>
</Box>
<Box>
<Text fw="bold" mb={6}>
Deskripsi
</Text>
<KonservasiAdatBaliTextEditor
showSubmit={false}
onChange={setContent}
initialContent={content}
/>
</Box>
<Group justify="right" mt="md">
<Button
onClick={submit}
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)',
}}
loading={filosofiTriHitaState.update.loading}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}

View File

@@ -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';
@@ -7,49 +6,73 @@ import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import stateKonservasiAdatBali from '../../../_state/lingkungan/konservasi-adat-bali';
function Page() {
const router = useRouter()
const listFilosofi = useProxy(stateKonservasiAdatBali.stateFilosofiTriHita)
const router = useRouter();
const listFilosofi = useProxy(stateKonservasiAdatBali.stateFilosofiTriHita);
useShallowEffect(() => {
listFilosofi.findById.load('edit')
}, [])
listFilosofi.findById.load('edit');
}, []);
if (!listFilosofi.findById.data) {
return (
<Stack>
<Skeleton radius={10} h={800} />
<Stack py={20}>
<Skeleton radius="md" height={600} />
</Stack>
)
);
}
return (
<Paper bg={colors['white-1']} p={'md'} radius={10}>
<Stack gap={"22"}>
<Grid>
<Box p="md">
<Paper withBorder p={{ base: 'md', md: 'lg' }} radius="md">
<Grid align="center" mb={{ base: 'md', md: 'lg' }}>
<GridCol span={{ base: 12, md: 11 }}>
<Title order={2}>Preview Filosofi Tri Hita Karana</Title>
<Title order={3} fw={600}>
Preview Filosofi Tri Hita Karana
</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button bg={colors['blue-button']} onClick={() => router.push('/admin/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana/edit')}>
<IconEdit size={16} />
<GridCol span={{ base: 12, md: 1 }} style={{ textAlign: 'right' }}>
<Button
size="sm"
variant="light"
color="green"
radius="md"
leftSection={<IconEdit size={16} />}
onClick={() =>
router.push(
'/admin/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana/edit'
)
}
>
Edit
</Button>
</GridCol>
</Grid>
<Box>
<Stack gap={'lg'}>
<Paper p={"xl"} bg={colors['BG-trans']}>
<Box px={{ base: 0, md: 30 }}>
<Text fz={{ base: "h3", md: "h2" }} fw={"bold"} dangerouslySetInnerHTML={{ __html: listFilosofi.findById.data.judul }} />
</Box>
<Box px={{ base: 0, md: 30 }}>
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: listFilosofi.findById.data.deskripsi }} />
</Box>
</Paper>
</Stack>
</Box>
</Stack>
</Paper>
)
<Stack gap="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}
c="black"
dangerouslySetInnerHTML={{ __html: listFilosofi.findById.data.judul }}
/>
</Box>
<Box px={{ base: 0, md: 20 }}>
<Text
fz={{ base: 'md', md: 'lg' }}
ta="justify"
c="dimmed"
lineClamp={10}
dangerouslySetInnerHTML={{ __html: listFilosofi.findById.data.deskripsi }}
/>
</Box>
</Paper>
</Stack>
</Paper>
</Box>
);
}
export default Page;

View File

@@ -9,77 +9,105 @@ import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
const KonservasiAdatBaliTextEditor = dynamic(() => import('../../_lib/konservasiAdatBaliTextEditor').then(mod => mod.KonservasiAdatBaliTextEditor), {
ssr: false,
});
const KonservasiAdatBaliTextEditor = dynamic(
() => import('../../_lib/konservasiAdatBaliTextEditor').then(mod => mod.KonservasiAdatBaliTextEditor),
{ ssr: false }
);
function EditNilaiKonservasiAdat() {
const router = useRouter()
const nilaiKonservasiState = useProxy(stateKonservasiAdatBali.stateNilaiKonservasiAdat)
const router = useRouter();
const nilaiKonservasiState = useProxy(stateKonservasiAdatBali.stateNilaiKonservasiAdat);
const [judul, setJudul] = useState('');
const [content, setContent] = useState('');
useShallowEffect(() => {
if (!nilaiKonservasiState.findById.data) {
nilaiKonservasiState.findById.initialize(); // biar masuk ke `findFirst` route kamu
nilaiKonservasiState.findById.initialize();
}
}, []);
useEffect(() => {
if (nilaiKonservasiState.findById.data) {
setJudul(nilaiKonservasiState.findById.data.judul ?? '')
setContent(nilaiKonservasiState.findById.data.deskripsi ?? '')
setJudul(nilaiKonservasiState.findById.data.judul ?? '');
setContent(nilaiKonservasiState.findById.data.deskripsi ?? '');
}
}, [nilaiKonservasiState.findById.data])
}, [nilaiKonservasiState.findById.data]);
const submit = () => {
if (nilaiKonservasiState.findById.data) {
nilaiKonservasiState.findById.data.judul = judul;
nilaiKonservasiState.findById.data.deskripsi = content;
nilaiKonservasiState.update.save(nilaiKonservasiState.findById.data)
nilaiKonservasiState.update.save(nilaiKonservasiState.findById.data);
}
router.push('/admin/lingkungan/konservasi-adat-bali/nilai-konservasi-adat')
}
router.push('/admin/lingkungan/konservasi-adat-bali/nilai-konservasi-adat');
};
return (
<Box>
<Stack gap={'xs'}>
<Box>
<Button
variant={'subtle'}
onClick={() => router.back()}
>
<IconArrowBack color={colors['blue-button']} size={20} />
</Button>
</Box>
<Box>
<Paper bg={colors['white-1']} p={'md'} radius={10} w={{ base: '100%', md: '50%' }}>
<Stack gap={'xs'}>
<Title order={3}>Edit Nilai Konservasi Adat</Title>
<Text fw={"bold"}>Judul</Text>
<KonservasiAdatBaliTextEditor
showSubmit={false}
onChange={setJudul}
initialContent={judul}
/>
<Text fw={"bold"}>Content</Text>
<KonservasiAdatBaliTextEditor
showSubmit={false}
onChange={setContent}
initialContent={content}
/>
<Group>
<Button
bg={colors['blue-button']}
onClick={submit}
loading={nilaiKonservasiState.update.loading}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
</Stack>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */}
<Group mb="md">
<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 Nilai Konservasi Adat
</Title>
</Group>
{/* Form Paper */}
<Paper
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
w={{ base: '100%', md: '50%' }}
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<Box>
<Text fw="bold" mb={6}>
Judul
</Text>
<KonservasiAdatBaliTextEditor
showSubmit={false}
onChange={setJudul}
initialContent={judul}
/>
</Box>
<Box>
<Text fw="bold" mb={6}>
Deskripsi
</Text>
<KonservasiAdatBaliTextEditor
showSubmit={false}
onChange={setContent}
initialContent={content}
/>
</Box>
<Group justify="right" mt="md">
<Button
onClick={submit}
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)',
}}
loading={nilaiKonservasiState.update.loading}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}

View File

@@ -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';
@@ -8,47 +7,70 @@ import { useProxy } from 'valtio/utils';
import stateKonservasiAdatBali from '../../../_state/lingkungan/konservasi-adat-bali';
function Page() {
const router = useRouter()
const listNilaiKonservasiAdat = useProxy(stateKonservasiAdatBali.stateNilaiKonservasiAdat)
const router = useRouter();
const listNilaiKonservasiAdat = useProxy(stateKonservasiAdatBali.stateNilaiKonservasiAdat);
useShallowEffect(() => {
listNilaiKonservasiAdat.findById.load('edit')
}, [])
listNilaiKonservasiAdat.findById.load('edit');
}, []);
if (!listNilaiKonservasiAdat.findById.data) {
return (
<Stack>
<Skeleton radius={10} h={800} />
<Stack py={20}>
<Skeleton radius="md" height={600} />
</Stack>
)
);
}
return (
<Paper bg={colors['white-1']} p={'md'} radius={10}>
<Stack gap={"22"}>
<Grid>
<Box p="md">
<Paper withBorder p={{ base: 'md', md: 'lg' }} radius="md">
<Grid align="center" mb={{ base: 'md', md: 'lg' }}>
<GridCol span={{ base: 12, md: 11 }}>
<Title order={2}>Preview Nilai Konservasi Adat</Title>
<Title order={3} fw={600}>
Preview Nilai Konservasi Adat
</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button bg={colors['blue-button']} onClick={() => router.push('/admin/lingkungan/konservasi-adat-bali/nilai-konservasi-adat/edit')}>
<IconEdit size={16} />
<GridCol span={{ base: 12, md: 1 }} style={{ textAlign: 'right' }}>
<Button
size="sm"
variant="light"
color="green"
radius="md"
leftSection={<IconEdit size={16} />}
onClick={() =>
router.push('/admin/lingkungan/konservasi-adat-bali/nilai-konservasi-adat/edit')
}
>
Edit
</Button>
</GridCol>
</Grid>
<Box>
<Stack gap={'lg'}>
<Paper p={"xl"} bg={colors['BG-trans']}>
<Box px={{ base: 0, md: 30 }}>
<Text fz={{ base: "h3", md: "h2" }} fw={"bold"} dangerouslySetInnerHTML={{ __html: listNilaiKonservasiAdat.findById.data.judul }} />
</Box>
<Box px={{ base: 0, md: 30 }}>
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: listNilaiKonservasiAdat.findById.data.deskripsi }} />
</Box>
</Paper>
</Stack>
</Box>
</Stack>
</Paper>
)
<Stack gap="md">
<Paper p={{ base: 'md', md: 'xl' }} bg="#ECEEF8" radius="md">
<Box mb="md">
<Text
fz={{ base: 'xl', md: '2xl' }}
fw={600}
c="black"
dangerouslySetInnerHTML={{ __html: listNilaiKonservasiAdat.findById.data.judul }}
/>
</Box>
<Box>
<Text
fz={{ base: 'md', md: 'lg' }}
ta="justify"
c="dimmed"
lineClamp={10}
dangerouslySetInnerHTML={{ __html: listNilaiKonservasiAdat.findById.data.deskripsi }}
/>
</Box>
</Paper>
</Stack>
</Paper>
</Box>
);
}
export default Page;

View File

@@ -2,8 +2,9 @@
'use client'
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
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();
@@ -45,49 +46,75 @@ function LayoutTabsPengelolaanSampahBankSampah({ children }: { children: React.R
}, [pathname]);
return (
<Stack gap="md">
<Title order={3} mb="sm">Pengelolaan Sampah Bank Sampah</Title>
<Tabs
value={activeTab}
onChange={handleTabChange}
<Stack gap="lg">
<Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
Pengelolaan Sampah Bank Sampah
</Title>
<Tabs
color={colors["blue-button"]}
variant="pills"
radius="md"
value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
<TabsList>
{tabs.map((tab) => (
<Tooltip
key={tab.value}
label={tab.tooltip}
position="top"
withArrow
transitionProps={{ transition: 'pop', duration: 300 }}
>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
padding: '10px 20px',
height: 'auto',
minHeight: 44,
}}
{/* ✅ Scroll horizontal wrapper */}
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem",
}}
>
{tabs.map((tab, i) => (
<Tooltip
key={i}
label={tab.tooltip}
position="bottom"
withArrow
transitionProps={{ transition: 'pop', duration: 200 }}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
<TabsPanel
value={activeTab || ''}
pt="lg"
style={{
minHeight: '60vh',
}}
>
{children}
</TabsPanel>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
</ScrollArea>
{tabs.map((tab, i) => (
<TabsPanel
key={i}
value={tab.value}
style={{
padding: "1.5rem",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
borderRadius: "1rem",
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
minHeight: "60vh",
}}
>
{children}
</TabsPanel>
))}
</Tabs>
</Stack>
);
}
export default LayoutTabsPengelolaanSampahBankSampah;
export default LayoutTabsPengelolaanSampahBankSampah;

View File

@@ -4,14 +4,23 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import SelectIconProgramEdit from '@/app/admin/(dashboard)/_com/selectIconEdit';
import programPenghijauanState from '@/app/admin/(dashboard)/_state/lingkungan/program-penghijauan';
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';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
interface FormProgramPenghijauan {
name: string;
deskripsi: string;
@@ -19,19 +28,32 @@ interface FormProgramPenghijauan {
icon: string;
}
type IconKey = 'ekowisata' | 'kompetisi' | 'wisata' | 'ekonomi' | 'sampah' | 'truck' | 'scale' | 'clipboard' | 'trash' | 'lingkunganSehat' | 'sumberOksigen' | 'ekonomiBerkelanjutan' | 'mencegahBencana';
type IconKey =
| 'ekowisata'
| 'kompetisi'
| 'wisata'
| 'ekonomi'
| 'sampah'
| 'truck'
| 'scale'
| 'clipboard'
| 'trash'
| 'lingkunganSehat'
| 'sumberOksigen'
| 'ekonomiBerkelanjutan'
| 'mencegahBencana';
function EditProgramPenghijauan() {
const stateProgramPenghijauan = useProxy(programPenghijauanState)
const params = useParams()
const stateProgramPenghijauan = useProxy(programPenghijauanState);
const params = useParams();
const router = useRouter();
const [formData, setFormData] = useState<FormProgramPenghijauan>({
name: '',
deskripsi: '',
judul: '',
icon: '',
})
});
useEffect(() => {
const loadProgramPenghijauan = async () => {
@@ -41,7 +63,6 @@ function EditProgramPenghijauan() {
try {
const data = await stateProgramPenghijauan.update.load(id);
if (data) {
// ⬇️ FIX PENTING: tambahkan ini
stateProgramPenghijauan.update.id = id;
stateProgramPenghijauan.update.form = {
@@ -59,16 +80,14 @@ function EditProgramPenghijauan() {
});
}
} catch (error) {
console.error("Error loading program penghijauan:", error);
toast.error("Gagal memuat data program penghijauan");
console.error('Error loading program penghijauan:', error);
toast.error('Gagal memuat data program penghijauan');
}
}
};
loadProgramPenghijauan();
}, [params?.id]);
const handleSubmit = async () => {
try {
stateProgramPenghijauan.update.form = {
@@ -77,49 +96,74 @@ function EditProgramPenghijauan() {
deskripsi: formData.deskripsi.trim(),
judul: formData.judul.trim(),
icon: formData.icon.trim(),
}
};
await stateProgramPenghijauan.update.submit();
router.push("/admin/lingkungan/program-penghijauan");
router.push('/admin/lingkungan/program-penghijauan');
} catch (error) {
console.error("Error updating program penghijauan:", error);
toast.error("Gagal memuat data program penghijauan");
console.error('Error updating program penghijauan:', error);
toast.error('Gagal memperbarui program penghijauan');
}
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
};
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={3}>Edit Program Penghijauan</Title>
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header dengan back button */}
<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 Program Penghijauan
</Title>
</Group>
{/* Card utama */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
value={formData.name}
label={<Text fz={"sm"} fw={"bold"}>Nama Program Penghijauan</Text>}
placeholder="masukkan nama program penghijauan"
onChange={(val) => {
label="Nama Program Penghijauan"
placeholder="Masukkan nama program penghijauan"
onChange={(val) =>
setFormData({
...formData,
name: val.target.value
name: val.target.value,
})
}}
}
required
/>
<TextInput
value={formData.judul}
label={<Text fz={"sm"} fw={"bold"}>Judul Deskripsi Program Penghijauan</Text>}
placeholder="masukkan judul deskripsi program penghijauan"
onChange={(val) => {
label="Judul Deskripsi Program Penghijauan"
placeholder="Masukkan judul deskripsi program penghijauan"
onChange={(val) =>
setFormData({
...formData,
judul: val.target.value
judul: val.target.value,
})
}}
}
required
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
@@ -128,16 +172,35 @@ function EditProgramPenghijauan() {
}}
/>
</Box>
<Box>
<Text fz={"sm"} fw={"bold"}>Ikon Program Penghijauan</Text>
<Text fw="bold" fz="sm" mb={6}>
Ikon Program Penghijauan
</Text>
<SelectIconProgramEdit
value={formData.icon as IconKey}
onChange={(value) => {
setFormData((prev) => ({ ...prev, icon: value }));
stateProgramPenghijauan.update.form.icon = value;
}} />
}}
/>
</Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
{/* Tombol simpan */}
<Group justify="flex-end">
<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>

View File

@@ -1,17 +1,20 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconChartLine, IconChristmasTreeFilled, IconClipboard, IconEdit, IconHomeEco, IconLeaf, IconRecycle, IconScale, IconShieldFilled, IconTent, IconTrash, IconTrendingUp, IconTrophy, IconTruck, IconX } from '@tabler/icons-react';
import {
IconArrowBack, IconChartLine, IconChristmasTreeFilled, IconClipboard,
IconEdit, IconHomeEco, IconLeaf, IconRecycle, IconScale,
IconShieldFilled, IconTent, IconTrash, IconTrendingUp,
IconTrophy, IconTruck
} from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import React, { useState } from 'react';
import { useProxy } from 'valtio/utils';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import programPenghijauanState from '../../../_state/lingkungan/program-penghijauan';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailProgramPenghijauan() {
const [modalHapus, setModalHapus] = useState(false)
const stateProgramPenghijauan = useProxy(programPenghijauanState)
@@ -50,72 +53,97 @@ function DetailProgramPenghijauan() {
if (!stateProgramPenghijauan.findUnique.data) {
return (
<Stack>
<Skeleton h={500} />
<Stack py={10}>
<Skeleton height={500} radius="md" />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Program Penghijauan</Text>
const data = stateProgramPenghijauan.findUnique.data
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
return (
<Box py={10}>
{/* Tombol kembali */}
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15}
>
Kembali
</Button>
{/* Konten detail */}
<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 Program Penghijauan
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Program Penghijauan</Text>
<Text fz={"lg"}>{stateProgramPenghijauan.findUnique.data?.name}</Text>
<Text fz="lg" fw="bold">Nama Program</Text>
<Text fz="md" c="dimmed">{data?.name || '-'}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Ikon Program Penghijauan</Text>
{iconMap[stateProgramPenghijauan.findUnique.data?.icon] && (
<Box title={stateProgramPenghijauan.findUnique.data?.icon}>
{React.createElement(iconMap[stateProgramPenghijauan.findUnique.data?.icon], { size: 24 })}
<Text fz="lg" fw="bold">Ikon Program</Text>
{iconMap[data?.icon] ? (
<Box title={data?.icon}>
{React.createElement(iconMap[data.icon], { size: 28, color: colors['blue-button'] })}
</Box>
) : (
<Text fz="sm" c="dimmed">Tidak ada ikon</Text>
)}
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Judul Deskripsi Program Penghijauan</Text>
<Text fz={"lg"}>{stateProgramPenghijauan.findUnique.data?.judul}</Text>
<Text fz="lg" fw="bold">Judul Deskripsi</Text>
<Text fz="md" c="dimmed">{data?.judul || '-'}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi Program Penghijauan</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: stateProgramPenghijauan.findUnique.data?.deskripsi }}></Text>
<Text fz="lg" fw="bold">Deskripsi</Text>
<Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data?.deskripsi || '-' }} />
</Box>
<Box>
<Flex gap={"xs"} mt={10}>
{/* Tombol aksi */}
<Group gap="sm" mt="md">
<Tooltip label="Hapus Program" withArrow position="top">
<Button
color="red"
onClick={() => {
if (stateProgramPenghijauan.findUnique.data) {
setSelectedId(stateProgramPenghijauan.findUnique.data.id);
setModalHapus(true);
}
setSelectedId(data.id);
setModalHapus(true);
}}
disabled={stateProgramPenghijauan.delete.loading || !stateProgramPenghijauan.findUnique.data}
color={"red"}
variant="light"
radius="md"
size="md"
>
<IconX size={20} />
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Program" withArrow position="top">
<Button
onClick={() => {
if (stateProgramPenghijauan.findUnique.data) {
router.push(`/admin/lingkungan/program-penghijauan/${stateProgramPenghijauan.findUnique.data.id}/edit`);
}
}}
disabled={!stateProgramPenghijauan.findUnique.data}
color={"green"}
color="green"
onClick={() => router.push(`/admin/lingkungan/program-penghijauan/${data.id}/edit`)}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Tooltip>
</Group>
</Stack>
</Paper>
</Stack>
@@ -126,7 +154,7 @@ function DetailProgramPenghijauan() {
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus program penghijauan ini?"
text="Apakah Anda yakin ingin menghapus program penghijauan ini?"
/>
</Box>
);

View File

@@ -1,6 +1,16 @@
'use client'
'use client';
import colors from '@/con/colors';
import { Box, Button, Group, 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 { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
@@ -8,59 +18,104 @@ import CreateEditor from '../../../_com/createEditor';
import SelectIconProgram from '../../../_com/selectIcon';
import programPenghijauanState from '../../../_state/lingkungan/program-penghijauan';
function CreateProgramPenghijauan() {
const stateCreate = useProxy(programPenghijauanState)
const stateCreate = useProxy(programPenghijauanState);
const router = useRouter();
const resetForm = () => {
stateCreate.create.form = {
name: "",
deskripsi: "",
judul: "",
icon: "",
}
}
name: '',
deskripsi: '',
judul: '',
icon: '',
};
};
const handleSubmit = async () => {
await stateCreate.create.create();
resetForm();
router.push("/admin/lingkungan/program-penghijauan")
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
router.push('/admin/lingkungan/program-penghijauan');
};
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={3}>Create Program Penghijauan</Title>
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Tombol back + title */}
<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">
Tambah Program Penghijauan
</Title>
</Group>
{/* Form */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Nama Program Penghijauan</Text>}
placeholder="masukkan nama program penghijauan"
onChange={(val) => stateCreate.create.form.name = val.target.value}
label={<Text fz="sm" fw="bold">Nama Program Penghijauan</Text>}
placeholder="Masukkan nama program penghijauan"
value={stateCreate.create.form.name || ''}
onChange={(e) => (stateCreate.create.form.name = e.target.value)}
required
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Ikon Program Penghijauan</Text>
<SelectIconProgram onChange={(value) => stateCreate.create.form.icon = value} />
</Box>
<TextInput
onChange={(e) => stateCreate.create.form.judul = e.currentTarget.value}
label={<Text fw={"bold"} fz={"sm"}>Judul Deskripsi Program Penghijauan</Text>}
placeholder='Masukkan judul deskripsi program penghijauan'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Program Penghijauan</Text>
<CreateEditor
value={stateCreate.create.form.deskripsi}
onChange={(htmlContent) => stateCreate.create.form.deskripsi = htmlContent}
<Text fz="sm" fw="bold" mb={6}>
Ikon Program Penghijauan
</Text>
<SelectIconProgram
onChange={(value) => (stateCreate.create.form.icon = value)}
/>
</Box>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
<TextInput
label={<Text fz="sm" fw="bold">Judul Deskripsi Program Penghijauan</Text>}
placeholder="Masukkan judul deskripsi program penghijauan"
value={stateCreate.create.form.judul || ''}
onChange={(e) => (stateCreate.create.form.judul = e.target.value)}
required
/>
<Box>
<Text fz="sm" fw="bold" mb={6}>
Deskripsi Program Penghijauan
</Text>
<CreateEditor
value={stateCreate.create.form.deskripsi}
onChange={(htmlContent) =>
(stateCreate.create.form.deskripsi = htmlContent)
}
/>
</Box>
<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>

View File

@@ -2,30 +2,55 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import {
IconChartLine, IconChristmasTreeFilled, IconClipboardTextFilled, IconDeviceImac, IconHomeEco, IconLeaf,
IconRecycle, IconScale, IconSearch, IconShieldFilled, IconTent,
Box,
Button,
Center,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip
} from '@mantine/core';
import {
IconChartLine,
IconChristmasTreeFilled,
IconClipboardTextFilled,
IconDeviceImac,
IconHomeEco,
IconLeaf,
IconRecycle,
IconScale,
IconSearch,
IconShieldFilled,
IconTent,
IconTrashFilled,
IconTrendingUp,
IconTrophy,
IconTruckFilled
IconTruckFilled,
IconPlus,
} from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import programPenghijauanState from '../../_state/lingkungan/program-penghijauan';
function ProgramPenghijauan() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Program Penghijauan'
placeholder='pencarian'
placeholder='Cari program penghijauan...'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -41,18 +66,10 @@ function ListProgramPenghijauan({ search }: { search: string }) {
const router = useRouter();
useEffect(() => {
load(page, 10)
}, [page])
load(page, 10, search)
}, [page, search])
const filteredData = (data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.deskripsi.toLowerCase().includes(keyword) ||
item.judul.toLowerCase().includes(keyword) ||
item.icon.toLowerCase().includes(keyword)
);
});
const filteredData = data || []
const iconMap: Record<string, React.FC<any>> = {
ekowisata: IconLeaf,
@@ -73,93 +90,104 @@ function ListProgramPenghijauan({ search }: { search: string }) {
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={650} />
<Skeleton height={600} radius="md" />
</Stack>
);
}
if (data.length === 0) {
return (
<Box py={10}>
<Paper p="md" >
<Stack>
<JudulList
title='List Program Penghijauan'
href='/admin/lingkungan/program-penghijauan/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Nama Program Penghijauan</TableTh>
<TableTh>Judul Deskripsi Program Penghijauan</TableTh>
<TableTh>Ikon</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
</Table>
<Text ta="center">Tidak ada data program penghijauan yang tersedia</Text>
</Stack>
</Paper>
</Box >
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'} h={{ base: 'auto', md: 650 }}>
<JudulList
title='List Program Penghijauan'
href='/admin/lingkungan/program-penghijauan/create'
/>
<Box style={{ overflowY: 'auto' }}>
<Table striped withTableBorder withRowBorders>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
{/* Header Section */}
<Box mb="md" display="flex" style={{ justifyContent: 'space-between', alignItems: 'center' }}>
<Title order={4}>Daftar Program Penghijauan</Title>
<Tooltip label="Tambah Program Penghijauan" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/lingkungan/program-penghijauan/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Box>
{/* Table Section */}
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh>
<TableTh style={{ width: '20%' }}>Nama Program Penghijauan</TableTh>
<TableTh style={{ width: '35%' }}>Judul Deskripsi Program Penghijauan</TableTh>
<TableTh style={{ width: '10%' }}>Ikon</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Detail</TableTh>
<TableTh style={{ width: '25%' }}>Nama Program</TableTh>
<TableTh style={{ width: '35%' }}>Judul / Deskripsi</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Ikon</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd style={{ width: '5%', textAlign: 'center' }}>{index + 1}</TableTd>
<TableTd style={{ width: '20%', wordWrap: 'break-word' }}>{item.name}</TableTd>
<TableTd style={{ width: '35%', wordWrap: 'break-word' }}>
<Text lineClamp={1} fz={"sm"} dangerouslySetInnerHTML={{ __html: item.judul }}/>
</TableTd>
<TableTd style={{ width: '10%' }}>
{iconMap[item.icon] && (
<Box title={item.icon}>
{React.createElement(iconMap[item.icon], { size: 24 })}
</Box>
)}
</TableTd>
<TableTd style={{ width: '15%', textAlign: 'center' }}>
<Button onClick={() => router.push(`/admin/lingkungan/program-penghijauan/${item.id}`)}>
<IconDeviceImac size={25} />
</Button>
{filteredData.length > 0 ? (
filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd style={{ textAlign: 'center' }}>{index + 1}</TableTd>
<TableTd>
<Text fw={500} truncate="end" lineClamp={1}>{item.name}</Text>
</TableTd>
<TableTd>
<Text lineClamp={1} fz="sm" c="dimmed" dangerouslySetInnerHTML={{ __html: item.judul }} />
</TableTd>
<TableTd style={{ textAlign: 'center' }}>
{iconMap[item.icon] && (
<Box title={item.icon} mx="auto">
{React.createElement(iconMap[item.icon], { size: 22 })}
</Box>
)}
</TableTd>
<TableTd style={{ textAlign: 'center' }}>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImac size={16} />}
onClick={() => router.push(`/admin/lingkungan/program-penghijauan/${item.id}`)}
>
Detail
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={5}>
<Center py={20}>
<Text c="dimmed">Tidak ada data program penghijauan yang cocok</Text>
</Center>
</TableTd>
</TableTr>
))}
)}
</TableTbody>
</Table>
</Box>
</Paper>
{/* Pagination */}
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
</Box>
);
}
export default ProgramPenghijauan;

View File

@@ -1,15 +1,53 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function kategoriKegiatanFindMany() {
const data = await prisma.kategoriKegiatan.findMany();
return {
success: true,
data: data.map((item: any) => {
return {
id: item.id,
nama: item.nama,
}
}),
export default async function kategoriKegiatanFindMany(context: Context) {
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || "";
const skip = (page - 1) * limit;
// Buat where clause
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [{ nama: { contains: search, mode: "insensitive" } }];
}
try {
const [data, total] = await Promise.all([
prisma.kategoriKegiatan.findMany({
where: where,
skip,
take: limit,
orderBy: { createdAt: "desc" },
}),
prisma.kategoriKegiatan.count({
where: where,
}),
]);
return {
success: true,
data: data.map((item: any) => {
return {
id: item.id,
nama: item.nama,
};
}),
message: "Success fetch administrasi online with pagination",
page,
limit,
totalPages: Math.ceil(total / limit),
total,
};
}
} catch (e) {
console.error("Find many paginated error:", e);
return {
success: false,
message: "Failed fetch administrasi online with pagination",
};
}
}

View File

@@ -24,7 +24,7 @@ export default async function pengelolaanSampahFindMany(context: Context) {
where,
skip,
take: limit,
orderBy: { createdAt: 'desc' },
orderBy: { createdAt: 'asc' },
}),
prisma.pengelolaanSampah.count({
where,

View File

@@ -1,105 +1,83 @@
import colors from '@/con/colors';
import { Box, Center, Image, List, ListItem, Paper, SimpleGrid, Stack, Text } from '@mantine/core';
import BackButton from '../darmasaba/(pages)/desa/layanan/_com/BackButto';
import { Stack, Box, Container, Grid, GridCol, Group, Paper, TextInput, Text, Image, Flex, Button } from '@mantine/core';
import { IconCalendar, IconMapPin, IconSearch, IconUsersGroup } from '@tabler/icons-react';
import React from 'react';
const data1 = [
{
id: 1,
judul: 'Peran Pecalang dalam Keamanan Desa',
image: '/api/img/pecalang.png',
pengertian: 'Pecalang adalah petugas keamanan adat di Bali yang memiliki peran penting dalam menjaga ketertiban dan budaya lokal. Tugas mereka meliputi:',
deskripsi: <List>
<ListItem fz={{ base: 'h4', md: 'lg' }}>Mengamankan upacara adat dan kegiatan keagamaan.</ListItem>
<ListItem fz={{ base: 'h4', md: 'lg' }}>Mengatur lalu lintas saat ada perayaan atau kegiatan besar.</ListItem>
<ListItem fz={{ base: 'h4', md: 'lg' }}>Berpatroli untuk mencegah gangguan keamanan di lingkungan desa.</ListItem>
<ListItem fz={{ base: 'h4', md: 'lg' }}>Berkoordinasi dengan aparat desa dan kepolisian dalam penanganan situasi darurat.</ListItem>
</List>
},
{
id: 2,
judul: 'Patwal (Patroli Pengawal) Desa',
image: '/api/img/patwal-1.png',
pengertian: 'Selain Pecalang, Desa Darmasaba juga memiliki Patwal yang bertugas menjaga keamanan sehari-hari. Peran mereka antara lain:',
deskripsi: <List>
<ListItem fz={{ base: 'h4', md: 'lg' }}>Berpatroli secara rutin untuk memastikan lingkungan tetap aman.</ListItem>
<ListItem fz={{ base: 'h4', md: 'lg' }}>Menjaga ketertiban lalu lintas di area desa.</ListItem>
<ListItem fz={{ base: 'h4', md: 'lg' }}>Melakukan tindakan preventif terhadap potensi gangguan keamanan.</ListItem>
<ListItem fz={{ base: 'h4', md: 'lg' }}>Siap siaga dalam keadaan darurat untuk membantu warga.</ListItem>
</List>
},
{
id: 3,
judul: 'Layanan Keamanan yang Tersedia',
image: '/api/img/pospecalang.png',
pengertian: 'Jika terjadi keadaan darurat atau membutuhkan bantuan keamanan, warga dapat menghubungi:',
deskripsi: <List>
<ListItem fz={{ base: 'h4', md: 'lg' }}>Pos Pecalang Desa: [Masukkan Nomor Kontak].</ListItem>
<ListItem fz={{ base: 'h4', md: 'lg' }}>Patwal Desa Darmasaba: [Masukkan Nomor Kontak].</ListItem>
<ListItem fz={{ base: 'h4', md: 'lg' }}>Polsek Terdekat: 110 (Layanan Kepolisian).</ListItem>
</List>
},
{
id: 4,
judul: 'Program Keamanan Desa',
image: '/api/img/rond.png',
pengertian: 'Untuk meningkatkan keamanan, Desa Darmasaba menjalankan berbagai program, seperti:',
deskripsi: <List>
<ListItem fz={{ base: 'h4', md: 'lg' }}>Ronda Malam Warga: Kegiatan jaga malam secara bergilir oleh warga bersama Pecalang dan Patwal.</ListItem>
<ListItem fz={{ base: 'h4', md: 'lg' }}>Sosialisasi Keamanan: Edukasi bagi warga tentang cara menjaga keamanan lingkungan.</ListItem>
<ListItem fz={{ base: 'h4', md: 'lg' }}> Pengawasan Kamera CCTV: Memantau titik- titik strategis untuk mencegah tindak kejahatan.</ListItem>
</List>
}
]
import Link from 'next/link';
function Page() {
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box>
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
Keamanan Lingkungan (Pecalang / Patwal)
</Text>
<Text px={{ base: 20, md: 150 }} ta={"center"} 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>
</Box>
<Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'}>
<SimpleGrid
pb={10}
cols={{
base: 1,
md: 3,
}}>
{data1.map((v, k) => {
return (
<Paper radius={10} key={k} bg={colors["white-trans-1"]}>
<Stack gap={'xs'}>
<Center px={10} py={20}>
<Image src={v.image} alt='' />
</Center>
<Box px={'lg'}>
<Box pb={20}>
<Text pb={10} c={colors["blue-button"]} fw={"bold"} fz={"h3"}>
{v.judul}
</Text>
<Text pb={10} fz={"h4"} ta={'justify'}>
{v.pengertian}
</Text>
<Box px={10}>
{v.deskripsi}
</Box>
</Box>
</Box>
</Stack>
</Paper>
)
})}
</SimpleGrid>
<Stack pos="relative" bg={colors.Bg} py="xl" gap="md">
{/* Header */}
<Container size="lg" px="md">
<Stack align="center" gap={0} mb="xl">
<Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" ta="center">
Program Gotong Royong
</Text>
<Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" ta="center">
Desa Darmasaba
</Text>
</Stack>
</Container>
{/* Tabs Menu */}
<Box px={{ base: "md", md: "xl" }} py="md" bg={colors['BG-trans']} mb="md">
<Grid align="center" justify="space-between" mb={20}>
<GridCol span={{ base: 12, md: 8 }}>
<Group gap="md" wrap="wrap">
<Paper bg={colors['blue-button']} radius="xl" py={5} px={20}>
<Text c={colors['white-1']} size="sm">
Semua
</Text>
</Paper>
{['Kebersihan', 'Infrastruktur', 'Sosial', 'Lingkungan'].map((kategori) => (
<Paper key={kategori} bg={colors['blue-button-trans']} radius="xl" py={5} px={20}>
<Text size="sm">
{kategori}
</Text>
</Paper>
))}
</Group>
</GridCol>
<GridCol span={{ base: 12, md: 4 }}>
<TextInput
radius="lg"
placeholder="Cari Program Gotong Royong"
leftSection={<IconSearch size={18} />}
w="100%"
/>
</GridCol>
</Grid>
<Paper p={"xl"} bg={colors['white-trans-1']} w={{ base: "100%", md: "100%" }}>
<Stack gap={'xs'}>
<Image radius={20} src={'/api/img/gotong-royong.png'} w={'100%'} alt='' />
<Text fw={"bold"} fz={{ base: "h2", md: "h1" }}>Membangun Fasilitas Desa</Text>
<Group>
<Paper py={5} px={20} bg={colors['blue-button-trans']} radius={20}>
<Text c={colors['white-1']}>Sosial</Text>
</Paper>
</Group>
<Text fz={{ base: "h4", md: "h3" }}>
Program Pembangunan Fasilitas Desa Maju, Masyarakat Sejahtera.
</Text>
<Flex gap={5} align={'center'}>
<IconCalendar color={colors['blue-button-trans']} size={45} />
<Text fz={{ base: "h4", md: "h3" }}>1 April 2025</Text>
</Flex>
<Flex gap={5} align={'center'}>
<IconMapPin color={colors['blue-button-trans']} size={45} />
<Text fz={{ base: "h4", md: "h3" }}>Banjar Desa Darmasaba</Text>
</Flex>
<Flex gap={5} align={'center'}>
<IconUsersGroup color={colors['blue-button-trans']} size={45} />
<Text fz={{ base: "h4", md: "h3" }}>30 Partisipan</Text>
</Flex>
<Text fw={'bold'} fz={'md'}>Deskripsi : Program pembangunan Pura sebagai pusat spiritual dan budaya desa, melibatkan gotong royong masyarakat dalam pembangunan struktur utama serta ornamen tradisional.</Text>
<Group py={20} justify='center'>
<Button component={Link} href={'https://www.whatsapp.com/?lang=id'} bg={colors['blue-button']} >Daftar Sebagai Relawan</Button>
</Group>
</Stack>
</Paper>
</Box>
</Stack>
);

View File

@@ -1,67 +1,76 @@
'use client'
import kolaborasiInovasiState from '@/app/admin/(dashboard)/_state/inovasi/kolaborasi-inovasi';
import mitraKolaborasi from '@/app/admin/(dashboard)/_state/inovasi/mitra-kolaborasi';
import colors from '@/con/colors';
import { Box, Center, Grid, GridCol, Group, Image, Pagination, Paper, Select, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
import {
Box,
Center,
Grid,
GridCol,
Group,
Image,
Pagination,
Paper,
Select,
SimpleGrid,
Skeleton,
Stack,
Text,
TextInput
} from '@mantine/core';
import { 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';
function Page() {
const state = useProxy(kolaborasiInovasiState)
const kolabState = useProxy(kolaborasiInovasiState)
const mitraState = useProxy(mitraKolaborasi)
const [search, setSearch] = useState('');
const [selectedYear, setSelectedYear] = useState<string | null>(null);
// Get unique years from the data
// Get unique years from kolaborasiInovasi data
const years = Array.from(
new Set(
state.findMany.data?.map(item =>
kolabState.findMany.data?.map(item =>
new Date(item.createdAt).getFullYear().toString()
) || []
)
)
.sort((a, b) => b.localeCompare(a)) // Sort descending (newest first)
.map(year => ({
value: year,
label: year,
}));
.sort((a, b) => b.localeCompare(a))
.map(year => ({ value: year, label: year }));
const {
data,
page,
totalPages,
loading,
load,
} = state.findMany
const { data, page, totalPages, loading, load } = kolabState.findMany;
useShallowEffect(() => {
load(page, 10, search, selectedYear || '')
}, [page, search, selectedYear])
mitraState.findMany.load(page, 10);
load(page, 10, search, selectedYear || '');
}, [page, search, selectedYear]);
const mitraData = mitraState.findMany.data || [];
const mitraLoading = mitraState.findMany.loading;
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={650} />
</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>
{/* Header Kolaborasi Inovasi */}
<Box px={{ base: 'md', md: 100 }} >
<Grid align='center'>
<GridCol span={{ base: 12, md: 9 }}>
<Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
<Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw="bold">
Kolaborasi Inovasi
</Text>
</GridCol>
<GridCol span={{ base: 12, md: 3 }}>
<TextInput
radius={"lg"}
radius="lg"
placeholder='Cari Kolaborasi Inovasi'
value={search}
onChange={(e) => setSearch(e.target.value)}
@@ -71,40 +80,43 @@ function Page() {
</GridCol>
</Grid>
</Box>
{/* Filter Tahun */}
<Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'} justify='center'>
<Group justify='flex-end'>
<Stack gap="lg" justify="center">
<Group justify="flex-end">
<Select
value={selectedYear}
onChange={setSelectedYear}
label={<Text fw={"bold"} fz={"sm"}>Tahun</Text>}
label={<Text fw="bold" fz="sm">Tahun</Text>}
placeholder='Semua Tahun'
clearable
data={[
{ value: '', label: 'Semua Tahun' },
...years
]}
data={[{ value: '', label: 'Semua Tahun' }, ...years]}
/>
</Group>
<SimpleGrid cols={{ base: 1, md: 2 }} spacing={'lg'}>
{data.map((v, k) => {
return (
<Paper p={'xl'} key={k}>
<Text fz={'h3'} fw={'bold'} c={colors['blue-button']}>{v.name}</Text>
<Box pr={'lg'} pb={20}>
{v.slug}
</Box>
<Box pr={'lg'}>
{v.kolaborator}
</Box>
{/* List Kolaborasi Inovasi */}
{loading || !data ? (
<Stack py={10}>
<Skeleton height={650} />
</Stack>
) : (
<SimpleGrid cols={{ base: 1, md: 2 }} spacing="lg">
{data.map((v, k) => (
<Paper p="xl" key={k}>
<Text fz="h3" fw="bold" c={colors['blue-button']}>{v.name}</Text>
<Box pr="lg" pb={20}>{v.slug}</Box>
<Box pr="lg">{v.kolaborator}</Box>
</Paper>
);
})}
</SimpleGrid>
))}
</SimpleGrid>
)}
{/* Pagination */}
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)} // ini penting!
onChange={(newPage) => load(newPage)}
total={totalPages}
mt="md"
mb="md"
@@ -113,17 +125,44 @@ function Page() {
</Stack>
</Box>
</Stack>
{/* Mitra Kolaborasi Section */}
<Box py={40} px={{ base: "md", md: 100 }} bg={colors['white-trans-1']}>
<Stack gap={'lg'} justify='center'>
<Box px={{ base: 'md', md: 100 }} >
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
<Stack gap="lg" justify="center">
<Box px={{ base: 'md', md: 100 }}>
<Text ta="center" fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw="bold">
Mitra Kolaborasi
</Text>
<Text ta={'center'} fz={'h4'}>Kami berkolaborasi dengan berbagai mitra dari berbagai sektor untuk mewujudkan visi Smart Village Darmasaba.</Text>
<Text ta="center" fz="h4">
Kami berkolaborasi dengan berbagai mitra dari berbagai sektor untuk mewujudkan visi Smart Village Darmasaba.
</Text>
</Box>
<Center>
<Image src={'/api/img/logoukm-kolaborasiinvoasi.png'} alt='' w={{ base: 500, md: 650 }} loading="lazy"/>
</Center>
{mitraLoading ? (
<Center py={20}><Skeleton height={100} width="80%" /></Center>
) : (
<SimpleGrid cols={{ base: 2, md: 4 }} spacing="xl">
{mitraData.map((m) => (
<Paper key={m.id} p="md" shadow="sm" radius="md">
<Center mb="sm">
{m.image?.link ? (
<Image
src={`${process.env.NEXT_PUBLIC_BASE_URL || ''}${m.image.link}`}
alt={m.name}
w={150}
h={100}
fit="cover"
radius="md"
/>
) : (
<Box w={100} h={100} bg={colors['blue-button']} style={{ borderRadius: '50%' }} />
)}
</Center>
<Text ta="center" fw="bold">{m.name}</Text>
</Paper>
))}
</SimpleGrid>
)}
</Stack>
</Box>
</>

View File

@@ -0,0 +1,95 @@
'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 { IconMapper, IconKey } from '@/app/admin/(dashboard)/_com/iconMap';
function Page() {
const stateProgramKreatif = useProxy(programKreatifState);
const router = useRouter();
const params = useParams();
useShallowEffect(() => {
stateProgramKreatif.findUnique.load(params?.id as string);
}, [params?.id]);
if (!stateProgramKreatif.findUnique.data) {
return (
<Stack py={10}>
<Skeleton height={500} radius="md" />
</Stack>
);
}
const data = stateProgramKreatif.findUnique.data;
return (
<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()}
>
&larr; Kembali
</Text>
</Box>
{/* Konten Utama */}
<Paper
withBorder
w={{ base: '100%', md: '60%' }}
bg={colors['white-1']}
p="lg"
radius="md"
mx="auto"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
{data?.name || 'Program Kreatif Desa'}
</Text>
{/* Ikon */}
{data?.icon && (
<Box>
<IconMapper
name={data.icon as IconKey}
size={40}
color={colors['blue-button']}
/>
</Box>
)}
{/* Deskripsi Singkat */}
<Box>
<Text fz="lg" fw="bold">
Deskripsi Singkat
</Text>
<Text fz="md" c="dimmed">{data?.slug || '-'}</Text>
</Box>
{/* Deskripsi Detail */}
<Box>
<Text fz="lg" fw="bold">
Deskripsi
</Text>
<Text
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{ __html: data?.deskripsi || '-' }}
/>
</Box>
</Stack>
</Paper>
</Box>
);
}
export default Page;

View File

@@ -1,51 +1,91 @@
'use client'
import programKreatifState from '@/app/admin/(dashboard)/_state/inovasi/program-kreatif';
import colors from '@/con/colors';
import { Stack, Box, Text, SimpleGrid, Paper, Center, Button } from '@mantine/core';
import React from 'react';
import { Box, Button, Center, Group, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { useTransitionRouter } from 'next-view-transitions';
import React, { useState } from 'react';
import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto';
import { IconBuildingCircus, IconChartLine, IconLeaf, IconRecycle, IconTrophy } from '@tabler/icons-react';
import { IconSearch } from '@tabler/icons-react';
import { IconKey, IconMapper } from '@/app/admin/(dashboard)/_com/iconMap';
const data = [
{
id: 1,
icon: <IconLeaf size={50} color={colors['blue-button']} />,
judul: 'Ekowisata dan Desa Hijau',
deskripsi: 'Inisiatif ramah lingkungan untuk desa berkelanjutan'
},
{
id: 2,
icon: <IconTrophy size={50} color={colors['blue-button']} />,
judul: 'Kompetisi dan Festival Desa',
deskripsi: 'Ajang kompetisi inovasi dan festival tahunan desa'
},
{
id: 3,
icon: <IconBuildingCircus size={50} color={colors['blue-button']} />,
judul: 'Wisata Kreatif dan Budaya',
deskripsi: 'Promosi destinasi wisata berbasis budaya dan alam'
},
{
id: 4,
icon: <IconChartLine size={50} color={colors['blue-button']} />,
judul: 'Ekonomi Kreatif',
deskripsi: 'Mendukung pelaku UMKM dengan platform digital untuk mempromosikan produk lokal ke pasar global'
},
{
id: 5,
icon: <IconRecycle size={50} color={colors['blue-button']} />,
judul: 'Smart Waste Management',
deskripsi: 'Inisiatif pengelolaan sampah berbasis teknologi untuk menciptakan lingkungan yang bersih dan berkelanjutan.'
},
]
// const data = [
// {
// id: 1,
// icon: <IconLeaf size={50} color={colors['blue-button']} />,
// judul: 'Ekowisata dan Desa Hijau',
// deskripsi: 'Inisiatif ramah lingkungan untuk desa berkelanjutan'
// },
// {
// id: 2,
// icon: <IconTrophy size={50} color={colors['blue-button']} />,
// judul: 'Kompetisi dan Festival Desa',
// deskripsi: 'Ajang kompetisi inovasi dan festival tahunan desa'
// },
// {
// id: 3,
// icon: <IconBuildingCircus size={50} color={colors['blue-button']} />,
// judul: 'Wisata Kreatif dan Budaya',
// deskripsi: 'Promosi destinasi wisata berbasis budaya dan alam'
// },
// {
// id: 4,
// icon: <IconChartLine size={50} color={colors['blue-button']} />,
// judul: 'Ekonomi Kreatif',
// deskripsi: 'Mendukung pelaku UMKM dengan platform digital untuk mempromosikan produk lokal ke pasar global'
// },
// {
// id: 5,
// icon: <IconRecycle size={50} color={colors['blue-button']} />,
// judul: 'Smart Waste Management',
// deskripsi: 'Inisiatif pengelolaan sampah berbasis teknologi untuk menciptakan lingkungan yang bersih dan berkelanjutan.'
// },
// ]
function Page() {
const listState = useProxy(programKreatifState);
const [search, setSearch] = useState("");
const [debouncedSearch] = useDebouncedValue(search, 500);
const router = useTransitionRouter()
const {
data,
loading,
page,
totalPages,
load
} = listState.findMany;
useShallowEffect(() => {
load(page, 10, debouncedSearch);
}, [debouncedSearch])
const filteredData = data || []
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={650} radius="md" />
</Stack>
)
}
return (
<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"}>
Program Kreatif Desa
</Text>
<Group justify="space-between" mb="md" align='center'>
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
Program Kreatif Desa
</Text>
<TextInput
placeholder="Cari program kreatif..."
leftSection={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
</Group>
</Box>
<Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'} justify='center'>
@@ -55,17 +95,25 @@ function Page() {
md: 3
}}
>
{data.map((v, k) => {
{filteredData.map((v, k) => {
return (
<Stack key={k} >
<Paper p={'xl'} >
<Center pb={20}>
{v.icon}
{v.icon ? (
<IconMapper
name={v.icon as IconKey}
size={32}
color={colors['blue-button']}
/>
) : (
<Text fz="sm" c="dimmed">Tidak ada ikon</Text>
)}
</Center>
<Text ta={'center'} fz={'h3'} fw={'bold'} c={colors['blue-button']}>{v.judul}</Text>
<Text py={10} ta={'center'} fz={'lg'} c={'black'}>{v.deskripsi}</Text>
<Text ta={'center'} fz={'h3'} fw={'bold'} c={colors['blue-button']}>{v.name}</Text>
<Text py={10} ta={'center'} fz={'lg'} c={'black'}>{v.slug}</Text>
<Center>
<Button bg={colors['blue-button']}>Selengkapnya</Button>
<Button onClick={() => router.push(`/darmasaba/inovasi/program-kreatif-desa/${v.id}`)} bg={colors['blue-button']}>Selengkapnya</Button>
</Center>
</Paper>
</Stack>
@@ -73,6 +121,20 @@ function Page() {
})}
</SimpleGrid>
</Stack>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
</Box>
</Stack>
);

View File

@@ -0,0 +1,116 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import programPenghijauanState from '@/app/admin/(dashboard)/_state/lingkungan/program-penghijauan';
import { Box, Button, Paper, Skeleton, Stack, Text, ThemeIcon } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import {
IconArrowLeft, IconChartLine, IconChristmasTreeFilled, IconClipboard,
IconHomeEco, IconLeaf, IconRecycle, IconScale,
IconShieldFilled, IconTent, IconTrash,
IconTrendingUp, IconTrophy, IconTruck
} from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import React from 'react';
import { useProxy } from 'valtio/utils';
function Page() {
const stateProgramPenghijauan = useProxy(programPenghijauanState);
const router = useRouter();
const params = useParams();
const iconMap: Record<string, React.FC<any>> = {
ekowisata: IconLeaf,
kompetisi: IconTrophy,
wisata: IconTent,
ekonomi: IconChartLine,
sampah: IconRecycle,
truck: IconTruck,
scale: IconScale,
clipboard: IconClipboard,
trash: IconTrash,
lingkunganSehat: IconHomeEco,
sumberOksigen: IconChristmasTreeFilled,
ekonomiBerkelanjutan: IconTrendingUp,
mencegahBencana: IconShieldFilled,
};
useShallowEffect(() => {
stateProgramPenghijauan.findUnique.load(params?.id as string);
}, [params?.id]);
if (!stateProgramPenghijauan.findUnique.data) {
return (
<Stack py="xl" align="center">
<Skeleton height={300} radius="lg" w="100%" />
<Text c="dimmed" fz="sm">Sedang memuat detail program...</Text>
</Stack>
);
}
const data = stateProgramPenghijauan.findUnique.data;
const IconComponent = data?.icon ? iconMap[data.icon] : null;
return (
<Box py="md" px={{ base: 'md', md: 100 }}>
<Button
variant="light"
color="blue"
onClick={() => router.back()}
leftSection={<IconArrowLeft size={20} />}
mb="lg"
radius="xl"
>
Kembali ke daftar
</Button>
<Paper
w={{ base: '100%', md: '75%' }}
mx="auto"
p="xl"
radius="xl"
shadow="md"
withBorder
>
<Stack gap="md" align="center" ta="center">
{IconComponent && (
<ThemeIcon
size={72}
radius="xl"
variant="light"
color="blue"
mb="sm"
style={{ boxShadow: '0 0 15px rgba(0, 123, 255, 0.3)' }}
>
<IconComponent size={40} />
</ThemeIcon>
)}
<Text fz="xl" fw={700} c="blue">
{data?.name || 'Nama program tidak tersedia'}
</Text>
<Text fz="lg" fw={600}>
{data?.judul || 'Judul belum tersedia'}
</Text>
<Box w="100%" mt="sm">
{data?.deskripsi ? (
<Text
fz="md"
lh={1.7}
ta="justify"
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
/>
) : (
<Text c="dimmed" fz="sm" ta="center">
Tidak ada deskripsi yang tersedia untuk program ini.
</Text>
)}
</Box>
</Stack>
</Paper>
</Box>
);
}
export default Page;

View File

@@ -8,11 +8,13 @@ import { IconChartLine, IconChristmasTreeFilled, IconClipboardTextFilled, IconHo
import React, { useState } from 'react';
import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto';
import { useTransitionRouter } from 'next-view-transitions';
function Page() {
const state = useProxy(programPenghijauanState);
const [search, setSearch] = useState("");
const [debouncedSearch] = useDebouncedValue(search, 500);
const router = useTransitionRouter()
const { data, load, page, totalPages, loading } = state.findMany;
useShallowEffect(() => {
@@ -96,6 +98,7 @@ function Page() {
el.style.transform = 'translateY(0)';
el.style.boxShadow = '0 4px 10px rgba(0,0,0,0.1)';
}}
onClick={() => router.push(`/darmasaba/lingkungan/program-penghijauan/${v.id}`)}
>
<Stack align="center" gap="sm">
<Center>