Fix Admin - User Menu Keamanan, Submenu Laporan Kontak Darurat, Laporan Publik

This commit is contained in:
2025-09-17 14:59:46 +08:00
parent 39e1e7b575
commit 79ad39fc55
50 changed files with 1326 additions and 1021 deletions

View File

@@ -4,10 +4,12 @@
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import artikelKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/artikelKesehatan';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import {
Box,
Button,
Group,
Image,
Paper,
Stack,
Text,
@@ -15,7 +17,8 @@ import {
Title,
Tooltip
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
@@ -24,6 +27,7 @@ import { useProxy } from 'valtio/utils';
interface ArtikelKesehatanFormBase {
title: string;
content: string;
imageId: string;
introduction: { content: string };
symptom: { title: string; content: string };
prevention: { title: string; content: string };
@@ -37,29 +41,32 @@ function EditArtikelKesehatan() {
const router = useRouter();
const params = useParams();
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState<ArtikelKesehatanFormBase>({
title: stateArtikelKesehatan.edit.form.title || '',
content: stateArtikelKesehatan.edit.form.content || '',
introduction: { content: stateArtikelKesehatan.edit.form.introduction?.content || '' },
title: stateArtikelKesehatan.edit.form.title,
content: stateArtikelKesehatan.edit.form.content,
imageId: stateArtikelKesehatan.edit.form.imageId,
introduction: { content: stateArtikelKesehatan.edit.form.introduction?.content },
symptom: {
title: stateArtikelKesehatan.edit.form.symptom?.title || '',
content: stateArtikelKesehatan.edit.form.symptom?.content || ''
title: stateArtikelKesehatan.edit.form.symptom?.title,
content: stateArtikelKesehatan.edit.form.symptom?.content
},
prevention: {
title: stateArtikelKesehatan.edit.form.prevention?.title || '',
content: stateArtikelKesehatan.edit.form.prevention?.content || ''
title: stateArtikelKesehatan.edit.form.prevention?.title,
content: stateArtikelKesehatan.edit.form.prevention?.content
},
firstAid: {
title: stateArtikelKesehatan.edit.form.firstAid?.title || '',
content: stateArtikelKesehatan.edit.form.firstAid?.content || ''
title: stateArtikelKesehatan.edit.form.firstAid?.title,
content: stateArtikelKesehatan.edit.form.firstAid?.content
},
mythVsFact: {
title: stateArtikelKesehatan.edit.form.mythVsFact?.title || '',
mitos: stateArtikelKesehatan.edit.form.mythVsFact?.mitos || '',
fakta: stateArtikelKesehatan.edit.form.mythVsFact?.fakta || ''
title: stateArtikelKesehatan.edit.form.mythVsFact?.title,
mitos: stateArtikelKesehatan.edit.form.mythVsFact?.mitos,
fakta: stateArtikelKesehatan.edit.form.mythVsFact?.fakta
},
doctorSign: {
content: stateArtikelKesehatan.edit.form.doctorSign?.content || ''
content: stateArtikelKesehatan.edit.form.doctorSign?.content
}
});
@@ -76,6 +83,7 @@ function EditArtikelKesehatan() {
title: form.title,
content: form.content,
introduction: { content: form.introduction?.content || '' },
imageId: form.imageId,
symptom: {
title: form.symptom?.title || '',
content: form.symptom?.content || ''
@@ -97,6 +105,10 @@ function EditArtikelKesehatan() {
content: form.doctorSign?.content || ''
}
});
if (form?.imageId) {
setPreviewImage(`${process.env.NEXT_PUBLIC_API_URL}/file/${form.imageId}`);
}
}
} catch (error) {
console.error("Error loading artikel kesehatan:", error);
@@ -109,6 +121,20 @@ function EditArtikelKesehatan() {
const handleSubmit = async () => {
try {
stateArtikelKesehatan.edit.form = { ...formData };
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
stateArtikelKesehatan.edit.form.imageId = uploaded.id;
}
const success = await stateArtikelKesehatan.edit.submit();
if (success) {
toast.success("Artikel kesehatan berhasil diperbarui!");
@@ -151,6 +177,60 @@ function EditArtikelKesehatan() {
onChange={(e) => setFormData(prev => ({ ...prev, title: e.target.value }))}
required
/>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Gambar Berita
</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error("File tidak valid, gunakan format gambar")}
maxSize={5 * 1024 ** 2}
accept={{ "image/*": [] }}
radius="md"
p="xl"
>
<Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept>
<IconUpload size={48} color={colors["blue-button"]} stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={48} color="red" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={48} color="#868e96" stroke={1.5} />
</Dropzone.Idle>
<Stack gap="xs" align="center">
<Text size="md" fw={500}>
Seret gambar atau klik untuk memilih file
</Text>
<Text size="sm" c="dimmed">
Maksimal 5MB, format gambar wajib
</Text>
</Stack>
</Group>
</Dropzone>
{previewImage && (
<Box mt="sm" style={{ display: "flex", justifyContent: "center" }}>
<Image
src={previewImage}
alt="Preview Gambar"
radius="md"
style={{
maxHeight: 220,
objectFit: "contain",
border: `1px solid ${colors["blue-button"]}`,
}}
/>
</Box>
)}
</Box>
<TextInput
label="Deskripsi"
placeholder="Masukkan deskripsi artikel"

View File

@@ -6,6 +6,7 @@ import {
Box,
Button,
Group,
Image,
Paper,
Skeleton,
Stack,
@@ -81,6 +82,22 @@ function DetailArtikelKesehatan() {
<Text fz="lg" fw="bold">Judul</Text>
<Text fz="md" c="dimmed">{data.title}</Text>
</Box>
{/* Gambar */}
<Box>
<Text fz="lg" fw="bold">Gambar</Text>
{data.image?.link ? (
<Image
src={data.image.link}
alt={data.title || 'Gambar Berita'}
w={200}
h={200}
radius="md"
fit="cover"
/>
) : (
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
)}
</Box>
{/* Deskripsi */}
<Box>

View File

@@ -2,10 +2,12 @@
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import artikelKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/artikelKesehatan';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import {
Box,
Button,
Group,
Image,
Paper,
Stack,
Text,
@@ -13,19 +15,24 @@ import {
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function CreateArtikelKesehatan() {
const stateArtikelKesehatan = useProxy(artikelKesehatanState);
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const router = useRouter();
const resetForm = () => {
stateArtikelKesehatan.create.form = {
title: '',
content: '',
imageId: '',
introduction: {
content: '',
},
@@ -50,10 +57,27 @@ function CreateArtikelKesehatan() {
content: '',
},
};
setPreviewImage(null);
setFile(null);
};
const handleSubmit = async (e?: React.FormEvent) => {
e?.preventDefault();
if (!file) {
return toast.warn('Silakan pilih file gambar terlebih dahulu');
}
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error('Gagal mengunggah gambar, silakan coba lagi');
}
stateArtikelKesehatan.create.form.imageId = uploaded.id;
await stateArtikelKesehatan.create.submit();
toast.success('Data berhasil disimpan');
resetForm();
@@ -89,6 +113,56 @@ function CreateArtikelKesehatan() {
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<Box>
<Text fw="bold" fz="sm" mb={6}>
Gambar Berita
</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }}
radius="md"
p="xl"
>
<Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept>
<IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
</Group>
<Text ta="center" mt="sm" size="sm" color="dimmed">
Seret gambar atau klik untuk memilih file (maks 5MB)
</Text>
</Dropzone>
{previewImage && (
<Box mt="sm" style={{ textAlign: 'center' }}>
<Image
src={previewImage}
alt="Preview Gambar"
radius="md"
style={{
maxHeight: 200,
objectFit: 'contain',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
<TextInput
label={"Judul"}
placeholder="Masukkan judul"

View File

@@ -5,8 +5,8 @@ import {
Button,
Center,
Group,
Paper,
Pagination,
Paper,
Skeleton,
Stack,
Table,
@@ -20,26 +20,18 @@ import {
Tooltip,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import artikelKesehatanState from '../../../_state/kesehatan/data_kesehatan_warga/artikelKesehatan';
import { useState } from 'react';
function ArtikelKesehatan() {
const router = useRouter();
const [search, setSearch] = useState("");
return (
<Box>
{/* Tombol Back */}
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors["blue-button"]} size={25} />
</Button>
</Box>
{/* Header Search */}
<HeaderSearch
title='Artikel Kesehatan'