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
1473 changed files with 120383 additions and 639 deletions
Showing only changes of commit 2fe8b8ce1a - Show all commits

View File

@@ -46,11 +46,15 @@ const category = proxy({
},
},
findMany: {
data: [] as Prisma.CategoryPengumumanGetPayload<{
data: [] as (Prisma.CategoryPengumumanGetPayload<{
omit: {
isActive: true;
};
}>[],
}> & {
_count: {
pengumumans: number;
};
})[],
loading: false,
async load() {
const res = await ApiFetch.api.desa.kategoripengumuman["findMany"].get();

View File

@@ -1,7 +1,15 @@
import prisma from "@/lib/prisma";
async function kategoriPengumumanFindMany() {
const data = await prisma.categoryPengumuman.findMany();
const data = await prisma.categoryPengumuman.findMany({
include: {
_count: {
select: {
pengumumans: true
}
}
}
});
return { data };
}

View File

@@ -1,65 +0,0 @@
import colors from '@/con/colors';
import { Calendar } from '@mantine/dates';
import { Stack, Box, Container, Text, Grid, Paper, GridCol, Center, SimpleGrid } from '@mantine/core';
import React from 'react';
import BackButton from '../../../layanan/_com/BackButto';
function Page() {
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="md">
{/* Header */}
<Box px={{ base: "md", md: 100 }}>
<BackButton />
</Box>
<Container size="lg" px="md" >
<Stack gap="0" >
<Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" >
Jadwal Rapat
</Text>
</Stack>
</Container>
<Box px={{ base: 'md', md: 100 }}>
<Grid>
<GridCol span={{ base: 12, md: 6, lg: 5, xl: 4 }} >
<Paper bg={colors['white-1']} p={'md'}>
<Center>
<Calendar size='xl' />
</Center>
</Paper>
</GridCol>
<GridCol span={{ base: 12, md: 6, lg: 7, xl: 8 }} >
<Paper bg={colors['white-1']} p={'md'}>
<SimpleGrid
cols={{
base: 1,
xl: 3,
}}
>
<Box>
<Text fz={'lg'} fw={'bold'} c={colors['blue-button']}>Hari Ini</Text>
<Text fz={'lg'} >Senin, 13 Februari 2023</Text>
<Text fz={'lg'} >09:00 - 12:00</Text>
<Text fz={'lg'} >12:00 - 15:00</Text>
</Box>
<Box>
<Text fz={'lg'} fw={'bold'} c={colors['blue-button']}>Besok</Text>
<Text fz={'lg'} >Selasa, 14 Februari 2023</Text>
<Text fz={'lg'} >09:00 - 12:00</Text>
<Text fz={'lg'} >12:00 - 15:00</Text>
</Box>
<Box>
<Text fz={'lg'} fw={'bold'} c={colors['blue-button']}>Minggu Depan</Text>
<Text fz={'lg'} >Senin, 20 Februari 2023</Text>
<Text fz={'lg'} >09:00 - 12:00</Text>
<Text fz={'lg'} >12:00 - 15:00</Text>
</Box>
</SimpleGrid>
</Paper>
</GridCol>
</Grid>
</Box>
</Stack>
);
}
export default Page;

View File

@@ -1,49 +0,0 @@
import colors from '@/con/colors';
import { Stack, Box, Container, Text, Image, Grid, GridCol, Paper, TextInput, Center, Button } from '@mantine/core';
import React from 'react';
import BackButton from '../../../layanan/_com/BackButto';
function Page() {
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="md">
{/* Header */}
<Box px={{ base: "md", md: 100 }}>
<BackButton />
</Box>
<Container size="lg" px="md" >
<Stack gap="0" >
<Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" >
Pendaftaran UMKM
</Text>
</Stack>
</Container>
<Box px={{ base: 'md', md: 100 }}>
<Grid>
<GridCol span={{ base: 12, md: 6, lg: 7, xl: 8 }}>
<Center>
<Image src={'/api/img/market.png'} w={{ base: 300, md: 500, lg: 530, xl: 550 }} alt='' />
</Center>
</GridCol>
<GridCol span={{ base: 12, md: 6, lg: 5, xl: 4 }}>
<Paper bg={colors['white-1']} p={'md'} h={'500'}>
<Text ta={'center'} fz={{ base: "1.5rem", md: "2rem" }} c={colors["blue-button"]} fw="bold" >
Pendaftaran UMKM
</Text>
<Stack pt={20}>
<TextInput placeholder='Nama Lengkap' />
<TextInput placeholder='Alamat' />
<TextInput placeholder='No. Telepon' />
<TextInput placeholder='Email' />
<TextInput placeholder='Nama UMKM' />
<Button bg={colors['blue-button']} fullWidth>Daftar Sekarang</Button>
</Stack>
</Paper>
</GridCol>
</Grid>
</Box>
</Stack>
);
}
export default Page;

