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

@@ -6,45 +6,17 @@ import { proxy } from "valtio";
import { z } from "zod";
const templateForm = z.object({
pencegahanKriminalitas: z.object({
programKeamanan: z.object({
nama: z.string().min(1, "Nama minimal 1 karakter"),
deskripsi: z.string().min(1, "Deskripsi 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"),
}),
}),
judul: z.string().min(1, "Judul minimal 1 karakter"),
deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
deskripsiSingkat: z.string().min(1, "Deskripsi singkat minimal 1 karakter"),
linkVideo: z.string().min(1, "Link video minimal 1 karakter"),
});
const defaultForm = {
pencegahanKriminalitas: {
programKeamanan: {
nama: "",
deskripsi: "",
slug: "",
},
tipsKeamanan: {
judul: "",
konten: "",
slug: "",
},
videoKeamanan: {
judul: "",
deskripsi: "",
videoUrl: "",
slug: "",
},
},
judul: "",
deskripsi: "",
deskripsiSingkat: "",
linkVideo: "",
};
const pencegahanKriminalitasState = proxy({
@@ -65,7 +37,7 @@ const pencegahanKriminalitasState = proxy({
pencegahanKriminalitasState.create.loading = true;
const res = await ApiFetch.api.keamanan.pencegahankriminalitas[
"create"
].post(pencegahanKriminalitasState.create.form.pencegahanKriminalitas);
].post(pencegahanKriminalitasState.create.form);
if (res.status === 200) {
pencegahanKriminalitasState.findMany.load();
return toast.success("success create");
@@ -82,11 +54,7 @@ const pencegahanKriminalitasState = proxy({
findMany: {
data: null as
| Prisma.PencegahanKriminalitasGetPayload<{
include: {
programKeamanan: true;
tipsKeamanan: true;
videoKeamanan: true;
};
omit: { isActive: true };
}>[]
| null,
page: 1,
@@ -125,11 +93,7 @@ const pencegahanKriminalitasState = proxy({
},
findUnique: {
data: null as Prisma.PencegahanKriminalitasGetPayload<{
include: {
programKeamanan: true;
tipsKeamanan: true;
videoKeamanan: true;
};
omit: { isActive: true };
}> | null,
loading: false,
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: {
loading: false,
async byId(id: string) {
@@ -213,24 +201,10 @@ const pencegahanKriminalitasState = proxy({
const data = result.data;
pencegahanKriminalitasState.update.id = data.id;
pencegahanKriminalitasState.update.form = {
pencegahanKriminalitas: {
programKeamanan: {
nama: data.programKeamanan.nama,
deskripsi: data.programKeamanan.deskripsi,
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,
},
},
judul: data.judul,
deskripsi: data.deskripsi,
deskripsiSingkat: data.deskripsiSingkat,
linkVideo: data.linkVideo,
};
return data;
} else {
@@ -266,40 +240,11 @@ const pencegahanKriminalitasState = proxy({
"Content-Type": "application/json",
},
body: JSON.stringify({
pencegahanKriminalitas: {
programKeamanan: {
nama: pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.programKeamanan.nama,
deskripsi:
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,
},
},
judul: pencegahanKriminalitasState.update.form.judul,
deskripsi: pencegahanKriminalitasState.update.form.deskripsi,
deskripsiSingkat:
pencegahanKriminalitasState.update.form.deskripsiSingkat,
linkVideo: pencegahanKriminalitasState.update.form.linkVideo,
}),
}
);

View File

@@ -121,7 +121,7 @@ function DetailLaporanPublik() {
<Box>
<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>

View File

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

View File

@@ -2,39 +2,35 @@
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
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 { 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 { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditPencegahanKriminalitas() {
const router = useRouter();
const params = useParams()
const kriminalitasState = useProxy(pencegahanKriminalitasState)
const params = useParams();
const kriminalitasState = useProxy(pencegahanKriminalitasState);
const [formData, setFormData] = useState({
pencegahanKriminalitas: {
programKeamanan: {
nama: kriminalitasState.update.form.pencegahanKriminalitas.programKeamanan.nama,
deskripsi: kriminalitasState.update.form.pencegahanKriminalitas.programKeamanan.deskripsi,
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,
},
},
})
judul: '',
deskripsi: '',
deskripsiSingkat: '',
linkVideo: '',
});
useEffect(() => {
const loadKriminalitas = async () => {
@@ -43,167 +39,154 @@ function EditPencegahanKriminalitas() {
try {
const data = await kriminalitasState.update.load(id);
if (data && data.pencegahanKriminalitas) {
const { programKeamanan, tipsKeamanan, videoKeamanan } = data.pencegahanKriminalitas;
if (data) {
setFormData({
pencegahanKriminalitas: {
programKeamanan: {
nama: programKeamanan?.nama || "",
deskripsi: programKeamanan?.deskripsi || "",
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 || "",
},
},
judul: data.judul || '',
deskripsi: data.deskripsi || '',
deskripsiSingkat: data.deskripsiSingkat || '',
linkVideo: data.linkVideo || '',
});
}
} catch (error) {
console.error("Error loading pencegahan kriminalitas:", error);
toast.error("Gagal memuat data pencegahan kriminalitas");
console.error('Error loading pencegahan kriminalitas:', error);
toast.error('Gagal memuat data pencegahan kriminalitas');
}
}
};
loadKriminalitas();
}, [params.id]);
}, [params?.id]);
const embedLink = convertYoutubeUrlToEmbed(formData.linkVideo);
const handleSubmit = async () => {
const converted = convertYoutubeUrlToEmbed(formData.linkVideo);
if (!converted) {
toast.error('Link YouTube tidak valid. Pastikan formatnya benar.');
return;
}
try {
// Update the form data first
kriminalitasState.update.form = {
...kriminalitasState.update.form,
pencegahanKriminalitas: {
programKeamanan: {
nama: formData.pencegahanKriminalitas.programKeamanan.nama,
deskripsi: formData.pencegahanKriminalitas.programKeamanan.deskripsi,
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>
judul: formData.judul,
deskripsi: formData.deskripsi,
deskripsiSingkat: formData.deskripsiSingkat,
linkVideo: formData.linkVideo,
};
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Pencegahan Kriminalitas</Title>
// Set the ID and then call update
kriminalitasState.update.id = params?.id as string;
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
value={formData.pencegahanKriminalitas.programKeamanan.nama}
onChange={(val) => {
formData.pencegahanKriminalitas.programKeamanan.nama = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Judul Program Keamanan</Text>}
placeholder='Masukkan judul Program Keamanan'
label="Judul"
placeholder="Masukkan judul"
value={formData.judul}
onChange={(e) => setFormData({ ...formData, judul: e.target.value })}
required
/>
<TextInput
value={formData.pencegahanKriminalitas.programKeamanan.slug}
onChange={(val) => {
formData.pencegahanKriminalitas.programKeamanan.slug = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Slug Program Keamanan</Text>}
placeholder='Masukkan slug Program Keamanan'
label="Deskripsi Singkat"
placeholder="Masukkan deskripsi singkat"
value={formData.deskripsiSingkat}
onChange={(e) =>
setFormData({ ...formData, deskripsiSingkat: e.target.value })
}
required
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Program Keamanan</Text>
<Title order={6} fw="bold" fz="sm" mb={6}>
Deskripsi Lengkap
</Title>
<EditEditor
value={formData.pencegahanKriminalitas.programKeamanan.deskripsi}
onChange={(val) => {
formData.pencegahanKriminalitas.programKeamanan.deskripsi = val;
}}
value={formData.deskripsi}
onChange={(val) =>
setFormData({ ...formData, deskripsi: val })
}
/>
</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>
<Text fw={"bold"} fz={"sm"}>Deskripsi Tips Keamanan</Text>
<EditEditor
value={formData.pencegahanKriminalitas.tipsKeamanan.konten}
onChange={(val) => {
formData.pencegahanKriminalitas.tipsKeamanan.konten = val;
}}
<TextInput
label="Link Video YouTube"
placeholder="https://www.youtube.com/watch?v=abc123"
value={formData.linkVideo}
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>
<TextInput
value={formData.pencegahanKriminalitas.videoKeamanan.judul}
onChange={(val) => {
formData.pencegahanKriminalitas.videoKeamanan.judul = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Judul Video Keamanan</Text>}
placeholder='Masukkan judul Video Keamanan'
/>
<TextInput
value={formData.pencegahanKriminalitas.videoKeamanan.slug}
onChange={(val) => {
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;
{/* Action button */}
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
/>
</Box>
<TextInput
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>
>
Simpan
</Button>
</Group>
</Stack>
</Paper>

View File

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

View File

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

View File

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