- QC User & Admin Menu Lingkungan

- Fix SubMenu : Edukasi Lingkungan & Konservasi Adat Bali dibagian User
- Fix SUbMenu : Gotong Royong User ( Tabs kategori menyesuaikan dengan data kategori kegiatan )
This commit is contained in:
2025-10-08 14:02:11 +08:00
parent d601b2fee3
commit 8ad38fc907
26 changed files with 1356 additions and 490 deletions

View File

@@ -135,7 +135,7 @@ export default function Content({ kategori }: { kategori: string }) {
{item.kategoriBerita?.name || kategori}
</Badge>
<Text fw={600} size="lg" mt="sm" lineClamp={2}>{item.judul}</Text>
<Text size="sm" c="dimmed" lineClamp={3} mt="xs" dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
<Text size="sm" c="dimmed" lineClamp={3} style={{wordBreak: "break-word", whiteSpace: "normal"}} mt="xs" dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
<Group justify="apart" mt="md" gap="xs">
<Text size="xs" c="dimmed">
{new Date(item.createdAt).toLocaleDateString('id-ID', {

View File

@@ -1,4 +1,3 @@
// src/app/darmasaba/(pages)/desa/berita/[kategori]/page.tsx
import { Suspense } from "react";
import Content from "./Content";

View File

@@ -1,24 +1,300 @@
import colors from '@/con/colors';
import { Stack, Box, Text, Image } from '@mantine/core';
import React from 'react';
import BackButton from '../../desa/layanan/_com/BackButto';
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi'
import colors from '@/con/colors'
import {
Box,
Button,
Card,
Center,
Container,
Group,
Image,
Loader,
Paper,
Stack,
Text,
Title,
Tooltip,
Transition,
} from '@mantine/core'
import { IconRefresh, IconSearch, IconUsers } from '@tabler/icons-react'
import { OrganizationChart } from 'primereact/organizationchart'
import { useEffect } from 'react'
import { useProxy } from 'valtio/utils'
import BackButton from '../../desa/layanan/_com/BackButto'
function Page() {
export default function Page() {
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Text px={{ base: 'md', md: 100 }} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
Struktur Organisasi dan SK Pengurus BUMDesa
</Text>
<Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'} justify='center'>
<Image src={'/api/img/bpddarmasaba.png'} alt='' loading="lazy"/>
<Box
style={{
minHeight: '100vh',
background: colors['Bg'],
color: '#E6F0FF',
paddingBottom: 48,
}}
>
<Container size="xl" py="xl">
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Stack align="center" gap="xl" mt="xl">
<Title
order={1}
ta="center"
c={colors['blue-button']}
fz={{ base: 28, md: 36, lg: 44 }}
>
Struktur Organisasi Dan SK Pengurus BumDes
</Title>
<Text ta="center" c="black" maw={800}>
Gambaran visual peran dan pegawai yang ditugaskan. Arahkan kursor
untuk melihat detail atau klik node untuk fokus tampilan.
</Text>
</Stack>
</Box>
</Stack>
);
<Box mt="lg">
<StrukturOrganisasiBumDes />
</Box>
</Container>
</Box>
)
}
export default Page;
function StrukturOrganisasiBumDes() {
const stateOrganisasi: any = useProxy(stateStrukturBumDes.pegawai)
useEffect(() => {
void stateOrganisasi.findMany.load()
}, [])
const isLoading =
!stateOrganisasi.findMany.data &&
stateOrganisasi.findMany.loading !== false
if (isLoading) {
return (
<Center py={48}>
<Stack align="center" gap="sm">
<Loader size="lg" />
<Text fw={600}>Memuat struktur organisasi</Text>
<Text c="dimmed" size="sm">
Mengambil data pegawai dan posisi. Mohon tunggu sebentar.
</Text>
</Stack>
</Center>
)
}
if (
!stateOrganisasi.findMany.data ||
stateOrganisasi.findMany.data.length === 0
) {
return (
<Center py={40}>
<Stack align="center" gap="md">
<Paper
radius="md"
p="xl"
style={{
width: 560,
background: 'rgba(28,110,164,0.2)',
border: `1px solid rgba(255,255,255,0.1)`,
textAlign: 'center',
}}
>
<Center>
<IconUsers size={56} />
</Center>
<Title order={3} mt="md">
Data pegawai belum tersedia
</Title>
<Text c="dimmed" mt="xs">
Belum ada data pegawai yang tercatat untuk BumDes. Silakan coba
muat ulang atau periksa sumber data.
</Text>
<Group justify="center" mt="lg">
<Button
leftSection={<IconRefresh size={16} />}
variant="gradient"
gradient={{ from: 'indigo', to: 'cyan' }}
onClick={() => stateOrganisasi.findMany.load()}
>
Muat Ulang
</Button>
<Button
leftSection={<IconSearch size={16} />}
variant="subtle"
onClick={() =>
stateOrganisasi.findMany.load({ query: { q: '' } })
}
>
Cari Pegawai
</Button>
</Group>
</Paper>
</Stack>
</Center>
)
}
const posisiMap = new Map<string, any>()
const aktifPegawai = stateOrganisasi.findMany.data.filter((p: any) => p.isActive);
for (const pegawai of aktifPegawai) {
const posisiId = pegawai.posisi.id;
if (!posisiMap.has(posisiId)) {
posisiMap.set(posisiId, {
...pegawai.posisi,
pegawaiList: [],
children: [],
});
}
posisiMap.get(posisiId)!.pegawaiList.push(pegawai);
}
// First, create a map of all unique positions
const allPositions = new Map();
aktifPegawai.forEach((pegawai: any) => {
if (!allPositions.has(pegawai.posisi.id)) {
allPositions.set(pegawai.posisi.id, {
...pegawai.posisi,
pegawaiList: [],
children: []
});
}
});
// Then assign employees to their positions
aktifPegawai.forEach((pegawai: any) => {
const posisi = allPositions.get(pegawai.posisi.id);
if (posisi) {
posisi.pegawaiList.push(pegawai);
}
});
// Now build the hierarchy
const root = [];
for (const [_, posisi] of allPositions) {
if (posisi.parentId) {
const parent = allPositions.get(posisi.parentId);
if (parent) {
parent.children.push(posisi);
} else {
// Only add to root if it's a top-level position
if (!posisi.parentId) {
root.push(posisi);
}
}
} else {
root.push(posisi);
}
}
function toOrgChartFormat(node: any): any {
return {
expanded: true,
type: 'person',
styleClass: 'p-person',
data: {
name: node.pegawaiList?.[0]?.namaLengkap || 'Belum ditugaskan',
title: node.nama || 'Tanpa jabatan',
image: node.pegawaiList?.[0]?.image?.link || '/img/default.png',
description: node.deskripsi || '',
positionId: node.id || null,
},
children: node.children?.map(toOrgChartFormat) || [],
}
}
const chartData = root.map(toOrgChartFormat)
return (
<Box py={16} >
<Paper
radius="md"
p="md"
style={{
background: 'rgba(28,110,164,0.2)',
border: `1px solid rgba(255,255,255,0.1)`,
overflowX: 'auto',
}}
>
<OrganizationChart
value={chartData}
nodeTemplate={nodeTemplate}
/>
</Paper>
</Box>
)
}
function nodeTemplate(node: any) {
const imageSrc = node?.data?.image || '/img/default.png'
const name = node?.data?.name || 'Tanpa Nama'
const title = node?.data?.title || 'Tanpa Jabatan'
const description = node?.data?.description || ''
return (
<Transition mounted transition="pop" duration={240}>
{(styles) => (
<Card
radius="lg"
withBorder
style={{
...styles,
width: 260,
padding: 16,
background: 'rgba(28,110,164,0.3)',
borderColor: 'rgba(255,255,255,0.15)',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
textAlign: 'center',
}}
>
<Image
src={imageSrc}
alt={name}
radius="md"
width={120}
height={120}
fit="cover"
style={{
objectFit: 'cover',
border: '2px solid rgba(255,255,255,0.2)',
marginBottom: 12,
}}
loading='lazy'
/>
<Text fw={700}>{name}</Text>
<Text size="sm" c="dimmed" mt={4}>
{title}
</Text>
<Text size="xs" c="dimmed" mt={8} lineClamp={3}>
{description || 'Belum ada deskripsi.'}
</Text>
<Tooltip label="Kembali ke struktur organisasi" withArrow position="bottom">
<Button
variant="light"
size="xs"
mt="md"
onClick={() => {
const id = node?.data?.positionId
if (id && (window as any).scrollTo) {
;(window as any).scrollTo({ top: 0, behavior: 'smooth' })
}
}}
>
Kembali
</Button>
</Tooltip>
</Card>
)}
</Transition>
)
}

View File

@@ -46,7 +46,7 @@ function Page() {
useShallowEffect(() => {
mitraState.findMany.load(page, 10);
load(page, 10, search, selectedYear || '');
load(page, 10, search, selectedYear ? `year:${selectedYear}` : '');
}, [page, search, selectedYear]);
const mitraData = mitraState.findMany.data || [];

View File

@@ -1,48 +1,31 @@
'use client'
import stateEdukasiLingkungan from '@/app/admin/(dashboard)/_state/lingkungan/edukasi-lingkungan';
import colors from '@/con/colors';
import { Box, List, ListItem, Paper, SimpleGrid, Stack, Text, Tooltip } from '@mantine/core';
import { Box, Paper, SimpleGrid, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconLeaf, IconPlant2, IconRecycle } from '@tabler/icons-react';
import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto';
const data = [
{
id: 1,
title: 'Tujuan Edukasi Lingkungan',
icon: <IconLeaf size={28} color={colors['blue-button']} />,
listDeskripsi: [
'Meningkatkan kesadaran masyarakat akan pentingnya lingkungan bersih dan sehat',
'Mendorong partisipasi warga dalam pengelolaan sampah, penghijauan, dan konservasi',
'Mengurangi dampak negatif kegiatan manusia terhadap lingkungan',
'Membentuk generasi muda peduli isu-isu lingkungan',
],
},
{
id: 2,
title: 'Materi Edukasi yang Diberikan',
icon: <IconRecycle size={28} color={colors['blue-button']} />,
listDeskripsi: [
'Pengelolaan sampah: pilah organik & anorganik',
'Pencegahan pencemaran lingkungan (air, udara, tanah)',
'Pemanfaatan lahan hijau dan penghijauan desa',
'Daur ulang dan kreativitas dari sampah',
'Bahaya pembakaran sampah sembarangan',
],
},
{
id: 3,
title: 'Contoh Kegiatan di Desa Darmasaba',
icon: <IconPlant2 size={28} color={colors['blue-button']} />,
listDeskripsi: [
'Pelatihan membuat kompos dari sampah rumah tangga',
'Gerakan "Jumat Bersih" rutin',
'Workshop pembuatan ecobrick',
'Lomba kebersihan antar banjar',
'Sosialisasi lingkungan di sekolah dan posyandu',
],
},
];
function Page() {
const tujuan = useProxy(stateEdukasiLingkungan.stateTujuanEdukasi.findById)
const materi = useProxy(stateEdukasiLingkungan.stateMateriEdukasiLingkungan.findById)
const contoh = useProxy(stateEdukasiLingkungan.stateContohEdukasiLingkungan.findById)
useShallowEffect(() => {
tujuan.load('edit')
materi.load('edit')
contoh.load('edit')
}, [])
if (tujuan.loading || !tujuan.data || materi.loading || !materi.data || contoh.loading || !contoh.data) {
return (
<Stack py={20}>
<Skeleton radius="md" height={600} />
</Stack>
);
}
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
<Box px={{ base: 'md', md: 100 }}>
@@ -60,28 +43,84 @@ function Page() {
</Box>
<Box px={{ base: 'md', md: 100 }}>
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg">
{data.map((item) => (
<Paper key={item.id} p={20} bg={colors['white-trans-1']} shadow="md" radius="md">
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg" style={{ alignItems: 'stretch' }}>
{/* Tujuan Edukasi Lingkungan */}
<Box style={{ display: 'flex', height: '100%' }}>
<Paper p={20} bg={colors['white-trans-1']} shadow="md" radius="md" style={{ width: '100%', display: 'flex', flexDirection: 'column' }}>
<Stack gap="md">
<Box>
<Tooltip label={item.title} position="top" withArrow>
<Tooltip label={tujuan.data?.judul} position="top" withArrow>
<Stack gap={4} align="center">
{item.icon}
<IconLeaf size={28} color={colors['blue-button']} />
<Text fz="h3" fw="bold" c={colors['blue-button']} ta="center">
{item.title}
{tujuan.data?.judul}
</Text>
</Stack>
</Tooltip>
</Box>
<List fz="h4" spacing="sm" withPadding>
{item.listDeskripsi.map((desc, idx) => (
<ListItem key={idx}>{desc}</ListItem>
))}
</List>
<Text
style={{
wordBreak: "break-word",
whiteSpace: "normal",
flexGrow: 1
}}
dangerouslySetInnerHTML={{ __html: tujuan.data?.deskripsi || '' }}
/>
<Box style={{ flexGrow: 1 }} />
</Stack>
</Paper>
))}
</Box>
{/* Materi Edukasi Lingkungan */}
<Box style={{ display: 'flex', height: '100%' }}>
<Paper p={20} bg={colors['white-trans-1']} shadow="md" radius="md">
<Stack gap="md">
<Box>
<Tooltip label={materi.data?.judul} position="top" withArrow>
<Stack gap={4} align="center">
<IconRecycle size={28} color={colors['blue-button']} />
<Text fz="h3" fw="bold" c={colors['blue-button']} ta="center">
{materi.data?.judul}
</Text>
</Stack>
</Tooltip>
</Box>
<Text
style={{
wordBreak: "break-word",
whiteSpace: "normal",
flexGrow: 1
}}
dangerouslySetInnerHTML={{ __html: materi.data?.deskripsi || '' }}
/>
<Box style={{ flexGrow: 1 }} />
</Stack>
</Paper>
</Box>
{/* Contoh Edukasi Lingkungan */}
<Box style={{ display: 'flex', height: '100%' }}>
<Paper p={20} bg={colors['white-trans-1']} shadow="md" radius="md">
<Stack gap="md">
<Box>
<Tooltip label={contoh.data?.judul} position="top" withArrow>
<Stack gap={4} align="center">
<IconPlant2 size={28} color={colors['blue-button']} />
<Text fz="h3" fw="bold" c={colors['blue-button']} ta="center">
{contoh.data?.judul}
</Text>
</Stack>
</Tooltip>
</Box>
<Text
style={{
wordBreak: "break-word",
whiteSpace: "normal",
flexGrow: 1
}}
dangerouslySetInnerHTML={{ __html: contoh.data?.deskripsi || '' }}
/>
</Stack>
</Paper>
</Box>
</SimpleGrid>
</Box>
</Stack>

View File

@@ -75,7 +75,7 @@ export default function Content({ kategori }: { kategori: string }) {
{featured.kategoriKegiatan?.nama || kategori}
</Badge>
<Title order={2} mb="md">{featured.judul}</Title>
<Text color="dimmed" lineClamp={3} mb="md">{featured.deskripsiLengkap}</Text>
<Text c="dimmed" lineClamp={3} mb="md" dangerouslySetInnerHTML={{ __html: featured.deskripsiLengkap }} />
</div>
<Group justify="apart" mt="auto">
<Group gap="xs">
@@ -135,9 +135,9 @@ export default function Content({ kategori }: { kategori: string }) {
{item.kategoriKegiatan?.nama || kategori}
</Badge>
<Text fw={600} size="lg" mt="sm" lineClamp={2}>{item.judul}</Text>
<Text size="sm" color="dimmed" lineClamp={3} style={{wordBreak: "break-word", whiteSpace: "normal"}} mt="xs" dangerouslySetInnerHTML={{ __html: item.deskripsiLengkap }} />
<Text size="sm" c="dimmed" lineClamp={3} style={{wordBreak: "break-word", whiteSpace: "normal"}} mt="xs" dangerouslySetInnerHTML={{ __html: item.deskripsiLengkap }} />
<Group justify="apart" mt="md" gap="xs">
<Text size="xs" color="dimmed">
<Text size="xs" c="dimmed">
{new Date(item.createdAt).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'short',

View File

@@ -1,4 +1,3 @@
// src/app/darmasaba/(pages)/desa/berita/[kategori]/page.tsx
import { Suspense } from "react";
import Content from "./content";

View File

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

View File

@@ -5,7 +5,7 @@ import { Badge, Box, Button, Card, Center, Container, Divider, Flex, Grid, GridC
import { IconArrowRight, IconCalendar } from '@tabler/icons-react';
import { useTransitionRouter } from 'next-view-transitions';
import { useSearchParams } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
function Page() {
@@ -14,8 +14,7 @@ function Page() {
// Parameter URL
const search = searchParams.get('search') || '';
const currentPage = parseInt(searchParams.get('page') || '1');
const [page, setPage] = useState(currentPage);
const page = parseInt(searchParams.get('page') || '1');
// Gunakan proxy untuk state
const state = useProxy(gotongRoyongState.kegiatanDesa);
@@ -37,12 +36,14 @@ function Page() {
}, [page, search]);
// Update URL saat page berubah
useEffect(() => {
const url = new URLSearchParams();
const handlePageChange = (newPage: number) => {
const url = new URLSearchParams(searchParams.toString());
if (search) url.set('search', search);
if (page > 1) url.set('page', page.toString());
if (newPage > 1) url.set('page', newPage.toString());
else url.delete('page'); // biar page=1 ga muncul di URL
router.replace(`?${url.toString()}`);
}, [page, search]);
};
const featuredData = featured.data;
const paginatedNews = state.findMany.data || [];
@@ -77,9 +78,7 @@ function Page() {
{featuredData.kategoriKegiatan?.nama || 'Gotong royong'}
</Badge>
<Title order={2} mb="md">{featuredData.judul}</Title>
<Text c="dimmed" lineClamp={3} mb="md">
{featuredData.deskripsiSingkat}
</Text>
<Text c="dimmed" lineClamp={3} mb="md" dangerouslySetInnerHTML={{ __html: featuredData.deskripsiSingkat }} />
</div>
<Group justify="apart" mt="auto">
<Group gap="xs">
@@ -146,7 +145,7 @@ function Page() {
<Text fw={600} size="lg" mt="sm" lineClamp={2}>{item.judul}</Text>
<Text size="sm" c="dimmed" lineClamp={3} mt="xs">{item.deskripsiSingkat}</Text>
<Text size="sm" c="dimmed" lineClamp={3} mt="xs" dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat }} />
<Flex align="center" justify="apart" mt="md" gap="xs">
<Text size="xs" c="dimmed">
@@ -169,7 +168,7 @@ function Page() {
<Pagination
total={totalPages}
value={page}
onChange={setPage}
onChange={handlePageChange}
siblings={1}
boundaries={1}
withEdges

View File

@@ -1,47 +1,30 @@
'use client'
import stateKonservasiAdatBali from '@/app/admin/(dashboard)/_state/lingkungan/konservasi-adat-bali';
import colors from '@/con/colors';
import { Box, Center, List, ListItem, Paper, SimpleGrid, Stack, Text } from '@mantine/core';
import { Box, Center, Paper, SimpleGrid, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto';
const data = [
{
id: 1,
title: 'Filosofi Tri Hita Karana',
listDeskripsi: (
<List fz={'lg'} spacing="sm" ta={'justify'}>
<ListItem>Parahyangan: Hubungan manusia dengan Tuhan yang dijaga penuh kesadaran spiritual</ListItem>
<ListItem>Pawongan: Harmoni dan kerja sama antar manusia dalam masyarakat</ListItem>
<ListItem>Palemahan: Pelestarian lingkungan dan hubungan manusia dengan alam</ListItem>
</List>
),
},
{
id: 2,
title: 'Bentuk Konservasi Berdasarkan Adat',
listDeskripsi: (
<List fz={'lg'} spacing="sm" ta={'justify'}>
<ListItem>Pelestarian Hutan Adat seperti Alas Pala Sangeh dan Wana Kerthi</ListItem>
<ListItem>Subak: Sistem irigasi tradisional yang menekankan kebersamaan dan keberlanjutan</ListItem>
<ListItem>Hari Raya Tumpek Uduh: Perayaan untuk menghormati pohon dan tumbuhan</ListItem>
<ListItem>Perarem & Awig-Awig: Aturan adat untuk menjaga lingkungan dari kerusakan</ListItem>
<ListItem>Ritual penyucian alam seperti Melasti dan Piodalan Segara</ListItem>
</List>
),
},
{
id: 3,
title: 'Nilai Konservasi Adat',
listDeskripsi: (
<List fz={'lg'} spacing="sm" ta={'justify'}>
<ListItem>Menjaga keseimbangan ekosistem dan lingkungan hidup</ListItem>
<ListItem>Melestarikan spiritualitas lokal dan kesucian alam</ListItem>
<ListItem>Meningkatkan kesadaran kolektif untuk hidup selaras dengan alam</ListItem>
<ListItem>Menjamin keberlanjutan sumber daya alam untuk generasi mendatang</ListItem>
</List>
),
},
];
function Page() {
const filosofi = useProxy(stateKonservasiAdatBali.stateFilosofiTriHita.findById)
const nilai = useProxy(stateKonservasiAdatBali.stateNilaiKonservasiAdat.findById)
const bentuk = useProxy(stateKonservasiAdatBali.stateBentukKonservasiBerdasarkanAdat.findById)
useShallowEffect(() => {
filosofi.load('edit')
nilai.load('edit')
bentuk.load('edit')
}, [])
if (filosofi.loading || !filosofi.data || nilai.loading || !nilai.data || bentuk.loading || !bentuk.data) {
return (
<Stack py={20} align="center">
<Skeleton radius="md" height={600} width="100%" />
</Stack>
);
}
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="24">
<Box px={{ base: 'md', md: 100 }}>
@@ -56,24 +39,99 @@ function Page() {
</Text>
</Box>
<Box px={{ base: 'md', md: 100 }}>
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg">
{data.map((item) => (
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg" style={{ alignItems: 'stretch' }}>
{/* Filsosofi */}
<Box style={{ display: 'flex', height: '100%' }}>
<Paper
key={item.id}
p="lg"
bg="linear-gradient(145deg, #DFE3E8FF 0%, #EFF1F4FF 100%)"
style={{ borderRadius: 16, boxShadow: '0 0 20px rgba(28, 110, 164, 0.5)' }}
style={{
borderRadius: 16,
boxShadow: '0 0 20px rgba(28, 110, 164, 0.5)',
width: '100%',
display: 'flex',
flexDirection: 'column'
}}
>
<Stack gap="md" px={20}>
<Stack gap="md" px={20} style={{ height: '100%' }}>
<Center>
<Text fz="xl" fw="bold" c="black">
{item.title}
</Text>
{filosofi.data?.judul}
</Text>
</Center>
{item.listDeskripsi}
<div
style={{
wordBreak: "break-word",
whiteSpace: "normal",
flexGrow: 1
}}
dangerouslySetInnerHTML={{ __html: filosofi.data?.deskripsi || '' }}
/>
</Stack>
</Paper>
))}
</Box>
{/* Nilai */}
<Box style={{ display: 'flex', height: '100%' }}>
<Paper
p="lg"
bg="linear-gradient(145deg, #DFE3E8FF 0%, #EFF1F4FF 100%)"
style={{
borderRadius: 16,
boxShadow: '0 0 20px rgba(28, 110, 164, 0.5)',
width: '100%',
display: 'flex',
flexDirection: 'column'
}}
>
<Stack gap="md" px={20} style={{ height: '100%' }}>
<Center>
<Text fz="xl" fw="bold" c="black">
{nilai.data?.judul}
</Text>
</Center>
<div
style={{
wordBreak: "break-word",
whiteSpace: "normal",
flexGrow: 1,
minHeight: 0
}}
dangerouslySetInnerHTML={{ __html: nilai.data?.deskripsi || '' }}
/>
</Stack>
</Paper>
</Box>
{/* Bentuk */}
<Box>
<Paper
p="lg"
bg="linear-gradient(145deg, #DFE3E8FF 0%, #EFF1F4FF 100%)"
style={{
borderRadius: 16,
boxShadow: '0 0 20px rgba(28, 110, 164, 0.5)',
width: '100%',
display: 'flex',
flexDirection: 'column'
}}
>
<Stack gap="md" px={20} style={{ height: '100%' }}>
<Center>
<Text fz="xl" fw="bold" c="black">
{bentuk.data?.judul}
</Text>
</Center>
<div
style={{
wordBreak: "break-word",
whiteSpace: "normal",
flexGrow: 1,
minHeight: 0
}}
dangerouslySetInnerHTML={{ __html: bentuk.data?.deskripsi || '' }}
/>
</Stack>
</Paper>
</Box>
</SimpleGrid>
</Box>
</Stack>

View File

@@ -1,10 +1,10 @@
'use client'
import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah';
import colors from '@/con/colors';
import { Box, Flex, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { Box, Center, Flex, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { Icon, IconChartLine, IconClipboardTextFilled, IconLeaf, IconRecycle, IconScale, IconSearch, IconTent, IconTrashFilled, IconTrophy, IconTruckFilled } from '@tabler/icons-react';
import React from 'react';
import React, { useState } from 'react';
import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto';
import dynamic from 'next/dynamic';
@@ -20,20 +20,26 @@ function Page() {
const state = useProxy(pengelolaanSampahState.pengelolaanSampah)
const state2 = useProxy(pengelolaanSampahState.keteranganSampah)
const [search, setSearch] = useState('')
const [debouncedSearch] = useDebouncedValue(search, 500);
const {
data,
load
load,
} = state.findMany
const {
data: data2,
load: load2
load: load2,
page,
totalPages,
} = state2.findMany
useShallowEffect(() => {
load()
load2()
}, [])
load2(page, 3, debouncedSearch)
}, [page, debouncedSearch])
const iconMap: Record<string, Icon> = {
ekowisata: IconLeaf,
@@ -104,8 +110,10 @@ function Page() {
px={{ base: 70, md: 150 }}
leftSection={<IconSearch size={20} />}
placeholder='Cari Bank Sampah Terdekat'
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<SimpleGrid cols={{ base: 1, md: 2 }} spacing="lg">
{/* Left side - List of bank locations */}
<Box>
@@ -131,9 +139,17 @@ function Page() {
</Paper>
))}
</Stack>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)} // ini penting!
total={totalPages}
my="md"
/>
</Center>
</Paper>
</Box>
{/* Right side - Single map showing all locations */}
<Box style={{ position: 'sticky', top: '20px' }}>
<Paper p="md" bg={colors['white-trans-1']} radius="lg" h="100%">