View File

@@ -0,0 +1,61 @@
'use client'
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
import colors from '@/con/colors';
import { Box, Container, Flex, Group, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useParams } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import BackButton from '../../../layanan/_com/BackButto';
function Page() {
const detail = useProxy(stateDesaPengumuman.pengumuman.findUnique)
const params = useParams()
useShallowEffect(() => {
stateDesaPengumuman.pengumuman.findUnique.load(params?.id as string)
}, [])
if (!detail.data) {
return (
<Box>
<Skeleton h={400} />
</Box>
)
}
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="md">
{/* Header */}
<Box px={{ base: "md", md: 100 }}>
<BackButton />
</Box>
<Container size="lg" px="md">
<Stack gap="xs" >
<Flex justify={"space-between"} align={"center"}>
<Text fz={{ base: "2rem", md: "2rem" }} c={colors["blue-button"]} fw="bold" >
{detail.data?.judul}
</Text>
<Group justify='end'>
<Paper bg={colors['blue-button']} p={5}>
<Text c={colors['white-1']}>{detail.data?.CategoryPengumuman?.name}</Text>
</Paper>
</Group>
</Flex>
<Paper bg={colors["white-1"]} p="md">
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: detail.data?.content }} />
<Text fz={"md"} c={colors["blue-button"]} fw="bold" >
{new Date(detail.data?.createdAt).toLocaleDateString('id-ID', {
weekday: 'long',
day: 'numeric',
month: 'long',
year: 'numeric'
})}
</Text>
</Paper>
</Stack>
</Container>
</Stack>
);
}
export default Page;

View File

@@ -0,0 +1,73 @@
'use client'
/* eslint-disable react-hooks/exhaustive-deps */
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
import colors from '@/con/colors';
import { Box, Container, Group, Paper, Stack, Text } from '@mantine/core';
import { IconCalendar } from '@tabler/icons-react';
import { useProxy } from 'valtio/utils';
import BackButton from '../../layanan/_com/BackButto';
import { useEffect } from 'react';
import { useParams } from 'next/navigation';
function Page() {
const unwrappedParams = useParams();
const kategoriState = useProxy(stateDesaPengumuman);
const categoryName = decodeURIComponent(unwrappedParams.name as string);
useEffect(() => {
kategoriState.category.findUnique.load(categoryName);
kategoriState.pengumuman.findMany.load(1, 10, '', categoryName);
}, [categoryName]);
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="md">
{/* Header */}
<Box px={{ base: "md", md: 100 }}>
<BackButton />
</Box>
<Container size="lg" px="md" >
<Stack align="center" gap="0" >
<Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" ta="center">
{categoryName.split('-').map(word =>
word.charAt(0).toUpperCase() + word.slice(1)
).join(' ')}
</Text>
<Text ta="center" px="md" pb={10}>
Informasi dan pengumuman resmi terkait {categoryName.split('-').join(' ')}
</Text>
</Stack>
</Container>
<Box px={{ base: 'md', md: 100 }}>
{!kategoriState.pengumuman.findMany.data?.length ? (
<Paper p="lg" radius="md" shadow="md" bg={colors["white-1"]}>
Tidak ada pengumuman yang ditemukan
</Paper>
) : kategoriState.pengumuman.findMany.data?.map((v, k) => {
return (
<Paper mb={10} key={k} withBorder p="lg" radius="md" shadow="md" bg={colors["white-1"]}>
<Text fz={'h3'}>{v.judul}</Text>
<Group style={{ color: 'black' }} pb={20}>
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">
{v.createdAt ? new Date(v.createdAt).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'long',
year: 'numeric',
}) : 'No date available'}
</Text>
</Group>
</Group>
<Text ta={'justify'}>
{v.deskripsi}
</Text>
</Paper>
)
})}
</Box>
</Stack>
);
}
export default Page;

View File

