Sinkronisasi UI & API Admin - User Submenu Tips Keamanan, Menu Keamanan

This commit is contained in:
2025-08-20 14:35:08 +08:00
parent 49067f0218
commit c22d865283
4 changed files with 176 additions and 115 deletions

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch"; import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
@@ -53,15 +54,39 @@ const tipsKeamananState = proxy({
findMany: { findMany: {
data: null as data: null as
| Prisma.MenuTipsKeamananGetPayload<{ | Prisma.MenuTipsKeamananGetPayload<{
include: { image: true }; include: {
image: true;
};
}>[] }>[]
| null, | null,
async load() { page: 1,
const res = await ApiFetch.api.keamanan.menutipskeamanan[ totalPages: 1,
"find-many" loading: false,
].get(); search: "",
if (res.status === 200) { load: async (page = 1, limit = 10, search = "") => {
tipsKeamananState.findMany.data = res.data?.data ?? []; tipsKeamananState.findMany.loading = true; // ✅ Akses langsung via nama path
tipsKeamananState.findMany.page = page;
tipsKeamananState.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.keamanan.menutipskeamanan["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
tipsKeamananState.findMany.data = res.data.data ?? [];
tipsKeamananState.findMany.totalPages = res.data.totalPages ?? 1;
} else {
tipsKeamananState.findMany.data = [];
tipsKeamananState.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch menu tips keamanan paginated:", err);
tipsKeamananState.findMany.data = [];
tipsKeamananState.findMany.totalPages = 1;
} finally {
tipsKeamananState.findMany.loading = false;
} }
}, },
}, },

View File

@@ -1,6 +1,6 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header'; import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList'; import JudulList from '../../_com/judulList';
@@ -30,19 +30,21 @@ function ListTipsKeamanan({ search }: { search: string }) {
const stateKeamanan = useProxy(tipsKeamananState) const stateKeamanan = useProxy(tipsKeamananState)
const router = useRouter(); const router = useRouter();
const {
data,
page,
totalPages,
loading,
load,
} = stateKeamanan.findMany
useShallowEffect(() => { useShallowEffect(() => {
stateKeamanan.findMany.load() load(page, 10, search)
}, []) }, [page, search])
const filteredData = (stateKeamanan.findMany.data || []).filter(item => { const filteredData = data || []
const keyword = search.toLowerCase();
return (
item.judul.toLowerCase().includes(keyword) ||
item.deskripsi.toLowerCase().includes(keyword)
);
});
if (!stateKeamanan.findMany.data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton h={500} />
@@ -67,9 +69,15 @@ function ListTipsKeamanan({ search }: { search: string }) {
<TableTbody> <TableTbody>
{filteredData.map((item) => ( {filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd>{item.judul}</TableTd>
<TableTd> <TableTd>
<Text fz={"xs"} dangerouslySetInnerHTML={{__html: item.deskripsi}} /> <Box w={150}>
<Text fz={"md"} truncate={"end"} lineClamp={1}>{item.judul}</Text>
</Box>
</TableTd>
<TableTd>
<Box w={250}>
<Text fz={"md"} truncate={"end"} lineClamp={1} dangerouslySetInnerHTML={{__html: item.deskripsi}} />
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button onClick={() => router.push(`/admin/keamanan/tips-keamanan/${item.id}`)}> <Button onClick={() => router.push(`/admin/keamanan/tips-keamanan/${item.id}`)}>
@@ -81,6 +89,14 @@ function ListTipsKeamanan({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Paper> </Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
total={totalPages}
my="md"
/>
</Center>
</Box> </Box>
); );
} }

View File

@@ -1,24 +1,57 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// /api/berita/findManyPaginated.ts
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function menuTipsKeamananFindMany() { async function tipsKeamananFindMany(context: Context) {
try { // Ambil parameter dari query
const data = await prisma.menuTipsKeamanan.findMany({ const page = Number(context.query.page) || 1;
where: { isActive: true }, const limit = Number(context.query.limit) || 10;
include: { const search = (context.query.search as string) || '';
image: true, const skip = (page - 1) * limit;
},
});
return { // Buat where clause
success: true, const where: any = { isActive: true };
message: "Success fetch menu tips keamanan",
data, // Tambahkan pencarian (jika ada)
}; if (search) {
} catch (e) { where.OR = [
console.error("Find many error:", e); { judul: { contains: search, mode: 'insensitive' } },
return { { deskripsi: { contains: search, mode: 'insensitive' } },
success: false, ];
message: "Failed fetch menu tips keamanan", }
};
} try {
// Ambil data dan total count secara paralel
const [data, total] = await Promise.all([
prisma.menuTipsKeamanan.findMany({
where,
include: {
image: true,
},
skip,
take: limit,
orderBy: { createdAt: 'desc' },
}),
prisma.menuTipsKeamanan.count({ where }),
]);
return {
success: true,
message: "Berhasil ambil menu tips keamanan dengan pagination",
data,
page,
limit,
total,
totalPages: Math.ceil(total / limit),
};
} catch (e) {
console.error("Error di findMany paginated:", e);
return {
success: false,
message: "Gagal mengambil data tips keamanan",
};
}
} }
export default tipsKeamananFindMany;

View File

@@ -1,83 +1,62 @@
'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Center, Image, List, ListItem, Paper, SimpleGrid, Stack, Text } from '@mantine/core'; import { Box, Center, Grid, GridCol, Image, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
import BackButton from '../../desa/layanan/_com/BackButto'; import BackButton from '../../desa/layanan/_com/BackButto';
import { useProxy } from 'valtio/utils';
import tipsKeamananState from '@/app/admin/(dashboard)/_state/keamanan/tips-keamanan';
import { useShallowEffect } from '@mantine/hooks';
import { useState } from 'react';
import { IconSearch } from '@tabler/icons-react';
const data1 = [
{
id: 1,
judul: 'Keamanan Rumah',
image: '/api/img/kemanan.png',
deskripsi: <List>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Pastikan pintu dan jendela selalu terkunci saat meninggalkan rumah.</ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Pasang lampu penerangan di halaman dan area sekitar rumah untuk mencegah tindak kejahatan.</ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Jangan mudah memberikan akses masuk ke orang yang tidak dikenal.</ListItem>
</List>
},
{
id: 2,
judul: 'Keamanan di Jalan',
image: '/api/img/keamananjalan.png',
deskripsi: <List>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Hindari berjalan sendirian di tempat sepi, terutama pada malam hari.</ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Simpan barang berharga di tempat yang aman saat bepergian.</ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Gunakan jalur yang ramai dan terang saat pulang malam.</ListItem>
</List>
},
{
id: 3,
judul: 'Keamanan Kendaraan',
image: '/api/img/keamanankendaraan.png',
deskripsi: <List>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Gunakan kunci ganda saat memarkir kendaraan, terutama di tempat umum.</ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Parkir di tempat yang terang dan mudah diawasi.</ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Jangan meninggalkan barang berharga di dalam kendaraan.</ListItem>
</List>
},
{
id: 4,
judul: 'Keamanan Sosial',
image: '/api/img/mencurigakan.png',
deskripsi: <List>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Laporkan kejadian mencurigakan kepada Pecalang atau perangkat desa.</ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Jangan mudah percaya terhadap informasi yang belum jelas sumbernya.</ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Ikuti program sosialisasi keamanan yang diadakan oleh desa.</ListItem>
</List>
},
{
id: 5,
judul: 'Sistem Laporan Kejadian',
image: '/api/img/securitydigital.png',
deskripsi: <List>
<ListItem>Jangan mudah membagikan informasi pribadi di media sosial.</ListItem>
<ListItem>Waspada terhadap penipuan online dan telepon yang mengatasnamakan instansi resmi.</ListItem>
<ListItem>Gunakan kata sandi yang kuat untuk akun digital dan ganti secara berkala.</ListItem>
</List>
},
{
id: 6,
judul: 'Nomor Darurat yang Bisa Dihubungi',
image: '/api/img/kontakpecalang.png',
deskripsi: <List>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Pecalang: 08125651052</ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Ambulans: 08125651052</ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Pemadam Kebakaran: 113</ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Polisi: 110</ListItem>
</List>
}
]
function Page() { function Page() {
const state = useProxy(tipsKeamananState)
const [search, setSearch] = useState('')
const {
data,
page,
totalPages,
loading,
load,
} = state.findMany;
useShallowEffect(() => {
load(page, 3, search)
}, [page, search])
if (loading || !data) {
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Skeleton h={500} />
</Stack>
)
}
return ( 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 }}> <Box px={{ base: 'md', md: 100 }}>
<BackButton /> <BackButton />
</Box> </Box>
<Box> <Box>
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}> <Grid align='center' px={{ base: 'md', md: 100 }}>
Tips Keamanan <GridCol span={{ base: 12, md: 9 }}>
</Text> <Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
<Text px={{ base: 20, md: 150 }} ta={"center"} fz={{ base: "h4", md: "h3" }} > Tips Keamanan
Desa Darmasaba berkomitmen untuk menjaga keamanan dan kenyamanan seluruh warganya. Berikut beberapa tips yang dapat membantu meningkatkan keamanan di lingkungan desa. </Text>
</GridCol>
<GridCol span={{ base: 12, md: 3 }}>
<TextInput
radius={"lg"}
placeholder='Cari Tips'
value={search}
onChange={(e) => setSearch(e.target.value)}
leftSection={<IconSearch size={20} />}
w={{ base: "50%", md: "100%" }}
/>
</GridCol>
</Grid>
<Text px={{ base: 'md', md: 100 }} ta={"justify"} fz={{ base: "h4", md: "h3" }} >
Keamanan dan ketertiban lingkungan di Desa Darmasaba dijaga melalui peran aktif Pecalang dan Patwal (Patroli Pengawal). Mereka bertugas memastikan desa tetap aman, tertib, dan kondusif bagi seluruh warga.
</Text> </Text>
</Box> </Box>
<Box px={{ base: "md", md: 100 }}> <Box px={{ base: "md", md: 100 }}>
@@ -88,21 +67,21 @@ function Page() {
base: 1, base: 1,
md: 3, md: 3,
}}> }}>
{data1.map((v, k) => { {data.map((v, k) => {
return ( return (
<Paper radius={10} key={k} bg={colors["white-trans-1"]}> <Paper radius={10} key={k} bg={colors["white-trans-1"]}>
<Stack gap={'xs'}> <Stack gap={'xs'}>
<Center p={10}> <Center p={10}>
<Image src={v.image} radius={10} <Image src={v.image?.link} radius={10}
alt='' /> alt='' />
</Center> </Center>
<Box px={'xl'}> <Box px={'xl'}>
<Box pb={20}> <Box pb={20}>
<Text pb={10} c={colors["blue-button"]} fw={"bold"} fz={"h3"}> <Text pb={10} c={colors["blue-button"]} fw={"bold"} fz={"h3"}>
{v.judul} {v.judul}
</Text> </Text>
<Box pr={10}> <Box>
{v.deskripsi} <Text pb={10} fz={"md"} dangerouslySetInnerHTML={{ __html: v.deskripsi }} />
</Box> </Box>
</Box> </Box>
</Box> </Box>
@@ -113,6 +92,14 @@ function Page() {
</SimpleGrid> </SimpleGrid>
</Stack> </Stack>
</Box> </Box>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)} // ini penting!
total={totalPages}
my="md"
/>
</Center>
</Stack> </Stack>
); );
} }