Fix Admin - User Menu Keamanan, Submenu Pencegahan Kriminalitas

This commit is contained in:
2025-09-17 17:54:03 +08:00
parent 79ad39fc55
commit 9f72e94557
18 changed files with 782 additions and 847 deletions

View File

@@ -1254,48 +1254,15 @@ model KontakDaruratToItem {
// ========================================= PENCEGAHAN KRIMINALITAS ========================================= // // ========================================= PENCEGAHAN KRIMINALITAS ========================================= //
model PencegahanKriminalitas { model PencegahanKriminalitas {
id String @id @default(cuid()) id String @id @default(cuid())
programKeamanan ProgramKeamanan @relation(fields: [programKeamananId], references: [id]) judul String
programKeamananId String deskripsi String
tipsKeamanan TipsKeamanan @relation(fields: [tipsKeamananId], references: [id]) deskripsiSingkat String
tipsKeamananId String linkVideo String
videoKeamanan VideoKeamanan @relation(fields: [videoKeamananId], references: [id]) createdAt DateTime @default(now())
videoKeamananId String updatedAt DateTime @updatedAt
createdAt DateTime @default(now()) deletedAt DateTime @default(now())
updatedAt DateTime @updatedAt isActive Boolean @default(true)
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model ProgramKeamanan {
id String @id @default(cuid())
nama String // contoh: "Ronda Malam"
deskripsi String? // jika mau tambahkan info detail
slug String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
PencegahanKriminalitas PencegahanKriminalitas[]
}
model TipsKeamanan {
id String @id @default(cuid())
judul String
konten String
slug String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
PencegahanKriminalitas PencegahanKriminalitas[]
}
model VideoKeamanan {
id String @id @default(cuid())
judul String
deskripsi String?
videoUrl String // link youtube atau embed url
slug String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
PencegahanKriminalitas PencegahanKriminalitas[]
} }
// ========================================= LAPORAN PUBLIK ========================================= // // ========================================= LAPORAN PUBLIK ========================================= //

View File

@@ -6,45 +6,17 @@ import { proxy } from "valtio";
import { z } from "zod"; import { z } from "zod";
const templateForm = z.object({ const templateForm = z.object({
pencegahanKriminalitas: z.object({ judul: z.string().min(1, "Judul minimal 1 karakter"),
programKeamanan: z.object({ deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
nama: z.string().min(1, "Nama minimal 1 karakter"), deskripsiSingkat: z.string().min(1, "Deskripsi singkat minimal 1 karakter"),
deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"), linkVideo: z.string().min(1, "Link video minimal 1 karakter"),
slug: z.string().min(1, "Slug minimal 1 karakter"),
}),
tipsKeamanan: z.object({
judul: z.string().min(1, "Judul minimal 1 karakter"),
konten: z.string().min(1, "Konten minimal 1 karakter"),
slug: z.string().min(1, "Slug minimal 1 karakter"),
}),
videoKeamanan: z.object({
judul: z.string().min(1, "Judul minimal 1 karakter"),
deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
videoUrl: z.string().min(1, "Video URL minimal 1 karakter"),
slug: z.string().min(1, "Slug minimal 1 karakter"),
}),
}),
}); });
const defaultForm = { const defaultForm = {
pencegahanKriminalitas: { judul: "",
programKeamanan: { deskripsi: "",
nama: "", deskripsiSingkat: "",
deskripsi: "", linkVideo: "",
slug: "",
},
tipsKeamanan: {
judul: "",
konten: "",
slug: "",
},
videoKeamanan: {
judul: "",
deskripsi: "",
videoUrl: "",
slug: "",
},
},
}; };
const pencegahanKriminalitasState = proxy({ const pencegahanKriminalitasState = proxy({
@@ -65,7 +37,7 @@ const pencegahanKriminalitasState = proxy({
pencegahanKriminalitasState.create.loading = true; pencegahanKriminalitasState.create.loading = true;
const res = await ApiFetch.api.keamanan.pencegahankriminalitas[ const res = await ApiFetch.api.keamanan.pencegahankriminalitas[
"create" "create"
].post(pencegahanKriminalitasState.create.form.pencegahanKriminalitas); ].post(pencegahanKriminalitasState.create.form);
if (res.status === 200) { if (res.status === 200) {
pencegahanKriminalitasState.findMany.load(); pencegahanKriminalitasState.findMany.load();
return toast.success("success create"); return toast.success("success create");
@@ -82,11 +54,7 @@ const pencegahanKriminalitasState = proxy({
findMany: { findMany: {
data: null as data: null as
| Prisma.PencegahanKriminalitasGetPayload<{ | Prisma.PencegahanKriminalitasGetPayload<{
include: { omit: { isActive: true };
programKeamanan: true;
tipsKeamanan: true;
videoKeamanan: true;
};
}>[] }>[]
| null, | null,
page: 1, page: 1,
@@ -125,11 +93,7 @@ const pencegahanKriminalitasState = proxy({
}, },
findUnique: { findUnique: {
data: null as Prisma.PencegahanKriminalitasGetPayload<{ data: null as Prisma.PencegahanKriminalitasGetPayload<{
include: { omit: { isActive: true };
programKeamanan: true;
tipsKeamanan: true;
videoKeamanan: true;
};
}> | null, }> | null,
loading: false, loading: false,
async load(id: string) { async load(id: string) {
@@ -148,6 +112,30 @@ const pencegahanKriminalitasState = proxy({
} }
}, },
}, },
findFirst: {
data: null as Prisma.PencegahanKriminalitasGetPayload<{
omit: { isActive: true };
}> | null,
loading: false,
// findFirst.load()
async load() {
this.loading = true;
try {
const res = await ApiFetch.api.keamanan.pencegahankriminalitas["find-first"].get();
if (res.status === 200 && res.data?.success) {
this.data = res.data.data || null;
} else {
this.data = null;
}
} catch (err) {
console.error("Gagal fetch pencegahan kriminalitas terbaru:", err);
this.data = null;
} finally {
this.loading = false;
}
},
},
delete: { delete: {
loading: false, loading: false,
async byId(id: string) { async byId(id: string) {
@@ -213,24 +201,10 @@ const pencegahanKriminalitasState = proxy({
const data = result.data; const data = result.data;
pencegahanKriminalitasState.update.id = data.id; pencegahanKriminalitasState.update.id = data.id;
pencegahanKriminalitasState.update.form = { pencegahanKriminalitasState.update.form = {
pencegahanKriminalitas: { judul: data.judul,
programKeamanan: { deskripsi: data.deskripsi,
nama: data.programKeamanan.nama, deskripsiSingkat: data.deskripsiSingkat,
deskripsi: data.programKeamanan.deskripsi, linkVideo: data.linkVideo,
slug: data.programKeamanan.slug,
},
tipsKeamanan: {
judul: data.tipsKeamanan.judul,
konten: data.tipsKeamanan.konten,
slug: data.tipsKeamanan.slug,
},
videoKeamanan: {
judul: data.videoKeamanan.judul,
deskripsi: data.videoKeamanan.deskripsi,
videoUrl: data.videoKeamanan.videoUrl,
slug: data.videoKeamanan.slug,
},
},
}; };
return data; return data;
} else { } else {
@@ -266,40 +240,11 @@ const pencegahanKriminalitasState = proxy({
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({
pencegahanKriminalitas: { judul: pencegahanKriminalitasState.update.form.judul,
programKeamanan: { deskripsi: pencegahanKriminalitasState.update.form.deskripsi,
nama: pencegahanKriminalitasState.update.form deskripsiSingkat:
.pencegahanKriminalitas.programKeamanan.nama, pencegahanKriminalitasState.update.form.deskripsiSingkat,
deskripsi: linkVideo: pencegahanKriminalitasState.update.form.linkVideo,
pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.programKeamanan.deskripsi,
slug: pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.programKeamanan.slug,
},
tipsKeamanan: {
judul:
pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.tipsKeamanan.judul,
konten:
pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.tipsKeamanan.konten,
slug: pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.tipsKeamanan.slug,
},
videoKeamanan: {
judul:
pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.videoKeamanan.judul,
deskripsi:
pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.videoKeamanan.deskripsi,
videoUrl:
pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.videoKeamanan.videoUrl,
slug: pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.videoKeamanan.slug,
},
},
}), }),
} }
); );

View File

@@ -121,7 +121,7 @@ function DetailLaporanPublik() {
<Box> <Box>
<Text fz="lg" fw="bold">Kronologi</Text> <Text fz="lg" fw="bold">Kronologi</Text>
<Text fz="md" c="dimmed">{data.kronologi || '-'}</Text> <Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.kronologi || '' }} />
</Box> </Box>
<Box> <Box>

View File

@@ -135,9 +135,7 @@ function ListLaporanPublik({ search }: { search: string }) {
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={200}> <Box w={200}>
<Text truncate fz="sm" c="dimmed" lineClamp={1}> <Text truncate fz="sm" c="dimmed" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.kronologi || '' }} />
{item.kronologi}
</Text>
</Box> </Box>
</TableTd> </TableTd>
<TableTd> <TableTd>

View File

@@ -2,39 +2,35 @@
'use client' 'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import pencegahanKriminalitasState from '@/app/admin/(dashboard)/_state/keamanan/pencegahan-kriminalitas'; import pencegahanKriminalitasState from '@/app/admin/(dashboard)/_state/keamanan/pencegahan-kriminalitas';
import { convertYoutubeUrlToEmbed } from '@/app/admin/(dashboard)/desa/gallery/lib/youtube-utils';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
TextInput,
Title,
Tooltip
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function EditPencegahanKriminalitas() { function EditPencegahanKriminalitas() {
const router = useRouter(); const router = useRouter();
const params = useParams() const params = useParams();
const kriminalitasState = useProxy(pencegahanKriminalitasState) const kriminalitasState = useProxy(pencegahanKriminalitasState);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
pencegahanKriminalitas: { judul: '',
programKeamanan: { deskripsi: '',
nama: kriminalitasState.update.form.pencegahanKriminalitas.programKeamanan.nama, deskripsiSingkat: '',
deskripsi: kriminalitasState.update.form.pencegahanKriminalitas.programKeamanan.deskripsi, linkVideo: '',
slug: kriminalitasState.update.form.pencegahanKriminalitas.programKeamanan.slug, });
},
tipsKeamanan: {
judul: kriminalitasState.update.form.pencegahanKriminalitas.tipsKeamanan.judul,
konten: kriminalitasState.update.form.pencegahanKriminalitas.tipsKeamanan.konten,
slug: kriminalitasState.update.form.pencegahanKriminalitas.tipsKeamanan.slug,
},
videoKeamanan: {
judul: kriminalitasState.update.form.pencegahanKriminalitas.videoKeamanan.judul,
deskripsi: kriminalitasState.update.form.pencegahanKriminalitas.videoKeamanan.deskripsi,
videoUrl: kriminalitasState.update.form.pencegahanKriminalitas.videoKeamanan.videoUrl,
slug: kriminalitasState.update.form.pencegahanKriminalitas.videoKeamanan.slug,
},
},
})
useEffect(() => { useEffect(() => {
const loadKriminalitas = async () => { const loadKriminalitas = async () => {
@@ -43,167 +39,154 @@ function EditPencegahanKriminalitas() {
try { try {
const data = await kriminalitasState.update.load(id); const data = await kriminalitasState.update.load(id);
if (data && data.pencegahanKriminalitas) { if (data) {
const { programKeamanan, tipsKeamanan, videoKeamanan } = data.pencegahanKriminalitas;
setFormData({ setFormData({
pencegahanKriminalitas: { judul: data.judul || '',
programKeamanan: { deskripsi: data.deskripsi || '',
nama: programKeamanan?.nama || "", deskripsiSingkat: data.deskripsiSingkat || '',
deskripsi: programKeamanan?.deskripsi || "", linkVideo: data.linkVideo || '',
slug: programKeamanan?.slug || "",
},
tipsKeamanan: {
judul: tipsKeamanan?.judul || "",
konten: tipsKeamanan?.konten || "",
slug: tipsKeamanan?.slug || "",
},
videoKeamanan: {
judul: videoKeamanan?.judul || "",
deskripsi: videoKeamanan?.deskripsi || "",
videoUrl: videoKeamanan?.videoUrl || "",
slug: videoKeamanan?.slug || "",
},
},
}); });
} }
} catch (error) { } catch (error) {
console.error("Error loading pencegahan kriminalitas:", error); console.error('Error loading pencegahan kriminalitas:', error);
toast.error("Gagal memuat data pencegahan kriminalitas"); toast.error('Gagal memuat data pencegahan kriminalitas');
} }
} };
loadKriminalitas(); loadKriminalitas();
}, [params.id]); }, [params?.id]);
const embedLink = convertYoutubeUrlToEmbed(formData.linkVideo);
const handleSubmit = async () => { const handleSubmit = async () => {
const converted = convertYoutubeUrlToEmbed(formData.linkVideo);
if (!converted) {
toast.error('Link YouTube tidak valid. Pastikan formatnya benar.');
return;
}
try { try {
// Update the form data first
kriminalitasState.update.form = { kriminalitasState.update.form = {
...kriminalitasState.update.form, ...kriminalitasState.update.form,
pencegahanKriminalitas: { judul: formData.judul,
programKeamanan: { deskripsi: formData.deskripsi,
nama: formData.pencegahanKriminalitas.programKeamanan.nama, deskripsiSingkat: formData.deskripsiSingkat,
deskripsi: formData.pencegahanKriminalitas.programKeamanan.deskripsi, linkVideo: formData.linkVideo,
slug: formData.pencegahanKriminalitas.programKeamanan.slug, };
},
tipsKeamanan: {
judul: formData.pencegahanKriminalitas.tipsKeamanan.judul,
konten: formData.pencegahanKriminalitas.tipsKeamanan.konten,
slug: formData.pencegahanKriminalitas.tipsKeamanan.slug,
},
videoKeamanan: {
judul: formData.pencegahanKriminalitas.videoKeamanan.judul,
deskripsi: formData.pencegahanKriminalitas.videoKeamanan.deskripsi,
videoUrl: formData.pencegahanKriminalitas.videoKeamanan.videoUrl,
slug: formData.pencegahanKriminalitas.videoKeamanan.slug,
},
},
}
await kriminalitasState.update.update();
toast.success("Pencegahan Kriminalitas berhasil diperbarui!");
router.push("/admin/keamanan/pencegahan-kriminalitas");
} catch (error) {
console.error("Error updating pencegahan kriminalitas:", error);
toast.error("Gagal memuat data pencegahan kriminalitas");
}
}
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'}> // Set the ID and then call update
<Stack gap={"xs"}> kriminalitasState.update.id = params?.id as string;
<Title order={4}>Edit Pencegahan Kriminalitas</Title>
await kriminalitasState.update.update();
toast.success('Pencegahan Kriminalitas berhasil diperbarui!');
router.push('/admin/keamanan/pencegahan-kriminalitas');
} catch (error) {
console.error('Error updating pencegahan kriminalitas:', error);
toast.error('Terjadi kesalahan saat memperbarui data');
}
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Back button + 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">
Edit Pencegahan Kriminalitas
</Title>
</Group>
{/* Form container */}
<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 <TextInput
value={formData.pencegahanKriminalitas.programKeamanan.nama} label="Judul"
onChange={(val) => { placeholder="Masukkan judul"
formData.pencegahanKriminalitas.programKeamanan.nama = val.target.value; value={formData.judul}
}} onChange={(e) => setFormData({ ...formData, judul: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Judul Program Keamanan</Text>} required
placeholder='Masukkan judul Program Keamanan'
/> />
<TextInput <TextInput
value={formData.pencegahanKriminalitas.programKeamanan.slug} label="Deskripsi Singkat"
onChange={(val) => { placeholder="Masukkan deskripsi singkat"
formData.pencegahanKriminalitas.programKeamanan.slug = val.target.value; value={formData.deskripsiSingkat}
}} onChange={(e) =>
label={<Text fw={"bold"} fz={"sm"}>Slug Program Keamanan</Text>} setFormData({ ...formData, deskripsiSingkat: e.target.value })
placeholder='Masukkan slug Program Keamanan' }
required
/> />
<Box> <Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Program Keamanan</Text> <Title order={6} fw="bold" fz="sm" mb={6}>
Deskripsi Lengkap
</Title>
<EditEditor <EditEditor
value={formData.pencegahanKriminalitas.programKeamanan.deskripsi} value={formData.deskripsi}
onChange={(val) => { onChange={(val) =>
formData.pencegahanKriminalitas.programKeamanan.deskripsi = val; setFormData({ ...formData, deskripsi: val })
}} }
/> />
</Box> </Box>
<TextInput
value={formData.pencegahanKriminalitas.tipsKeamanan.judul}
onChange={(val) => {
formData.pencegahanKriminalitas.tipsKeamanan.judul = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Judul Tips Keamanan</Text>}
placeholder='Masukkan judul Tips Keamanan'
/>
<TextInput
value={formData.pencegahanKriminalitas.tipsKeamanan.slug}
onChange={(val) => {
formData.pencegahanKriminalitas.tipsKeamanan.slug = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Slug Tips Keamanan</Text>}
placeholder='Masukkan slug Tips Keamanan'
/>
<Box> <Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Tips Keamanan</Text> <TextInput
<EditEditor label="Link Video YouTube"
value={formData.pencegahanKriminalitas.tipsKeamanan.konten} placeholder="https://www.youtube.com/watch?v=abc123"
onChange={(val) => { value={formData.linkVideo}
formData.pencegahanKriminalitas.tipsKeamanan.konten = val; onChange={(e) =>
}} setFormData({ ...formData, linkVideo: e.currentTarget.value })
}
required
/> />
{embedLink && (
<Box
mt="sm"
style={{ display: 'flex', justifyContent: 'center' }}
>
<iframe
className="rounded"
width="100%"
height="220"
src={embedLink}
title="Preview Video"
allowFullScreen
></iframe>
</Box>
)}
</Box> </Box>
<TextInput
value={formData.pencegahanKriminalitas.videoKeamanan.judul} {/* Action button */}
onChange={(val) => { <Group justify="right">
formData.pencegahanKriminalitas.videoKeamanan.judul = val.target.value; <Button
}} onClick={handleSubmit}
label={<Text fw={"bold"} fz={"sm"}>Judul Video Keamanan</Text>} radius="md"
placeholder='Masukkan judul Video Keamanan' size="md"
/> style={{
<TextInput background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
value={formData.pencegahanKriminalitas.videoKeamanan.slug} color: '#fff',
onChange={(val) => { boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
formData.pencegahanKriminalitas.videoKeamanan.slug = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Slug Video Keamanan</Text>}
placeholder='Masukkan slug Video Keamanan'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Tips Keamanan</Text>
<EditEditor
value={formData.pencegahanKriminalitas.videoKeamanan.deskripsi}
onChange={(val) => {
formData.pencegahanKriminalitas.videoKeamanan.deskripsi = val;
}} }}
/> >
</Box> Simpan
<TextInput </Button>
value={formData.pencegahanKriminalitas.videoKeamanan.videoUrl}
onChange={(val) => {
formData.pencegahanKriminalitas.videoKeamanan.videoUrl = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Video URL</Text>}
placeholder='Masukkan video URL'
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -1,113 +1,146 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { Box, Button, Flex, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter, useParams } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { useParams } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import pencegahanKriminalitasState from '../../../_state/keamanan/pencegahan-kriminalitas'; import pencegahanKriminalitasState from '../../../_state/keamanan/pencegahan-kriminalitas';
function DetailPencegahanKriminalitas() { function DetailPencegahanKriminalitas() {
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null);
const router = useRouter(); const router = useRouter();
const params = useParams() const params = useParams();
const kriminalitasState = useProxy(pencegahanKriminalitasState) const kriminalitasState = useProxy(pencegahanKriminalitasState);
useShallowEffect(() => { useShallowEffect(() => {
kriminalitasState.findUnique.load(params?.id as string) kriminalitasState.findUnique.load(params?.id as string);
}, []) }, []);
const handleDelete = () => { const handleDelete = () => {
if (selectedId) { if (selectedId) {
kriminalitasState.delete.byId(selectedId) kriminalitasState.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/keamanan/pencegahan-kriminalitas") router.push("/admin/keamanan/pencegahan-kriminalitas");
} }
} };
if (!kriminalitasState.findUnique.data) { if (!kriminalitasState.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={40} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) );
} }
const data = kriminalitasState.findUnique.data;
return ( return (
<Box> <Box py={10}>
<Box mb={10}> {/* Tombol Kembali */}
<Button variant="subtle" onClick={() => router.back()}> <Button
<IconArrowBack color={colors['blue-button']} size={25} /> variant="subtle"
</Button> onClick={() => router.back()}
</Box> leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}> mb={15}
<Stack> >
<Paper bg={colors['BG-trans']} p={'md'}> Kembali
<Text fz={"xl"} fw={"bold"}>Detail Pencegahan Kriminalitas</Text> </Button>
{kriminalitasState.findUnique.data ? (
<Paper key={kriminalitasState.findUnique.data.id} bg={colors['BG-trans']}> {/* Detail */}
<Stack gap={"xs"} py={'md'}> <Paper
<Box> withBorder
<Text fw={"bold"} fz={"lg"}>Judul Program Keamanan</Text> w={{ base: "100%", md: "50%" }}
<Text fz={"lg"}>{kriminalitasState.findUnique.data?.programKeamanan.nama}</Text> bg={colors['white-1']}
</Box> p="lg"
<Box> radius="md"
<Text fw={"bold"} fz={"lg"}>Slug</Text> shadow="sm"
<Text fz={"lg"}>{kriminalitasState.findUnique.data?.programKeamanan.slug}</Text> >
</Box> <Stack gap="md">
<Box> <Text fz="2xl" fw="bold" c={colors['blue-button']}>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text> Detail Pencegahan Kriminalitas
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: kriminalitasState.findUnique.data?.programKeamanan.deskripsi || '' }} /> </Text>
</Box>
<Box> <Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Text fw={"bold"} fz={"lg"}>Judul Tips Keamanan</Text> <Stack gap="sm">
<Text fz={"lg"}>{kriminalitasState.findUnique.data?.tipsKeamanan.judul}</Text> <Box>
</Box> <Text fz="lg" fw="bold">Judul</Text>
<Box> <Text fz="md" c="dimmed">{data?.judul || '-'}</Text>
<Text fw={"bold"} fz={"lg"}>Slug Tips Keamanan</Text> </Box>
<Text fz={"lg"}>{kriminalitasState.findUnique.data?.tipsKeamanan.slug}</Text>
</Box> <Box>
<Box> <Text fz="lg" fw="bold">Deskripsi Singkat</Text>
<Text fw={"bold"} fz={"lg"}>Deskripsi Tips Keamanan</Text> {data?.deskripsiSingkat ? (
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: kriminalitasState.findUnique.data?.tipsKeamanan.konten || '' }} /> <Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.deskripsiSingkat }} />
</Box> ) : (
<Flex gap={"xs"} mt={10}> <Text fz="sm" c="dimmed">Tidak ada deskripsi singkat</Text>
<Button )}
onClick={() => { </Box>
if (kriminalitasState.findUnique.data) {
setSelectedId(kriminalitasState.findUnique.data.id); <Box>
setModalHapus(true); <Text fz="lg" fw="bold">Deskripsi</Text>
} {data?.deskripsi ? (
}} <Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.deskripsi }} />
disabled={kriminalitasState.delete.loading || !kriminalitasState.findUnique.data} ) : (
color={"red"} <Text fz="sm" c="dimmed">Tidak ada deskripsi</Text>
> )}
<IconX size={20} /> </Box>
</Button>
<Button <Box>
onClick={() => { <Text fz="lg" fw="bold">Video</Text>
if (kriminalitasState.findUnique.data) { {data?.linkVideo ? (
router.push(`/admin/keamanan/pencegahan-kriminalitas/${kriminalitasState.findUnique.data.id}/edit`); <Box
} component="iframe"
}} src={convertToEmbedUrl(data.linkVideo)}
disabled={!kriminalitasState.findUnique.data} width="100%"
color={"green"} height={300}
> allowFullScreen
<IconEdit size={20} /> style={{ borderRadius: 8 }}
</Button> />
</Flex> ) : (
</Stack> <Text fz="sm" c="dimmed">Tidak ada video</Text>
</Paper> )}
) : null} </Box>
{/* Tombol Aksi */}
<Flex gap="sm" mt="sm">
<Tooltip label="Hapus" withArrow position="top">
<Button
color="red"
onClick={() => {
setSelectedId(data.id);
setModalHapus(true);
}}
variant="light"
radius="md"
size="md"
>
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit" withArrow position="top">
<Button
color="green"
onClick={() => router.push(`/admin/keamanan/pencegahan-kriminalitas/${data.id}/edit`)}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Tooltip>
</Flex>
</Stack>
</Paper> </Paper>
</Stack> </Stack>
</Paper> </Paper>
{/* Modal Hapus */} {/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
@@ -116,6 +149,18 @@ function DetailPencegahanKriminalitas() {
/> />
</Box> </Box>
); );
function convertToEmbedUrl(youtubeUrl: string): string {
try {
const url = new URL(youtubeUrl);
const videoId = url.searchParams.get("v");
if (!videoId) return youtubeUrl;
return `https://www.youtube.com/embed/${videoId}`;
} catch (err) {
console.error("Error converting YouTube URL to embed:", err);
return youtubeUrl;
}
}
} }
export default DetailPencegahanKriminalitas; export default DetailPencegahanKriminalitas;

View File

@@ -1,141 +1,158 @@
'use client' 'use client'
import colors from '@/con/colors'; 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 { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor'; import CreateEditor from '../../../_com/createEditor';
import pencegahanKriminalitasState from '../../../_state/keamanan/pencegahan-kriminalitas'; import pencegahanKriminalitasState from '../../../_state/keamanan/pencegahan-kriminalitas';
import { useState } from 'react';
import { convertYoutubeUrlToEmbed } from '../../../desa/gallery/lib/youtube-utils';
import { toast } from 'react-toastify';
function CreatePencegahanKriminalitas() { function CreatePencegahanKriminalitas() {
const router = useRouter(); const router = useRouter();
const kriminalitasState = useProxy(pencegahanKriminalitasState) const kriminalitasState = useProxy(pencegahanKriminalitasState);
const [link, setLink] = useState('');
const embedLink = convertYoutubeUrlToEmbed(link);
const resetForm = () => { const resetForm = () => {
kriminalitasState.create.form = { kriminalitasState.create.form = {
pencegahanKriminalitas: { judul: "",
programKeamanan: { deskripsi: "",
nama: "", deskripsiSingkat: "",
deskripsi: "", linkVideo: "",
slug: "", };
}, setLink('');
tipsKeamanan: { };
judul: "",
konten: "",
slug: "",
},
videoKeamanan: {
judul: "",
deskripsi: "",
videoUrl: "",
slug: "",
},
},
}
}
const handleSubmit = async () => { const handleSubmit = async () => {
if (!embedLink) {
toast.error('Link YouTube tidak valid. Pastikan formatnya benar.');
return;
}
kriminalitasState.create.form.linkVideo = embedLink;
await kriminalitasState.create.create(); await kriminalitasState.create.create();
resetForm(); resetForm();
router.push('/admin/keamanan/pencegahan-kriminalitas'); router.push('/admin/keamanan/pencegahan-kriminalitas');
} };
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'}> return (
<Stack gap={"xs"}> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Title order={4}>Create Pencegahan Kriminalitas</Title> {/* Header Back Button + 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 Pencegahan Kriminalitas
</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">
{/* Judul */}
<TextInput <TextInput
value={kriminalitasState.create.form.pencegahanKriminalitas.programKeamanan.nama} label="Judul Pencegahan Kriminalitas"
onChange={(val) => { placeholder="Masukkan judul Pencegahan Kriminalitas"
kriminalitasState.create.form.pencegahanKriminalitas.programKeamanan.nama = val.target.value; value={kriminalitasState.create.form.judul}
onChange={(e) => {
kriminalitasState.create.form.judul = e.currentTarget.value;
}} }}
label={<Text fw={"bold"} fz={"sm"}>Judul Program Keamanan</Text>} required
placeholder='Masukkan judul Program Keamanan'
/> />
{/* Deskripsi Singkat */}
<TextInput <TextInput
value={kriminalitasState.create.form.pencegahanKriminalitas.programKeamanan.slug} label="Deskripsi Singkat"
onChange={(val) => { placeholder="Masukkan deskripsi singkat"
kriminalitasState.create.form.pencegahanKriminalitas.programKeamanan.slug = val.target.value; value={kriminalitasState.create.form.deskripsiSingkat}
onChange={(e) => {
kriminalitasState.create.form.deskripsiSingkat = e.currentTarget.value;
}} }}
label={<Text fw={"bold"} fz={"sm"}>Slug Program Keamanan</Text>} required
placeholder='Masukkan slug Program Keamanan'
/> />
{/* Deskripsi Panjang */}
<Box> <Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Program Keamanan</Text> <Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<CreateEditor <CreateEditor
value={kriminalitasState.create.form.pencegahanKriminalitas.programKeamanan.deskripsi} value={kriminalitasState.create.form.deskripsi}
onChange={(val) => { onChange={(val) => {
kriminalitasState.create.form.pencegahanKriminalitas.programKeamanan.deskripsi = val; kriminalitasState.create.form.deskripsi = val;
}} }}
/> />
</Box> </Box>
{/* Link YouTube */}
<TextInput <TextInput
value={kriminalitasState.create.form.pencegahanKriminalitas.tipsKeamanan.judul} label="Link Video YouTube"
onChange={(val) => { placeholder="https://www.youtube.com/watch?v=abc123"
kriminalitasState.create.form.pencegahanKriminalitas.tipsKeamanan.judul = val.target.value; value={link}
}} onChange={(e) => setLink(e.currentTarget.value)}
label={<Text fw={"bold"} fz={"sm"}>Judul Tips Keamanan</Text>} required
placeholder='Masukkan judul Tips Keamanan'
/> />
<TextInput
value={kriminalitasState.create.form.pencegahanKriminalitas.tipsKeamanan.slug} {/* Preview Video */}
onChange={(val) => { {embedLink && (
kriminalitasState.create.form.pencegahanKriminalitas.tipsKeamanan.slug = val.target.value; <Box mt="sm">
}} <iframe
label={<Text fw={"bold"} fz={"sm"}>Slug Tips Keamanan</Text>} style={{
placeholder='Masukkan slug Tips Keamanan' borderRadius: 10,
/> width: '100%',
<Box> height: 400,
<Text fw={"bold"} fz={"sm"}>Deskripsi Tips Keamanan</Text> border: '1px solid #ddd',
<CreateEditor }}
value={kriminalitasState.create.form.pencegahanKriminalitas.tipsKeamanan.konten} src={embedLink}
onChange={(val) => { title="Preview Video"
kriminalitasState.create.form.pencegahanKriminalitas.tipsKeamanan.konten = val; allowFullScreen
></iframe>
</Box>
)}
{/* 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)',
}} }}
/> >
</Box> Simpan
<TextInput </Button>
value={kriminalitasState.create.form.pencegahanKriminalitas.videoKeamanan.judul}
onChange={(val) => {
kriminalitasState.create.form.pencegahanKriminalitas.videoKeamanan.judul = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Judul Video Keamanan</Text>}
placeholder='Masukkan judul Video Keamanan'
/>
<TextInput
value={kriminalitasState.create.form.pencegahanKriminalitas.videoKeamanan.slug}
onChange={(val) => {
kriminalitasState.create.form.pencegahanKriminalitas.videoKeamanan.slug = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Slug Video Keamanan</Text>}
placeholder='Masukkan slug Video Keamanan'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Tips Keamanan</Text>
<CreateEditor
value={kriminalitasState.create.form.pencegahanKriminalitas.videoKeamanan.deskripsi}
onChange={(val) => {
kriminalitasState.create.form.pencegahanKriminalitas.videoKeamanan.deskripsi = val;
}}
/>
</Box>
<TextInput
value={kriminalitasState.create.form.pencegahanKriminalitas.videoKeamanan.videoUrl}
onChange={(val) => {
kriminalitasState.create.form.pencegahanKriminalitas.videoKeamanan.videoUrl = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Video URL</Text>}
placeholder='Masukkan video URL'
/>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -62,14 +62,6 @@ function ListPencegahanKriminalitas({ search }: { search: string }) {
load(page, 10, search); load(page, 10, search);
}, [page, search]); }, [page, search]);
const filteredData = (data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.programKeamanan.nama.toLowerCase().includes(keyword) ||
item.programKeamanan.slug.toLowerCase().includes(keyword) ||
item.programKeamanan.deskripsi?.toLowerCase().includes(keyword)
);
});
if (loading || !data) { if (loading || !data) {
return ( return (
@@ -103,35 +95,43 @@ function ListPencegahanKriminalitas({ search }: { search: string }) {
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Nama Pencegahan</TableTh> <TableTh>Nama Pencegahan</TableTh>
<TableTh>Slug</TableTh>
<TableTh>Deskripsi</TableTh> <TableTh>Deskripsi</TableTh>
<TableTh>Deskripsi Singkat</TableTh>
<TableTh>Aksi</TableTh> <TableTh>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.length > 0 ? ( {data.length > 0 ? (
filteredData.map((item) => ( data.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Text fw={500} truncate="end" lineClamp={1}> <Text fw={500} truncate="end" lineClamp={1}>
{item.programKeamanan.nama} {item.judul}
</Text> </Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Text fz="sm" c="dimmed" truncate lineClamp={1}> <Box w={200}>
{item.programKeamanan.slug} <Text
</Text> dangerouslySetInnerHTML={{ __html: item.deskripsi }}
fz="sm"
c="dimmed"
truncate
lineClamp={1}
/>
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Text <Box w={200}>
fz="sm" <Text
c="dimmed" fz="sm"
truncate c="dimmed"
lineClamp={1} truncate
dangerouslySetInnerHTML={{ lineClamp={1}
__html: item.programKeamanan.deskripsi || '' dangerouslySetInnerHTML={{
}} __html: item.deskripsiSingkat || ''
/> }}
/>
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button <Button

View File

@@ -1,81 +1,35 @@
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import { Context } from "elysia"; import { Context } from "elysia";
type ProgramKeamananInput = { type FormCreate = Prisma.PencegahanKriminalitasGetPayload<{
nama: string; select: {
deskripsi: string; judul: true;
slug: string; deskripsi: true;
}; deskripsiSingkat: true;
linkVideo: true;
type TipsKeamanan = { };
judul: string; }>;
konten: string;
slug: string;
};
type VideoKeamanan = {
judul: string;
deskripsi: string;
videoUrl: string;
slug: string;
};
type createdPencegahanKriminalitas = {
programKeamanan: ProgramKeamananInput;
tipsKeamanan: TipsKeamanan;
videoKeamanan: VideoKeamanan;
};
const pencegahanKriminalitasCreate = async (context: Context) => { const pencegahanKriminalitasCreate = async (context: Context) => {
const { programKeamanan, tipsKeamanan, videoKeamanan } = const body = context.body as FormCreate;
(await context.body) as createdPencegahanKriminalitas;
const createdProgram = await prisma.programKeamanan.create({ await prisma.pencegahanKriminalitas.create({
data: { data: {
nama: programKeamanan.nama, judul: body.judul,
deskripsi: programKeamanan.deskripsi, deskripsi: body.deskripsi,
slug: programKeamanan.slug, deskripsiSingkat: body.deskripsiSingkat,
linkVideo: body.linkVideo,
}, },
}); });
const createdTips = await prisma.tipsKeamanan.create({ return {
success: true,
message: "Success create pencegahan kriminalitas",
data: { data: {
judul: tipsKeamanan.judul, ...body,
konten: tipsKeamanan.konten,
slug: tipsKeamanan.slug,
}, },
}); status: 200,
};
const createdVideo = await prisma.videoKeamanan.create({
data: {
judul: videoKeamanan.judul,
deskripsi: videoKeamanan.deskripsi,
videoUrl: videoKeamanan.videoUrl,
slug: videoKeamanan.slug,
},
});
const createdPencegahanKriminalitas =
await prisma.pencegahanKriminalitas.create({
data: {
programKeamananId: createdProgram.id,
tipsKeamananId: createdTips.id,
videoKeamananId: createdVideo.id,
},
include: {
programKeamanan: true,
tipsKeamanan: true,
videoKeamanan: true,
},
});
return Response.json(
{
success: true,
message: "Success create program keamanan",
data: createdPencegahanKriminalitas,
},
{ status: 200 }
);
}; };
export default pencegahanKriminalitasCreate; export default pencegahanKriminalitasCreate;

View File

@@ -13,11 +13,6 @@ const pencegahanKriminalitasDelete = async (context: Context) => {
const pencegahanKriminalitas = await prisma.pencegahanKriminalitas.findUnique({ const pencegahanKriminalitas = await prisma.pencegahanKriminalitas.findUnique({
where: { id }, where: { id },
include: {
programKeamanan: true,
tipsKeamanan: true,
videoKeamanan: true,
}
}); });
if (!pencegahanKriminalitas) { if (!pencegahanKriminalitas) {

View File

@@ -0,0 +1,60 @@
import prisma from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import { Context } from "elysia";
export default async function pencegahanKriminalitasFindFirst(context: Context) {
// Ambil parameter query
const search = (context.query.search as string) || "";
// Buat where clause
const where: Prisma.PencegahanKriminalitasWhereInput = { isActive: true };
if (search) {
where.OR = [
{
judul: {
contains: search,
mode: "insensitive" as const,
},
},
{
deskripsi: {
contains: search,
mode: "insensitive" as const,
},
},
{
deskripsiSingkat: {
contains: search,
mode: "insensitive" as const,
},
},
];
}
try {
const data = await prisma.pencegahanKriminalitas.findFirst({
where,
orderBy: { createdAt: "desc" }, // ambil yang terbaru
});
if (!data) {
return {
success: false,
message: "Data pencegahan kriminalitas tidak ditemukan",
};
}
return {
success: true,
message: "Success fetch first pencegahan kriminalitas",
data,
};
} catch (error) {
console.error("Find first error:", error);
return {
success: false,
message: "Failed fetch first pencegahan kriminalitas",
};
}
}

View File

@@ -16,18 +16,21 @@ export default async function pencegahanKriminalitasFindMany(context: Context) {
if (search) { if (search) {
where.OR = [ where.OR = [
{ {
programKeamanan: { judul: {
nama: { contains: search, mode: "insensitive" as const }, contains: search,
mode: "insensitive" as const,
}, },
}, },
{ {
tipsKeamanan: { deskripsi: {
judul: { contains: search, mode: "insensitive" as const }, contains: search,
mode: "insensitive" as const,
}, },
}, },
{ {
videoKeamanan: { deskripsiSingkat: {
judul: { contains: search, mode: "insensitive" as const }, contains: search,
mode: "insensitive" as const,
}, },
}, },
]; ];
@@ -36,11 +39,6 @@ export default async function pencegahanKriminalitasFindMany(context: Context) {
const [data, total] = await Promise.all([ const [data, total] = await Promise.all([
prisma.pencegahanKriminalitas.findMany({ prisma.pencegahanKriminalitas.findMany({
where, where,
include: {
programKeamanan: true,
tipsKeamanan: true,
videoKeamanan: true,
},
skip, skip,
take: limit, take: limit,
orderBy: { createdAt: "desc" }, orderBy: { createdAt: "desc" },

View File

@@ -22,11 +22,6 @@ export default async function pencegahanKriminalitasFindUnique(request: Request)
const data = await prisma.pencegahanKriminalitas.findUnique({ const data = await prisma.pencegahanKriminalitas.findUnique({
where: {id}, where: {id},
include: {
programKeamanan: true,
tipsKeamanan: true,
videoKeamanan: true,
}
}) })
if (!data) { if (!data) {

View File

@@ -4,6 +4,7 @@ import pencegahanKriminalitasFindUnique from "./findUnique";
import pencegahanKriminalitasFindMany from "./findMany"; import pencegahanKriminalitasFindMany from "./findMany";
import pencegahanKriminalitasDelete from "./del"; import pencegahanKriminalitasDelete from "./del";
import pencegahanKriminalitasUpdate from "./updt"; import pencegahanKriminalitasUpdate from "./updt";
import pencegahanKriminalitasFindFirst from "./findFirst";
const PencegahanKriminalitas = new Elysia({ const PencegahanKriminalitas = new Elysia({
prefix: "pencegahankriminalitas", prefix: "pencegahankriminalitas",
@@ -11,22 +12,10 @@ const PencegahanKriminalitas = new Elysia({
}) })
.post("/create", pencegahanKriminalitasCreate, { .post("/create", pencegahanKriminalitasCreate, {
body: t.Object({ body: t.Object({
programKeamanan: t.Object({ judul: t.String(),
nama: t.String(), deskripsi: t.String(),
deskripsi: t.String(), deskripsiSingkat: t.String(),
slug: t.String(), linkVideo: t.String(),
}),
tipsKeamanan: t.Object({
judul: t.String(),
konten: t.String(),
slug: t.String(),
}),
videoKeamanan: t.Object({
judul: t.String(),
deskripsi: t.String(),
videoUrl: t.String(),
slug: t.String(),
}),
}), }),
}) })
.get("/find-many", pencegahanKriminalitasFindMany) .get("/find-many", pencegahanKriminalitasFindMany)
@@ -36,31 +25,20 @@ const PencegahanKriminalitas = new Elysia({
); );
return response; return response;
}) })
.get("/find-first", pencegahanKriminalitasFindFirst)
.delete("/del/:id", pencegahanKriminalitasDelete) .delete("/del/:id", pencegahanKriminalitasDelete)
.put( .put(
":/id", "/:id",
async (context) => { async (context) => {
const response = await pencegahanKriminalitasUpdate(context); const response = await pencegahanKriminalitasUpdate(context);
return response; return response;
}, },
{ {
body: t.Object({ body: t.Object({
programKeamanan: t.Object({ judul: t.String(),
nama: t.String(), deskripsi: t.String(),
deskripsi: t.String(), deskripsiSingkat: t.String(),
slug: t.String(), linkVideo: t.String(),
}),
tipsKeamanan: t.Object({
judul: t.String(),
konten: t.String(),
slug: t.String(),
}),
videoKeamanan: t.Object({
judul: t.String(),
deskripsi: t.String(),
videoUrl: t.String(),
slug: t.String(),
}),
}), }),
} }
); );

View File

@@ -3,110 +3,91 @@ import { Prisma } from "@prisma/client";
import { Context } from "elysia"; import { Context } from "elysia";
type FormUpdate = Prisma.PencegahanKriminalitasGetPayload<{ type FormUpdate = Prisma.PencegahanKriminalitasGetPayload<{
select: { select: {
id: true; id: true;
programKeamanan: true; judul: true;
tipsKeamanan: true; deskripsi: true;
videoKeamanan: true; deskripsiSingkat: true;
} linkVideo: true;
}> };
}>;
export default async function pencegahanKriminalitasUpdate(context: Context) { export default async function pencegahanKriminalitasUpdate(context: Context) {
try { try {
const id = context.params?.id as string; const id = context.params?.id as string;
const body = (await context.body) as Omit<FormUpdate, "id">; const body = (await context.body) as Omit<FormUpdate, "id">;
const {programKeamanan, tipsKeamanan, videoKeamanan} = body; const { judul, deskripsi, deskripsiSingkat, linkVideo } = body;
if (!id) { if (!id) {
return new Response(JSON.stringify({ return new Response(
success: false, JSON.stringify({
message: "ID tidak diberikan", success: false,
}), { message: "ID tidak diberikan",
status: 400, }),
headers: { {
"Content-Type": "application/json", status: 400,
}, headers: {
}); "Content-Type": "application/json",
},
} }
);
const existing = await prisma.pencegahanKriminalitas.findUnique({
where: { id },
include: {
programKeamanan: true,
tipsKeamanan: true,
videoKeamanan: true,
}
});
if (!existing) {
return new Response(JSON.stringify({
success: false,
message: "Pencegahan kriminalitas tidak ditemukan",
}), {
status: 404,
headers: {
"Content-Type": "application/json",
},
});
}
await prisma.programKeamanan.update({
where: {id: existing.programKeamananId},
data: {
nama: programKeamanan.nama,
deskripsi: programKeamanan.deskripsi,
slug: programKeamanan.slug,
}
});
await prisma.tipsKeamanan.update({
where: {id: existing.tipsKeamananId},
data: {
judul: tipsKeamanan.judul,
konten: tipsKeamanan.konten,
slug: tipsKeamanan.slug,
}
});
await prisma.videoKeamanan.update({
where: {id: existing.videoKeamananId},
data: {
judul: videoKeamanan.judul,
deskripsi: videoKeamanan.deskripsi,
videoUrl: videoKeamanan.videoUrl,
slug: videoKeamanan.slug,
}
});
const updated = await prisma.pencegahanKriminalitas.update({
where: { id },
data: {
programKeamananId: programKeamanan.id,
tipsKeamananId: tipsKeamanan.id,
videoKeamananId: videoKeamanan.id,
}
});
return new Response(JSON.stringify({
success: true,
message: "Success update pencegahan kriminalitas",
data: updated,
}), {
status: 200,
headers: {
"Content-Type": "application/json",
},
});
} catch (error) {
console.error("Update error:", error);
return new Response(JSON.stringify({
success: false,
message: "Failed update pencegahan kriminalitas",
}), {
status: 500,
headers: {
"Content-Type": "application/json",
},
});
} }
const existing = await prisma.pencegahanKriminalitas.findUnique({
where: { id },
});
if (!existing) {
return new Response(
JSON.stringify({
success: false,
message: "Pencegahan kriminalitas tidak ditemukan",
}),
{
status: 404,
headers: {
"Content-Type": "application/json",
},
}
);
}
const updated = await prisma.pencegahanKriminalitas.update({
where: { id },
data: {
judul,
deskripsi,
deskripsiSingkat,
linkVideo,
},
});
return new Response(
JSON.stringify({
success: true,
message: "Success update pencegahan kriminalitas",
data: updated,
}),
{
status: 200,
headers: {
"Content-Type": "application/json",
},
}
);
} catch (error) {
console.error("Update error:", error);
return new Response(
JSON.stringify({
success: false,
message: "Failed update pencegahan kriminalitas",
}),
{
status: 500,
headers: {
"Content-Type": "application/json",
},
}
);
}
} }

View File

@@ -0,0 +1,11 @@
import React from 'react';
function Page() {
return (
<div>
Page
</div>
);
}
export default Page;

View File

@@ -1,141 +1,138 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import pencegahanKriminalitasState from '@/app/admin/(dashboard)/_state/keamanan/pencegahan-kriminalitas';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Flex, Group, Paper, SimpleGrid, Stack, Text } from '@mantine/core'; import { Box, Button, Center, Flex, Paper, SimpleGrid, Skeleton, Stack, Text } from '@mantine/core';
import BackButton from '../../desa/layanan/_com/BackButto'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowRight } from '@tabler/icons-react'; import { IconArrowRight } from '@tabler/icons-react';
import { useTransitionRouter } from 'next-view-transitions';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
function Page() { function Page() {
const kriminalitasState = useProxy(pencegahanKriminalitasState);
const findFirst = useProxy(pencegahanKriminalitasState.findFirst);
const router = useTransitionRouter();
const {
data,
loading,
page,
load,
} = kriminalitasState.findMany;
useEffect(() => {
if (!findFirst.data && !findFirst.loading) {
kriminalitasState.findFirst.load();
}
}, [findFirst.data, findFirst.loading]);
useShallowEffect(() => {
const LIMIT = 3;
load(page, LIMIT);
}, [page]);
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={600} radius="md" />
</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 /> <Text fz={{ base: 'h1', md: '2.5rem' }} c={colors['blue-button']} fw="bold">
Pencegahan Kriminalitas
</Text>
<Text c={colors['blue-button']} fz={{ base: 'h4', md: 'h3' }}>
Keamanan Komunitas & Pencegahan Kriminal
</Text>
</Box> </Box>
<SimpleGrid <SimpleGrid
px={{ base: 20, md: 100 }} px={{ base: 20, md: 100 }}
cols={{ base: 1, md: 2 }} cols={{ base: 1, md: 2 }}
spacing="xl" spacing="xl"
> >
<Box pt={{ base: 0, md: 35 }}> <Paper p="xl" radius="xl" shadow="lg" >
<Text c={colors['blue-button']} fz={{ base: 'h4', md: 'h3' }}> <Text fz={{ base: 'h3', md: 'h2' }} c={colors['blue-button']} fw="bold">
Community Safety & Crime Prevention Program Keamanan Berjalan
</Text> </Text>
<Text fz={{ base: 'h1', md: '2.5rem' }} c={colors['blue-button']} fw="bold"> <Stack pt={30} gap="lg">
Emergency Contacts {data.length > 0 ? (
</Text> data.map((item) => (
<Group> <Paper key={item.id} p="md" bg={colors['blue-button']} radius="md" shadow="sm" onClick={() => router.push(`/darmasaba/keamanan/pencegahan-kriminalitas/${item.id}`)}>
<Stack gap={"xs"}>
<Flex align="center" justify="space-between">
<Text fz="h3" c={colors['white-1']}>
{item.judul}
</Text>
<IconArrowRight size={28} color={colors['white-1']} />
</Flex>
</Stack>
</Paper>
))
) : (
<Text color="dimmed">
Tidak ada data pencegahan kriminalitas yang cocok
</Text>
)}
<Button <Button
rightSection={<IconArrowRight />}
mt={20} mt={20}
fullWidth
radius="xl" radius="xl"
size="md" size="md"
bg={colors['blue-button']} bg={colors['blue-button']}
c={colors['white-1']} rightSection={<IconArrowRight size={20} color={colors['white-1']} />}
onClick={() => router.push(`/darmasaba/keamanan/pencegahan-kriminalitas/program-lainnya`)}
> >
View Details Jelajahi Program Lainnya
</Button> </Button>
</Group>
</Box>
<SimpleGrid cols={{ base: 1, md: 2 }} spacing="lg">
<Paper p="lg" radius="xl" shadow="md">
<Text c={colors['blue-button']} fw="bold" fz={{ base: 'h4', md: 'h3' }}>
How to Keep Your Neighborhood Safe
</Text>
<Text fz={{ base: 'h5', md: 'h4' }} c={colors['blue-button']} mt="sm">
Practical safety habits everyone can apply daily to reduce risks.
</Text>
<Group>
<Button
rightSection={<IconArrowRight />}
mt={20}
radius="xl"
size="md"
bg={colors['blue-button']}
c={colors['white-1']}
>
Learn More
</Button>
</Group>
</Paper>
<Paper p="lg" radius="xl" shadow="md">
<Text c={colors['blue-button']} fw="bold" fz={{ base: 'h4', md: 'h3' }}>
Recognizing Criminal Activities
</Text>
<Text fz={{ base: 'h5', md: 'h4' }} c={colors['blue-button']} mt="sm">
Key warning signs and behavior patterns you should stay aware of.
</Text>
<Group>
<Button
rightSection={<IconArrowRight />}
mt={20}
radius="xl"
size="md"
bg={colors['blue-button']}
c={colors['white-1']}
>
Learn More
</Button>
</Group>
</Paper>
</SimpleGrid>
<Paper p="xl" radius="xl" shadow="lg">
<Text fz={{ base: 'h3', md: 'h2' }} c={colors['blue-button']} fw="bold">
Ongoing Security Programs
</Text>
<Stack pt={30} gap="lg">
{['Night Patrol', 'Neighborhood Watch', 'Emergency Preparedness'].map((program, i) => (
<Paper key={i} p="md" bg={colors['blue-button']} radius="md" shadow="sm">
<Flex align="center" justify="space-between">
<Text fz="h3" c={colors['white-1']}>
{program}
</Text>
<IconArrowRight size={28} color={colors['white-1']} />
</Flex>
</Paper>
))}
<Box pt={25}>
<Button
fullWidth
radius="xl"
size="md"
bg={colors['blue-button']}
rightSection={<IconArrowRight size={20} color={colors['white-1']} />}
>
Explore More Programs
</Button>
</Box>
</Stack> </Stack>
</Paper> </Paper>
<Box> <Box>
<Paper p="xl" radius="xl" shadow="lg"> {findFirst.loading ? (
<Box style={{ maxWidth: 560, width: '100%', aspectRatio: '16/9' }}> <Center><Skeleton h={400} /></Center>
<iframe ) : findFirst.data ? (
width="100%" <Paper p="xl" radius="xl" shadow="lg">
height="100%" {findFirst.data?.linkVideo ? (
src="https://www.youtube.com/embed/p5OkdBgasWA?si=6lFKXeE9LN5zcJQq" <Box
title="Community Safety Patrol" component="iframe"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" src={convertToEmbedUrl(findFirst.data.linkVideo)}
/> width="100%"
</Box> height={300}
<Text py={10} fz={{ base: 'h3', md: 'h2' }} fw="bold" c={colors['blue-button']}> allowFullScreen
Darmasaba Night Patrol style={{ borderRadius: 8 }}
</Text> />
<Text fz="h4" c={colors['blue-button']}> ) : (
A closer look at how the community works together to maintain safety. <Text fz="sm" c="dimmed">Tidak ada video</Text>
</Text> )}
<Box pt={10}> <Text py={10} fz={{ base: 'h3', md: 'h2' }} fw="bold" c={colors['blue-button']}>
<Button {findFirst.data?.judul}
radius="xl" </Text>
size="md" <Text fz="h4" c={colors['blue-button']}>
bg={colors['blue-button']} {findFirst.data?.deskripsiSingkat}
rightSection={<IconArrowRight size={20} color={colors['white-1']} />} </Text>
> </Paper>
Watch More Videos ) : null}
</Button>
</Box>
</Paper>
</Box> </Box>
</SimpleGrid> </SimpleGrid>
</Stack> </Stack>
); );
function convertToEmbedUrl(youtubeUrl: string): string {
try {
const url = new URL(youtubeUrl);
const videoId = url.searchParams.get("v");
if (!videoId) return youtubeUrl;
return `https://www.youtube.com/embed/${videoId}`;
} catch (err) {
console.error("Error converting YouTube URL to embed:", err);
return youtubeUrl;
}
}
} }
export default Page; export default Page;

View File

@@ -0,0 +1,11 @@
import React from 'react';
function Page() {
return (
<div>
Page
</div>
);
}
export default Page;