@@ -1,171 +1,299 @@
'use client';
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
import colors from '@/con/colors';
import { Anchor, Box, Container, Flex, Group, Notification, Paper, SimpleGrid, Stack, Text, TextInput, UnstyledButton } from '@mantine/core';
import {
Anchor,
Box,
Center,
Container,
Divider,
Flex,
Grid,
GridCol,
Group,
Notification,
Pagination,
Paper,
SimpleGrid,
Skeleton,
Stack,
Text,
TextInput,
Title,
UnstyledButton,
} from '@mantine/core';
import { IconCalendar, IconClock, IconSearch } from '@tabler/icons-react';
import BackButton from '../layanan/_com/BackButto';
import { useTransitionRouter } from 'next-view-transitions';
import Link from 'next/link';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import BackButton from '../layanan/_com/BackButto';
const dataKategori = [
{
id: 1,
kategori: 'Sosial & Kesehatan',
jumlah: 5,
link: '/darmasaba/desa/pengumuman/sosial-&-kesehatan'
},
{
id: 2,
kategori: 'Ekonomi & UMKM',
jumlah: 7,
link: '/darmasaba/desa/pengumuman/ekonomi-&-umkm'
},
{
id: 3,
kategori: 'Pendidikan & Kepemudaan',
jumlah: 9,
link: '/darmasaba/desa/pengumuman/pendidikan-&-kepemudaan'
},
{
id: 4,
kategori: 'Lingkungan & Bencana',
jumlah: 6,
link: '/darmasaba/desa/pengumuman/lingkungan-&-bencana'
},
{
id: 5,
kategori: 'Adat & Budaya',
jumlah: 8,
link: '/darmasaba/desa/pengumuman/adat-&-budaya'
},
{
id: 6,
kategori: 'Digitalisasi Desa',
jumlah: 6,
link: '/darmasaba/desa/pengumuman/digitalisasi-desa'
},
]
const dataPenting = [
{
id: 1,
judul: 'Jadwal Rapat',
jumlah: 5,
link: '/darmasaba/desa/pengumuman/jadwal-rapat'
},
{
id: 2,
judul: 'Pendaftaran UMKM',
jumlah: 7,
link: '/darmasaba/desa/pengumuman/pendaftaran-umkm'
}
]
function Page() {
const router = useTransitionRouter();
// State lokal
const [search, setSearch] = useState('');
const [searchInput, setSearchInput] = useState('');
const [page, setPage] = useState(1);
const state = useProxy(stateDesaPengumuman.pengumuman);
const totalPages = state.findMany.totalPages || 1;
const recent = useProxy(stateDesaPengumuman.pengumuman.findRecent);
// ✅ Baca URL saat pertama kali mount
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
const urlSearch = urlParams.get('search') || '';
const urlPage = parseInt(urlParams.get('page') || '1');
setSearch(urlSearch);
setSearchInput(urlSearch);
setPage(Math.max(1, urlPage)); // Pastikan page >= 1
}, []);
// ✅ Sinkronkan URL saat `search` atau `page` berubah
useEffect(() => {
const url = new URL(window.location.href);
if (search) {
url.searchParams.set('search', search);
} else {
url.searchParams.delete('search');
}
if (page > 1) {
url.searchParams.set('page', page.toString());
} else {
url.searchParams.delete('page');
}
router.replace(url.toString());
}, [search, page, router]);
// ✅ Debounce untuk pencarian
useEffect(() => {
const timeoutId = setTimeout(() => {
if (searchInput !== search) {
setSearch(searchInput);
setPage(1); // Reset ke halaman 1 saat pencarian baru
}
}, 500);
return () => clearTimeout(timeoutId);
}, [searchInput, search]);
// ✅ Load data dari state (valtio)
useEffect(() => {
stateDesaPengumuman.category.findMany.load();
stateDesaPengumuman.pengumuman.findRecent.load();
state.findMany.load(page, 3, search);
}, [search, page]); // 🔁 Depend pada `search` dan `page`
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="md">
{/* Header */}
<Box px={{ base: "md", md: 100 }}>
<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">
<Stack align="center" gap="0">
<Text fz={{ base: '2rem', md: '3.4rem' }} c={colors['blue-button']} fw="bold" ta="center">
Pengumuman Desa Darmasaba
</Text>
<Text ta="center" px="md" pb={10}>
Informasi dan pengumuman resmi terkait kegiatan dan kebijakan Desa Darmasaba
</Text>
<TextInput
placeholder='Cari Pengumuman'
radius="lg"
leftSection={<IconSearch size={20} />}
w={{ base: "55%", md: "70%" }}
/>
</Stack>
</Container>
<Box px={{ base: "md", md: 100 }}>
<SimpleGrid
cols={{
base: 1,
md: 2,
}}
>
<Notification styles={{ title: { fontWeight: "bold" } }} withCloseButton={false} title="Penting">
<Stack gap={"xs"}>
<Text fz={"sm"} fw={"bold"} c={"black"}>PENGUMUMAN LOWONGAN KERJA TPS3R PUDAK MESARI DESA DARMASABA</Text>
<Text ta={"justify"} fz={"sm"} c={"black"}>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis nec arcu ac ornare. Praesent a porttitor felis. Proin varius ex nisl, in hendrerit odio tristique vel. </Text>
</Stack>
<Flex pt={20} gap={"md"} justify={"space-between"}>
<Group style={{ color: 'black' }}>
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">Kamis, 13 Januari 2025</Text>
</Group>
<Group gap="xs">
<IconClock size={18} />
<Text size="sm">09:00 WITA</Text>
</Group>
</Group>
<Anchor>
<Text fs={'unset'} c={colors["blue-button"]} fz={"sm"}>Baca Selengkapnya</Text>
</Anchor>
</Flex>
</Notification>
<Paper p={"md"}>
<Stack gap={"xs"}>
<Text fw={"bold"} fz={"lg"} c={colors['blue-button']}>Kategori</Text>
{dataKategori.map((v, k) => {
{/* Recent & Kategori */}
<Box px={{ base: 'md', md: 100 }}>
<SimpleGrid cols={{ base: 1, md: 2 }}>
<Stack>
{!recent.data?.length ? (
<Notification withCloseButton={false} h={100}>
Tidak ada pengumuman yang ditemukan
</Notification>
) : (
recent.data
?.slice(0, 2)
.map((item, index) => (
<Notification
key={item.id}
color={index === 0 ? undefined : 'yellow'}
styles={{ title: { fontWeight: 'bold' } }}
withCloseButton={false}
title={item.CategoryPengumuman?.name || 'Pengumuman'}
>
<Stack gap={"xs"}>
<Text fz="sm" fw="bold" c="black" style={{ textTransform: 'uppercase' }}>
{item.judul}
</Text>
<Text ta="justify" fz="sm" c="black" lineClamp={3} dangerouslySetInnerHTML={{ __html: item.content }} />
</Stack>
<Flex pt={20} gap="md" justify="space-between">
<Group style={{ color: 'black' }}>
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">
{new Date(item.createdAt).toLocaleDateString('id-ID', {
weekday: 'long',
day: 'numeric',
month: 'long',
year: 'numeric',
})}
</Text>
</Group>
<Group gap="xs">
<IconClock size={18} />
<Text size="sm">
{new Date(item.createdAt).toLocaleTimeString('id-ID', {
hour: '2-digit',
minute: '2-digit',
timeZoneName: 'short',
})}
</Text>
</Group>
</Group>
<Anchor variant="transparent" href={`/darmasaba/desa/pengumuman/${item.CategoryPengumuman?.name}/${item.id}`}>
<Text fs="unset" c={colors['blue-button']} fz="sm">
Baca Selengkapnya
</Text>
</Anchor>
</Flex>
</Notification>
))
)}
</Stack>
<Paper p="md">
<Stack gap="xs">
<Text fw="bold" fz="lg" c={colors['blue-button']}>
Kategori
</Text>
{stateDesaPengumuman.category.findMany.data?.map((v: any, k) => {
const count = v._count?.pengumumans || 0;
return (
<UnstyledButton component={Link} href={v.link} key={k}>
<Paper bg={colors["BG-trans"]} p={5}>
<Group px={3} justify={"space-between"}>
<Text fz={"md"} c={"black"}>{v.kategori}</Text>
<Text fz={"md"} c={"black"}>{v.jumlah}</Text>
<UnstyledButton component={Link} href={`/darmasaba/desa/pengumuman/${v.name}`} key={k}>
<Paper bg={colors['BG-trans']} p={5}>
<Group px={3} justify="space-between">
<Text fz="md" c="black">
{v.name}
</Text>
<Text fz="md" c="black">
{count}
</Text>
</Group>
</Paper>
</UnstyledButton>
)
})}
</Stack>
</Paper>
<Notification color='yellow' styles={{ title: { fontWeight: "bold" } }} withCloseButton={false} title="Informasi">
<Stack gap={"xs"}>
<Text fz={"sm"} fw={"bold"} c={"black"}>LELANG PEMASANGAN CCTV DESA DARMASABA</Text>
<Text ta={"justify"} fz={"sm"} c={"black"}>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis nec arcu ac ornare. Praesent a porttitor felis. Proin varius ex nisl, in hendrerit odio tristique vel. </Text>
</Stack>
<Flex pt={20} gap={"md"} justify={"space-between"}><Group style={{ color: 'black' }}>
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">Kamis, 2 Februari 2025</Text>
</Group>
<Group gap="xs">
<IconClock size={18} />
<Text size="sm">10:00 WITA</Text>
</Group>
</Group>
<Anchor>
<Text fs={'unset'} c={colors["blue-button"]} fz={"sm"}>Baca Selengkapnya</Text>
</Anchor>
</Flex>
</Notification>
<Paper p={"md"}>
<Stack gap={"xs"}>
<Text fw={"bold"} fz={"lg"} c={colors['blue-button']}>Pengumuman Penting</Text>
{dataPenting.map((v, k) => {
return (
<UnstyledButton component={Link} href={v.link} key={k}>
<Paper bg={colors["BG-trans"]} p={5}>
<Group px={3} justify={"space-between"}>
<Text fz={"md"} c={"black"}>{v.judul}</Text>
<Text fz={"md"} c={"black"}>{v.jumlah}</Text>
</Group>
</Paper>
</UnstyledButton>
)
);
})}
</Stack>
</Paper>
</SimpleGrid>
</Box>
{/* Daftar Pengumuman */}
<Box px={{ base: 'md', md: 100 }}>
<Stack gap="xs">
<Divider mb={10} color={colors['blue-button']} />
<Grid>
<GridCol span={{ base: 12, md: 8 }}>
<Title order={3}>Daftar Pengumuman</Title>
</GridCol>
<GridCol span={{ base: 12, md: 4 }}>
<TextInput
placeholder="Cari Pengumuman"
radius="lg"
leftSection={<IconSearch size={20} />}
w="100%"
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)}
/>
</GridCol>
</Grid>
{/* Loading */}
{state.findMany.loading ? (
<SimpleGrid cols={{ base: 1, md: 3 }}>
{[1, 2, 3].map((i) => (
<Skeleton key={i} h={400} />
))}
</SimpleGrid>
) : !state.findMany.data?.length ? (
<Notification withCloseButton={false} h={100}>
Tidak ada pengumuman yang ditemukan
</Notification>
) : (
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg" verticalSpacing="lg">
{state.findMany.data.map((item) => (
<Paper key={item.id} p="md" withBorder radius="md" h="100%">
<Stack h="100%" justify="space-between">
<div>
<Text fw={600} c={colors['blue-button']} mb={5}>
{item.CategoryPengumuman?.name || 'Pengumuman'}
</Text>
<Text fz="lg" fw={700} mb="sm" lineClamp={2} style={{ textTransform: 'uppercase' }}>
{item.judul}
</Text>
<Text
fz="sm"
c="dimmed"
lineClamp={4}
dangerouslySetInnerHTML={{ __html: item.content }}
mb="md"
/>
</div>
<div>
<Group mb="sm" c="dimmed">
<Group gap={5}>
<IconCalendar size={16} />
<Text size="xs">
{new Date(item.createdAt).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'short',
year: 'numeric',
})}
</Text>
</Group>
<Group gap={5}>
<IconClock size={16} />
<Text size="xs">
{new Date(item.createdAt).toLocaleTimeString('id-ID', {
hour: '2-digit',
minute: '2-digit',
})}
</Text>
</Group>
</Group>
<Anchor variant="transparent" href={`/darmasaba/desa/pengumuman/${item.CategoryPengumuman?.name}/${item.id}`}>
<Text fw={600} c={colors['blue-button']} size="sm">
Baca Selengkapnya
</Text>
</Anchor>
</div>
</Stack>
</Paper>
))}
</SimpleGrid>
)}
{/* Pagination */}
<Center mt="xl">
<Pagination
total={totalPages}
value={page}
onChange={setPage}
siblings={1}
boundaries={1}
withEdges
/>
</Center>
</Stack>
</Box>
</Stack>
);
}
export default Page;
export default Page;