Compare commits

...

8 Commits

Author SHA1 Message Date
242ea86f77 Fix konsisten font, menu landing page & PPID 2025-12-10 17:44:31 +08:00
99c2c9c6d7 Fix semua tulisan profile jadi profil, mulai dari navbar, dan route 2025-12-10 14:16:15 +08:00
ac2fc1a705 Fix QC Kak Inno 8 Des
Fix QC Kak Ayu 8 Des
Fix QC Pak Jun 8 Des
2025-12-09 17:27:23 +08:00
9dbe172165 Fix QC Kak Inno Tgl 4 & 5 Desember
Fix QC Kak Ayu Tgl 4 & 5 Desember
Fix QC Pak Jun Tgl 5 Desember
2025-12-09 12:00:27 +08:00
cc318d4d54 Fix QC Kak Inno Tgl 4 & 5 Desember
Fix QC Kak Ayu Tgl 4 & 5 Desember
Fix QC Pak Jun Tgl 5 Desember
2025-12-09 10:28:17 +08:00
dcb8017594 Fix undefined ke detail berita terbaru 2025-12-05 17:42:04 +08:00
ec3ad12531 Fix Notifikasi saat ada berita atau pengumuman baru, notifikasi baru muncul. Ga setiap masuk landing page ada notifikasi 2025-12-05 14:30:53 +08:00
dad44c0537 Fix Menu Gallery : Gallery Foto
Fix detail berita
2025-12-05 10:56:03 +08:00
125 changed files with 4688 additions and 2782 deletions

View File

@@ -828,11 +828,11 @@ model DokterdanTenagaMedis {
name String
specialist String
jadwal String
jadwalLibur String
jamBukaOperasional String
jamTutupOperasional String
jamBukaLibur String
jamTutupLibur String
jadwalLibur String?
jamBukaOperasional String?
jamTutupOperasional String?
jamBukaLibur String?
jamTutupLibur String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())

BIN
public/mangupuraaward.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

View File

@@ -6,145 +6,176 @@ import { z } from "zod";
const templateForm = z.object({
name: z.string().min(3, "Nama minimal 3 karakter"),
nik: z.string().min(3, "NIK minimal 3 karakter"),
notelp: z.string().min(3, "Nomor Telepon minimal 3 karakter"),
nik: z
.string()
.min(3, "NIK minimal 3 karakter")
.max(16, "NIK maksimal 16 angka"),
notelp: z
.string()
.min(3, "Nomor Telepon minimal 3 karakter")
.max(15, "Nomor Telepon maksimal 15 angka"),
alamat: z.string().min(3, "Alamat minimal 3 karakter"),
email: z.string().min(3, "Email minimal 3 karakter"),
jenisInformasiDimintaId: z.string().nonempty(),
caraMemperolehInformasiId: z.string().nonempty(),
caraMemperolehSalinanInformasiId: z.string().nonempty(),
})
});
const jenisInformasiDiminta = proxy({
findMany: {
data: null as
| null
| Prisma.JenisInformasiDimintaGetPayload<{ omit: { isActive: true } }>[],
async load(){
const res = await ApiFetch.api.ppid.permohonaninformasipublik.jenisInformasi["find-many"].get();
if (res.status === 200) {
jenisInformasiDiminta.findMany.data = res.data?.data ?? [];
}
}
}
})
findMany: {
data: null as
| null
| Prisma.JenisInformasiDimintaGetPayload<{ omit: { isActive: true } }>[],
async load() {
const res =
await ApiFetch.api.ppid.permohonaninformasipublik.jenisInformasi[
"find-many"
].get();
if (res.status === 200) {
jenisInformasiDiminta.findMany.data = res.data?.data ?? [];
}
},
},
});
const caraMemperolehInformasi = proxy({
findMany: {
data: null as
| null
| Prisma.CaraMemperolehInformasiGetPayload<{ omit: { isActive: true } }>[],
async load() {
const res = await ApiFetch.api.ppid.permohonaninformasipublik.memperolehInformasi["find-many"].get();
if (res.status === 200) {
caraMemperolehInformasi.findMany.data = res.data?.data ?? [];
}
}
}
})
findMany: {
data: null as
| null
| Prisma.CaraMemperolehInformasiGetPayload<{
omit: { isActive: true };
}>[],
async load() {
const res =
await ApiFetch.api.ppid.permohonaninformasipublik.memperolehInformasi[
"find-many"
].get();
if (res.status === 200) {
caraMemperolehInformasi.findMany.data = res.data?.data ?? [];
}
},
},
});
const caraMemperolehSalinanInformasi = proxy({
findMany: {
data: null as
| null
| Prisma.CaraMemperolehSalinanInformasiGetPayload<{ omit: { isActive: true } }>[],
async load() {
const res = await ApiFetch.api.ppid.permohonaninformasipublik.salinanInformasi["find-many"].get();
if (res.status === 200) {
caraMemperolehSalinanInformasi.findMany.data = res.data?.data ?? [];
}
}
}
})
console.log(caraMemperolehSalinanInformasi)
findMany: {
data: null as
| null
| Prisma.CaraMemperolehSalinanInformasiGetPayload<{
omit: { isActive: true };
}>[],
async load() {
const res =
await ApiFetch.api.ppid.permohonaninformasipublik.salinanInformasi[
"find-many"
].get();
if (res.status === 200) {
caraMemperolehSalinanInformasi.findMany.data = res.data?.data ?? [];
}
},
},
});
console.log(caraMemperolehSalinanInformasi);
type PermohonanInformasiPublikForm = Prisma.PermohonanInformasiPublikGetPayload<{
type PermohonanInformasiPublikForm =
Prisma.PermohonanInformasiPublikGetPayload<{
select: {
name: true;
nik: true;
notelp: true;
alamat: true;
email: true;
jenisInformasiDimintaId: true;
caraMemperolehInformasiId: true;
caraMemperolehSalinanInformasiId: true;
name: true;
nik: true;
notelp: true;
alamat: true;
email: true;
jenisInformasiDimintaId: true;
caraMemperolehInformasiId: true;
caraMemperolehSalinanInformasiId: true;
};
}>;
}>;
const statepermohonanInformasiPublik = proxy({
create: {
form: {} as PermohonanInformasiPublikForm,
loading: false,
async create(){
const cek = templateForm.safeParse(statepermohonanInformasiPublik.create.form);
if(!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
statepermohonanInformasiPublik.create.loading = true;
const res = await ApiFetch.api.ppid.permohonaninformasipublik["create"].post(statepermohonanInformasiPublik.create.form);
if (res.status === 200) {
statepermohonanInformasiPublik.findMany.load();
return toast.success("Sukses menambahkan");
}
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
statepermohonanInformasiPublik.create.loading = false;
}
create: {
form: {} as PermohonanInformasiPublikForm,
loading: false,
async create() {
const cek = templateForm.safeParse(
statepermohonanInformasiPublik.create.form
);
if (!cek.success) {
toast.error(cek.error.issues.map((i) => i.message).join("\n"));
return false; // ⬅️ tambahkan return false
}
try {
statepermohonanInformasiPublik.create.loading = true;
const res = await ApiFetch.api.ppid.permohonaninformasipublik[
"create"
].post(statepermohonanInformasiPublik.create.form);
if (res.data?.success === false) {
toast.error(res.data?.message);
return false; // ⬅️ gagal
}
toast.success("Sukses menambahkan");
return true; // ⬅️ sukses
} catch {
toast.error("Terjadi kesalahan server");
return false;
} finally {
statepermohonanInformasiPublik.create.loading = false;
}
},
findMany: {
data: null as
| Prisma.PermohonanInformasiPublikGetPayload<{ include: {
caraMemperolehSalinanInformasi: true,
jenisInformasiDiminta: true,
caraMemperolehInformasi: true,
} }>[]
| null,
async load() {
const res = await ApiFetch.api.ppid.permohonaninformasipublik["find-many"].get();
if (res.status === 200) {
statepermohonanInformasiPublik.findMany.data = res.data?.data ?? [];
}
}
},
findUnique: {
data: null as Prisma.PermohonanInformasiPublikGetPayload<{
},
findMany: {
data: null as
| Prisma.PermohonanInformasiPublikGetPayload<{
include: {
jenisInformasiDiminta: true,
caraMemperolehInformasi: true,
caraMemperolehSalinanInformasi: true,
caraMemperolehSalinanInformasi: true;
jenisInformasiDiminta: true;
caraMemperolehInformasi: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/ppid/permohonaninformasipublik/${id}`);
if (res.ok) {
const data = await res.json();
statepermohonanInformasiPublik.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch program inovasi:", res.statusText);
statepermohonanInformasiPublik.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching program inovasi:", error);
statepermohonanInformasiPublik.findUnique.data = null;
}
},
},
})
}>[]
| null,
async load() {
const res = await ApiFetch.api.ppid.permohonaninformasipublik[
"find-many"
].get();
if (res.status === 200) {
statepermohonanInformasiPublik.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.PermohonanInformasiPublikGetPayload<{
include: {
jenisInformasiDiminta: true;
caraMemperolehInformasi: true;
caraMemperolehSalinanInformasi: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/ppid/permohonaninformasipublik/${id}`);
if (res.ok) {
const data = await res.json();
statepermohonanInformasiPublik.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch program inovasi:", res.statusText);
statepermohonanInformasiPublik.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching program inovasi:", error);
statepermohonanInformasiPublik.findUnique.data = null;
}
},
},
});
const statepermohonanInformasiPublikForm = proxy({
statepermohonanInformasiPublik,
jenisInformasiDiminta,
caraMemperolehInformasi,
caraMemperolehSalinanInformasi,
})
statepermohonanInformasiPublik,
jenisInformasiDiminta,
caraMemperolehInformasi,
caraMemperolehSalinanInformasi,
});
export default statepermohonanInformasiPublikForm;

View File

@@ -5,82 +5,99 @@ import { proxy } from "valtio";
import { z } from "zod";
const templateForm = z.object({
name: z.string().min(3, "Nama minimal 3 karakter"),
email: z.string().min(3, "Email minimal 3 karakter"),
notelp: z.string().min(3, "Nomor Telepon minimal 3 karakter"),
alasan: z.string().min(3, "Alasan minimal 3 karakter"),
})
name: z.string().min(3, "Nama minimal 3 karakter"),
email: z.string().min(3, "Email minimal 3 karakter"),
notelp: z
.string()
.min(3, "Nomor Telepon minimal 3 karakter")
.max(15, "Nomor Telepon maksimal 15 angka"),
alasan: z.string().min(3, "Alasan minimal 3 karakter"),
});
type PermohonanKeberatanInformasiForm = Prisma.FormulirPermohonanKeberatanGetPayload<{
type PermohonanKeberatanInformasiForm =
Prisma.FormulirPermohonanKeberatanGetPayload<{
select: {
name: true;
email: true;
notelp: true;
alasan: true;
name: true;
email: true;
notelp: true;
alasan: true;
};
}>;
}>;
const permohonanKeberatanInformasi = proxy({
create: {
form: {} as PermohonanKeberatanInformasiForm,
loading: false,
async create(){
const cek = templateForm.safeParse(permohonanKeberatanInformasi.create.form);
if(!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
permohonanKeberatanInformasi.create.loading = true;
const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik["create"].post(permohonanKeberatanInformasi.create.form);
if (res.status === 200) {
permohonanKeberatanInformasi.findMany.load();
return toast.success("Sukses menambahkan");
}
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
permohonanKeberatanInformasi.create.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.FormulirPermohonanKeberatanGetPayload<{omit: {isActive: true}}>[]
| null,
async load() {
const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik["find-many"].get();
if (res.status === 200) {
permohonanKeberatanInformasi.findMany.data = res.data?.data ?? [];
}
}
},
findUnique: {
data: null as Prisma.FormulirPermohonanKeberatanGetPayload<{
omit: {
isActive: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/ppid/permohonankeberataninformasipublik/${id}`);
if (res.ok) {
const data = await res.json();
permohonanKeberatanInformasi.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch permohonan keberatan informasi:", res.statusText);
permohonanKeberatanInformasi.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching permohonan keberatan informasi:", error);
permohonanKeberatanInformasi.findUnique.data = null;
}
},
create: {
form: {} as PermohonanKeberatanInformasiForm,
loading: false,
async create() {
const cek = templateForm.safeParse(
permohonanKeberatanInformasi.create.form
);
if (!cek.success) {
toast.error(cek.error.issues.map((i) => i.message).join("\n"));
return false; // ⬅️ tambahkan return false
}
try {
permohonanKeberatanInformasi.create.loading = true;
const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik[
"create"
].post(permohonanKeberatanInformasi.create.form);
if (res.data?.success === false) {
toast.error(res.data?.message);
return false; // ⬅️ gagal
}
toast.success("Sukses menambahkan");
return true; // ⬅️ sukses
} catch {
toast.error("Terjadi kesalahan server");
return false;
} finally {
permohonanKeberatanInformasi.create.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.FormulirPermohonanKeberatanGetPayload<{
omit: { isActive: true };
}>[]
| null,
async load() {
const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik[
"find-many"
].get();
if (res.status === 200) {
permohonanKeberatanInformasi.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.FormulirPermohonanKeberatanGetPayload<{
omit: {
isActive: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(
`/api/ppid/permohonankeberataninformasipublik/${id}`
);
if (res.ok) {
const data = await res.json();
permohonanKeberatanInformasi.findUnique.data = data.data ?? null;
} else {
console.error(
"Failed to fetch permohonan keberatan informasi:",
res.statusText
);
permohonanKeberatanInformasi.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching permohonan keberatan informasi:", error);
permohonanKeberatanInformasi.findUnique.data = null;
}
},
},
});
export default permohonanKeberatanInformasi;

View File

@@ -0,0 +1,303 @@
/* eslint-disable react-hooks/exhaustive-deps */
"use client";
import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
import stateGallery from "@/app/admin/(dashboard)/_state/desa/gallery";
import colors from "@/con/colors";
import ApiFetch from "@/lib/api-fetch";
import {
ActionIcon,
Box,
Button,
Group,
Image,
Loader,
Paper,
Stack,
Text,
TextInput,
Title
} from "@mantine/core";
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";
import { useProxy } from "valtio/utils";
function EditFoto() {
const FotoState = useProxy(stateGallery.foto);
const router = useRouter();
const params = useParams();
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
name: "",
deskripsi: "",
imagesId: "",
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [originalData, setOriginalData] = useState({
name: "",
deskripsi: "",
imagesId: "",
imageUrl: "",
});
// Load kategori + Foto
useEffect(() => {
FotoState.findMany.load();
const loadFoto = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await FotoState.update.load(id);
if (data) {
setFormData({
name: data.name || "",
deskripsi: data.deskripsi || "",
imagesId: data.imagesId || "",
});
setOriginalData({
name: data.name || "",
deskripsi: data.deskripsi || "",
imagesId: data.imagesId || "",
imageUrl: data.imageGalleryFoto?.link || ""
});
if (data?.imageGalleryFoto?.link) {
setPreviewImage(data.imageGalleryFoto.link);
}
}
} catch (error) {
console.error("Error loading Foto:", error);
toast.error("Gagal memuat data Foto");
}
};
loadFoto();
}, [params?.id]);
const handleChange = (field: string, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
const handleSubmit = async () => {
try {
setIsSubmitting(true);
// Update global state hanya sekali di sini
FotoState.update.form = {
...FotoState.update.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");
}
FotoState.update.form.imagesId = uploaded.id;
}
await FotoState.update.update();
toast.success("Foto berhasil diperbarui!");
router.push("/admin/desa/gallery/foto");
} catch (error) {
console.error("Error updating foto:", error);
toast.error("Terjadi kesalahan saat memperbarui foto");
} finally {
setIsSubmitting(false);
}
};
const handleResetForm = () => {
setFormData({
name: originalData.name,
deskripsi: originalData.deskripsi,
imagesId: originalData.imagesId,
});
setPreviewImage(originalData.imageUrl || null);
setFile(null);
toast.info("Form dikembalikan ke data awal");
};
return (
<Box px={{ base: "sm", md: "lg" }} py="md">
{/* Header */}
<Group mb="md">
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors["blue-button"]} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">
Edit Foto
</Title>
</Group>
{/* 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">
<TextInput
label="Judul Foto"
placeholder="Masukkan judul foto"
value={formData.name}
onChange={(e) => handleChange("name", e.target.value)}
required
/>
{/* Upload Gambar */}
<Box>
<Text fw="bold" fz="sm" mb={6}>
Gambar Foto
</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 .png, .jpg, .jpeg, webp
</Text>
</Stack>
</Group>
</Dropzone>
{previewImage && (
<Box mt="sm" pos="relative" style={{ textAlign: 'center' }}>
<Image
src={previewImage}
alt="Preview Gambar"
radius="md"
style={{
maxHeight: 220,
objectFit: "contain",
border: `1px solid ${colors["blue-button"]}`,
}}
loading="lazy"
/>
<ActionIcon
variant="filled"
color="red"
radius="xl"
size="sm"
pos="absolute"
top={5}
right={5}
onClick={() => {
setPreviewImage(null);
setFile(null);
}}
style={{
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
}}
>
<IconX size={14} />
</ActionIcon>
</Box>
)}
</Box>
{/* Deskripsi */}
<Box>
<Text fz="sm" fw="bold">
Deskripsi Foto
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) =>
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }))
}
/>
</Box>
{/* Action */}
<Group justify="right">
{/* Tombol Batal */}
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<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)',
}}
>
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditFoto;

View File

@@ -0,0 +1,175 @@
'use client';
import { Box, Button, Group, Paper, Skeleton, Stack, Text, Alert } from '@mantine/core';
import Image from 'next/image';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconTrash, IconPhoto } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import colors from '@/con/colors';
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
function DetailFoto() {
const FotoState = useProxy(stateGallery.foto);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const [imageError, setImageError] = useState(false);
const params = useParams();
const router = useRouter();
useShallowEffect(() => {
FotoState.findUnique.load(params?.id as string);
}, []);
const handleHapus = () => {
if (selectedId) {
FotoState.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
router.push("/admin/desa/gallery/foto");
}
};
if (!FotoState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton height={500} radius="md" />
</Stack>
);
}
const data = FotoState.findUnique.data;
const imageUrl = data.imageGalleryFoto?.link;
return (
<Box py={10}>
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15}
>
Kembali
</Button>
<Paper
withBorder
// Gunakan max-width agar tidak terlalu lebar di desktop
maw={800}
w="100%"
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz={{ base: 'xl', md: '2xl' }} fw="bold" c={colors['blue-button']}>
Detail Foto
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz="lg" fw="bold">Judul Foto</Text>
<Text fz="md" c="dimmed">{data.name || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Deskripsi</Text>
<Text
fz="md"
c="dimmed"
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
/>
</Box>
<Box>
<Text fz="lg" fw="bold">Gambar</Text>
{imageUrl ? (
<Box
pos="relative"
style={{
width: '100%',
maxWidth: '600px', // Set a maximum width
margin: '0 auto', // Center the container
aspectRatio: '16/9', // Use 16:9 aspect ratio
borderRadius: 8,
overflow: 'hidden',
position: 'relative'
}}
>
<Image
src={imageUrl}
alt={data.name || 'Gambar Foto'}
fill
style={{
objectFit: 'contain', // Changed from 'cover' to 'contain' to show full image
width: '100%',
height: '100%',
position: 'absolute',
top: 0,
left: 0
}}
loading="lazy"
onError={() => setImageError(true)}
/>
</Box>
) : imageError ? (
<Alert
color="orange"
icon={<IconPhoto size={16} />}
title="Gagal memuat gambar"
radius="md"
>
Gambar tidak dapat ditampilkan.
</Alert>
) : (
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
)}
</Box>
{/* Action Buttons */}
<Group gap="sm" justify="flex-start">
<Button
color="red"
onClick={() => {
setSelectedId(data.id);
setModalHapus(true);
}}
variant="light"
radius="md"
size="md"
>
<IconTrash size={20} />
</Button>
<Button
color="green"
onClick={() => router.push(`/admin/desa/gallery/foto/${data.id}/edit`)}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Group>
</Stack>
</Paper>
</Stack>
</Paper>
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah Anda yakin ingin menghapus foto ini?"
/>
</Box>
);
}
export default DetailFoto;

View File

@@ -0,0 +1,228 @@
'use client';
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import {
ActionIcon,
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Loader,
Image
} from '@mantine/core';
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 CreateFoto() {
const FotoState = useProxy(stateGallery.foto);
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const resetForm = () => {
FotoState.create.form = {
name: '',
deskripsi: '',
imagesId: '',
};
setPreviewImage(null);
setFile(null);
};
const handleSubmit = async () => {
try {
setIsSubmitting(true);
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');
}
FotoState.create.form.imagesId = uploaded.id;
await FotoState.create.create();
resetForm();
router.push('/admin/desa/gallery/foto');
} catch (error) {
console.error('Error creating foto:', error);
toast.error('Terjadi kesalahan saat membuat foto');
} finally {
setIsSubmitting(false);
}
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header Back Button + Title */}
<Group mb="md">
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">
Tambah Foto
</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
label="Judul Foto"
placeholder="Masukkan judul Foto"
value={FotoState.create.form.name}
onChange={(e) => {
FotoState.create.form.name = e.currentTarget.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/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
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" pos="relative" style={{ textAlign: 'center' }}>
<Image
src={previewImage}
alt="Preview Gambar"
radius="md"
style={{
maxHeight: 200,
objectFit: 'contain',
border: '1px solid #ddd',
}}
loading="lazy"
/>
{/* Tombol hapus (pojok kanan atas) */}
<ActionIcon
variant="filled"
color="red"
radius="xl"
size="sm"
pos="absolute"
top={5}
right={5}
onClick={() => {
setPreviewImage(null);
setFile(null);
}}
style={{
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
}}
>
<IconX size={14} />
</ActionIcon>
</Box>
)}
</Box>
{/* Deskripsi */}
<Box>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi Foto
</Text>
<CreateEditor
value={FotoState.create.form.deskripsi}
onChange={(val) => {
FotoState.create.form.deskripsi = val;
}}
/>
</Box>
{/* Button Submit */}
<Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={resetForm}
>
Reset
</Button>
{/* Tombol Simpan */}
<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)',
}}
>
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateFoto;

View File

@@ -1,157 +1,163 @@
"use client";
import stateFileStorage from "@/state/state-list-image";
'use client'
import colors from '@/con/colors';
import {
ActionIcon,
Box,
Card,
Flex,
Button,
Center,
Group,
Image,
Pagination,
Paper,
SimpleGrid,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
TextInput,
Title
} from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks";
import { IconSearch, IconTrash, IconX } from "@tabler/icons-react";
import { motion } from "framer-motion";
import toast from "react-simple-toasts";
import { useSnapshot } from "valtio";
export default function ListImage() {
const { list, total } = useSnapshot(stateFileStorage);
useShallowEffect(() => {
stateFileStorage.load();
}, []);
let timeOut: NodeJS.Timer;
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, 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 stateGallery from '../../../_state/desa/gallery';
function Foto() {
const [search, setSearch] = useState("");
return (
<Stack p="lg" gap="lg">
<Flex justify="space-between" align="center" wrap="wrap" gap="md">
<Title order={2} fw={700}>
Galeri Foto
</Title>
<TextInput
radius="xl"
size="md"
placeholder="Cari foto berdasarkan nama..."
leftSection={<IconSearch size={18} />}
rightSection={
<ActionIcon
variant="light"
color="gray"
radius="xl"
onClick={() => stateFileStorage.load()}
>
<IconX size={18} />
</ActionIcon>
}
onChange={(e) => {
if (timeOut) clearTimeout(timeOut);
timeOut = setTimeout(() => {
stateFileStorage.load({ search: e.target.value });
}, 300);
}}
/>
</Flex>
<Paper withBorder radius="lg" p="md" shadow="sm">
{list && list.length > 0 ? (
<SimpleGrid
cols={{ base: 2, sm: 3, md: 5, lg: 8 }}
spacing="md"
verticalSpacing="md"
>
{list.map((v, k) => (
<Card
key={k}
withBorder
radius="md"
shadow="sm"
className="hover:shadow-md transition-all duration-200"
>
<Stack gap="xs">
<motion.div
onClick={() => {
navigator.clipboard.writeText(v.url);
toast("Tautan foto berhasil disalin");
}}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
style={{ cursor: "pointer" }}
>
<Image
src={`${v.url}?size=200`}
alt={v.name}
radius="md"
h={120}
fit="cover"
loading="lazy"
/>
</motion.div>
<Box>
<Text size="sm" fw={500} lineClamp={2}>
{v.name}
</Text>
</Box>
<Group justify="space-between" align="center" pt="xs">
<ActionIcon
variant="subtle"
color="red"
radius="md"
onClick={() => {
stateFileStorage
.del({ id: v.id })
.finally(() => toast("Foto berhasil dihapus"));
}}
>
<IconTrash size={18} />
</ActionIcon>
</Group>
</Stack>
</Card>
))}
</SimpleGrid>
) : (
<Stack align="center" justify="center" py="xl" gap="sm">
<Image
src="https://cdn-icons-png.flaticon.com/512/4076/4076549.png"
alt="Kosong"
w={120}
h={120}
fit="contain"
opacity={0.7}
loading="lazy"
/>
<Text c="dimmed" ta="center">
Belum ada foto yang tersedia
</Text>
</Stack>
)}
</Paper>
{total && total > 1 && (
<Flex justify="center">
<Pagination
total={total}
value={stateFileStorage.page} // Changed from page to value
size="md"
radius="md"
withEdges
onChange={(page) => {
stateFileStorage.load({ page });
}}
/>
</Flex>
)}
</Stack>
<Box>
<HeaderSearch
title='Foto'
placeholder='Cari judul atau deskripsi foto...'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListFoto search={search} />
</Box>
);
}
function ListFoto({ search }: { search: string }) {
const FotoState = useProxy(stateGallery.foto)
const router = useRouter();
const {
data,
page,
totalPages,
loading,
load,
} = FotoState.findMany;
useShallowEffect(() => {
load(page, 10, search)
}, [page, search])
const filteredData = data || []
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={600} radius="md" />
</Stack>
)
}
return (
<Box py={10}>
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Foto</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/desa/gallery/foto/create')}
>
Tambah Baru
</Button>
</Group>
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '25%' }}>Judul Foto</TableTh>
<TableTh style={{ width: '20%' }}>Tanggal</TableTh>
<TableTh style={{ width: '30%' }}>Deskripsi</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd style={{ width: '25%' }}>
<Box w={200}>
<Text fw={500} truncate="end" lineClamp={1}>{item.name}</Text>
</Box>
</TableTd>
<TableTd style={{ width: '20%' }}>
<Box w={200}>
<Text fz="sm" c="dimmed">
{new Date(item.createdAt).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'long',
year: 'numeric',
})}
</Text>
</Box>
</TableTd>
<TableTd style={{ width: '30%' }}>
<Box w={200}>
<Text fz="sm" truncate="end" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</Box>
</TableTd>
<TableTd style={{ width: '15%' }}>
<Button
variant="light"
color="blue"
onClick={() => router.push(`/admin/desa/gallery/foto/${item.id}`)}
>
<IconDeviceImac size={20} />
<Text ml={5}>Detail</Text>
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text c="dimmed">Tidak ada foto yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10)
window.scrollTo({ top: 0, behavior: 'smooth' })
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
</Box>
);
}
export default Foto;

View File

@@ -11,21 +11,21 @@ function LayoutTabsDetail({ children }: { children: React.ReactNode }) {
const pathname = usePathname()
const tabs = [
{
label: "Profile Desa",
value: "profiledesa",
href: "/admin/desa/profile/profile-desa",
label: "Profil Desa",
value: "profildesa",
href: "/admin/desa/profil/profil-desa",
icon: <IconUser size={18} stroke={1.8} />
},
{
label: "Profile Perbekel",
value: "profileperbekel",
href: "/admin/desa/profile/profile-perbekel",
label: "Profil Perbekel",
value: "profilperbekel",
href: "/admin/desa/profil/profil-perbekel",
icon: <IconUsers size={18} stroke={1.8} />
},
{
label: "Profile Perbekel Dari Masa Ke Masa",
value: "profile-perbekel-dari-masa-ke-masa",
href: "/admin/desa/profile/profile-perbekel-dari-masa-ke-masa",
label: "Profil Perbekel Dari Masa Ke Masa",
value: "profilperbekeldarimasakemasa",
href: "/admin/desa/profil/profil-perbekel-dari-masa-ke-masa",
icon: <IconCalendar size={18} stroke={1.8} />
}
];

View File

@@ -12,22 +12,22 @@ function LayoutTabsEdit({ children }: { children: React.ReactNode }) {
{
label: "Sejarah Desa",
value: "sejarahdesa",
href: "/admin/desa/profile/edit/sejarah_desa"
href: "/admin/desa/profil/edit/sejarah_desa"
},
{
label: "Visi Misi Desa",
value: "visimisidesa",
href: "/admin/desa/profile/edit/visi_misi_desa"
href: "/admin/desa/profil/edit/visi_misi_desa"
},
{
label: "Lambang Desa",
value: "lambangdesa",
href: "/admin/desa/profile/edit/lambang_desa"
href: "/admin/desa/profil/edit/lambang_desa"
},
{
label: "Maskot Desa",
value: "maskotdesa",
href: "/admin/desa/profile/edit/maskot_desa"
href: "/admin/desa/profil/edit/maskot_desa"
},
];
const curentTab = tabs.find(tab => tab.href === pathname)

View File

@@ -43,7 +43,7 @@ function Page() {
const id = params?.id as string;
if (!id) {
toast.error('ID tidak valid');
router.push('/admin/desa/profile/profile-desa');
router.push('/admin/desa/profil/profil-desa');
return;
}
@@ -106,7 +106,7 @@ function Page() {
if (success) {
toast.success('Data berhasil disimpan');
router.push('/admin/desa/profile/profile-desa');
router.push('/admin/desa/profil/profil-desa');
} else {
toast.error('Gagal menyimpan data');
}
@@ -156,7 +156,7 @@ function Page() {
<Alert icon={<IconAlertCircle size={20} />} color="red" title="Terjadi Kesalahan" radius="md">
{loadError}
</Alert>
<Button onClick={() => router.push('/admin/desa/profile/profile-desa')} variant="outline">
<Button onClick={() => router.push('/admin/desa/profil/profil-desa')} variant="outline">
Kembali ke Halaman Utama
</Button>
</Stack>

View File

@@ -40,7 +40,7 @@ function Page() {
const id = params?.id as string;
if (!id) {
toast.error("ID tidak valid");
router.push("/admin/desa/profile/profile-desa");
router.push("/admin/desa/profil/profil-desa");
return;
}
@@ -157,7 +157,7 @@ function Page() {
if (success) {
toast.success("Maskot berhasil diperbarui!");
router.push("/admin/desa/profile/profile-desa");
router.push("/admin/desa/profil/profil-desa");
}
} catch (error) {
console.error("Error update maskot:", error);

View File

@@ -50,7 +50,7 @@ function Page() {
const id = params?.id as string;
if (!id) {
toast.error('ID tidak valid');
router.push('/admin/desa/profile/profile-desa');
router.push('/admin/desa/profil/profil-desa');
return;
}
@@ -122,7 +122,7 @@ function Page() {
if (success) {
toast.success('Data berhasil disimpan');
router.push('/admin/desa/profile/profile-desa');
router.push('/admin/desa/profil/profil-desa');
} else {
toast.error('Gagal menyimpan data');
}
@@ -179,7 +179,7 @@ function Page() {
{loadError}
</Alert>
<Button
onClick={() => router.push('/admin/desa/profile/profile-desa')}
onClick={() => router.push('/admin/desa/profil/profil-desa')}
variant="outline"
>
Kembali ke Halaman Utama

View File

@@ -42,7 +42,7 @@ function Page() {
const id = params?.id as string;
if (!id) {
toast.error('ID tidak valid');
router.push('/admin/desa/profile/profile-desa');
router.push('/admin/desa/profil/profil-desa');
return;
}
@@ -106,7 +106,7 @@ function Page() {
if (success) {
toast.success('Data berhasil disimpan');
router.push('/admin/desa/profile/profile-desa');
router.push('/admin/desa/profil/profil-desa');
} else {
toast.error('Gagal menyimpan data');
}
@@ -156,7 +156,7 @@ function Page() {
{loadError}
</Alert>
<Button
onClick={() => router.push('/admin/desa/profile/profile-desa')}
onClick={() => router.push('/admin/desa/profil/profil-desa')}
variant="outline"
>
Kembali ke Halaman Utama

View File

@@ -27,7 +27,7 @@ function Page() {
return (
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
<Stack gap="lg">
<Title order={2} c={colors['blue-button']}>Preview Profile Desa</Title>
<Title order={2} c={colors['blue-button']}>Preview Profil Desa</Title>
{/* Sejarah Desa */}
{sejarah && (
@@ -42,7 +42,7 @@ function Page() {
variant="light"
leftSection={<IconEdit size={18} stroke={2} />}
radius="md"
onClick={() => router.push(`/admin/desa/profile/profile-desa/${sejarah.id}/sejarah_desa`)}
onClick={() => router.push(`/admin/desa/profil/profil-desa/${sejarah.id}/sejarah_desa`)}
>
Edit
</Button>
@@ -87,7 +87,7 @@ function Page() {
variant="light"
leftSection={<IconEdit size={18} stroke={2} />}
radius="md"
onClick={() => router.push(`/admin/desa/profile/profile-desa/${visiMisi.id}/visi_misi_desa`)}
onClick={() => router.push(`/admin/desa/profil/profil-desa/${visiMisi.id}/visi_misi_desa`)}
>
Edit
</Button>
@@ -135,7 +135,7 @@ function Page() {
variant="light"
leftSection={<IconEdit size={18} stroke={2} />}
radius="md"
onClick={() => router.push(`/admin/desa/profile/profile-desa/${lambang.id}/lambang_desa`)}
onClick={() => router.push(`/admin/desa/profil/profil-desa/${lambang.id}/lambang_desa`)}
>
Edit
</Button>
@@ -180,7 +180,7 @@ function Page() {
variant="light"
leftSection={<IconEdit size={18} stroke={2} />}
radius="md"
onClick={() => router.push(`/admin/desa/profile/profile-desa/${maskot.id}/maskot_desa`)}
onClick={() => router.push(`/admin/desa/profil/profil-desa/${maskot.id}/maskot_desa`)}
>
Edit
</Button>

View File

@@ -117,7 +117,7 @@ function EditPerbekelDariMasaKeMasa() {
await state.update.update();
toast.success('Perbekel dari masa ke masa berhasil diperbarui!');
router.push('/admin/desa/profile/profile-perbekel-dari-masa-ke-masa');
router.push('/admin/desa/profil/profil-perbekel-dari-masa-ke-masa');
} catch (error) {
console.error('Error updating perbekel dari masa ke masa:', error);
toast.error('Terjadi kesalahan saat memperbarui perbekel dari masa ke masa');

View File

@@ -25,7 +25,7 @@ function DetailPerbekelDariMasa() {
state.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
router.push("/admin/desa/profile/profile-perbekel-dari-masa-ke-masa");
router.push("/admin/desa/profil/profil-perbekel-dari-masa-ke-masa");
}
};
@@ -113,7 +113,7 @@ function DetailPerbekelDariMasa() {
<Button
color="green"
onClick={() => router.push(`/admin/desa/profile/profile-perbekel-dari-masa-ke-masa/${data.id}/edit`)}
onClick={() => router.push(`/admin/desa/profil/profil-perbekel-dari-masa-ke-masa/${data.id}/edit`)}
variant="light"
radius="md"
size="md"

View File

@@ -46,7 +46,7 @@ function CreatePerbekelDariMasaKeMasa() {
state.create.form.imageId = uploaded.id;
await state.create.create();
resetForm();
router.push('/admin/desa/profile/profile-perbekel-dari-masa-ke-masa');
router.push('/admin/desa/profil/profil-perbekel-dari-masa-ke-masa');
} catch (error) {
console.error(error);
toast.error('Gagal menambahkan perbekel dari masa ke masa');

View File

@@ -53,7 +53,7 @@ function ListPerbekelDariMasaKeMasa({ search }: { search: string }) {
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/desa/profile/profile-perbekel-dari-masa-ke-masa/create')}
onClick={() => router.push('/admin/desa/profil/profil-perbekel-dari-masa-ke-masa/create')}
>
Tambah Baru
</Button>
@@ -90,7 +90,7 @@ function ListPerbekelDariMasaKeMasa({ search }: { search: string }) {
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() => router.push(`/admin/desa/profile/profile-perbekel-dari-masa-ke-masa/${item.id}`)}
onClick={() => router.push(`/admin/desa/profil/profil-perbekel-dari-masa-ke-masa/${item.id}`)}
>
Detail
</Button>

View File

@@ -25,7 +25,7 @@ function ProfilePerbekel() {
const id = params?.id as string;
if (!id) {
toast.error("ID tidak valid");
router.push("/admin/desa/profile/profile-perbekel");
router.push("/admin/desa/profil/profil-perbekel");
return;
}
@@ -74,7 +74,7 @@ function ProfilePerbekel() {
const success = await perbekelState.edit.submit()
if (success) {
toast.success("Data berhasil disimpan");
router.push("/admin/desa/profile/profile-perbekel");
router.push("/admin/desa/profil/profil-perbekel");
}
} catch (error) {
console.error("Error update sejarah desa:", error);

View File

@@ -41,7 +41,7 @@ function Page() {
variant="light"
leftSection={<IconEdit size={18} stroke={2} />}
radius="md"
onClick={() => router.push(`/admin/desa/profile/profile-perbekel/${perbekel.id}`)}
onClick={() => router.push(`/admin/desa/profil/profil-perbekel/${perbekel.id}`)}
>
Edit
</Button>

View File

@@ -20,9 +20,9 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
icon: <IconActivity size={18} stroke={1.8} />
},
{
label: "Grafik Hasil Kepuasan Masyarakat",
value: "grafikhasilkepuasan",
href: "/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan",
label: "Penderita Penyakit",
value: "penderitapenyakit",
href: "/admin/kesehatan/data-kesehatan-warga/penderita_penyakit",
icon: <IconGauge size={18} stroke={1.8} />
},
{

View File

@@ -70,8 +70,8 @@ function EditGrafikHasilKepuasan() {
});
}
} catch (err) {
console.error("Error loading grafik hasil kepuasan:", err);
toast.error("Gagal memuat data grafik hasil kepuasan");
console.error("Error loading penderita penyakit:", err);
toast.error("Gagal memuat data penderita penyakit");
}
};
@@ -99,11 +99,11 @@ function EditGrafikHasilKepuasan() {
setIsSubmitting(true);
editState.update.form = { ...editState.update.form, ...formData };
await editState.update.submit();
toast.success('Grafik hasil kepuasan berhasil diperbarui!');
router.push('/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan');
toast.success('penderita penyakit berhasil diperbarui!');
router.push('/admin/kesehatan/data-kesehatan-warga/penderita_penyakit');
} catch (err) {
console.error('Error updating grafik hasil kepuasan:', err);
toast.error('Terjadi kesalahan saat memperbarui grafik hasil kepuasan');
console.error('Error updating penderita penyakit:', err);
toast.error('Terjadi kesalahan saat memperbarui penderita penyakit');
} finally {
setIsSubmitting(false);
}
@@ -122,7 +122,7 @@ function EditGrafikHasilKepuasan() {
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">
Edit Grafik Hasil Kepuasan
Edit Penderita Penyakit
</Title>
</Group>

View File

@@ -26,7 +26,7 @@ function DetailGrafikHasilKepuasan() {
state.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
router.push("/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan");
router.push("/admin/kesehatan/data-kesehatan-warga/penderita_penyakit");
}
};
@@ -63,7 +63,7 @@ function DetailGrafikHasilKepuasan() {
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Data Grafik Hasil Kepuasan
Detail Data Penderita Penyakit
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
@@ -118,7 +118,7 @@ function DetailGrafikHasilKepuasan() {
color="green"
onClick={() =>
router.push(
`/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/${data.id}/edit`
`/admin/kesehatan/data-kesehatan-warga/penderita_penyakit/${data.id}/edit`
)
}
variant="light"

View File

@@ -40,7 +40,7 @@ function CreateGrafikHasilKepuasanMasyarakat() {
setIsSubmitting(true);
await stateGrafikKepuasan.create.create();
resetForm();
router.push("/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan");
router.push("/admin/kesehatan/data-kesehatan-warga/penderita_penyakit");
} catch (error) {
console.error("Error creating grafik kepuasan:", error);
toast.error("Terjadi kesalahan saat membuat grafik kepuasan");
@@ -62,7 +62,7 @@ function CreateGrafikHasilKepuasanMasyarakat() {
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">
Tambah Grafik Hasil Kepuasan Masyarakat
Tambah Penderita Penyakit
</Title>
</Group>

View File

@@ -36,7 +36,7 @@ function GrafikHasilKepuasanMasyarakat() {
<Box>
{/* Header Search */}
<HeaderSearch
title='Grafik Hasil Kepuasan Masyarakat'
title='Penderita Penyakit'
placeholder='Cari nama atau alamat...'
searchIcon={<IconSearch size={20} />}
value={search}
@@ -115,14 +115,14 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
{/* Judul + Tombol Tambah */}
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Grafik Hasil Kepuasan Masyarakat</Title>
<Title order={4}>Daftar Penderita Penyakit</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push(
'/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/create'
'/admin/kesehatan/data-kesehatan-warga/penderita_penyakit/create'
)
}
>
@@ -176,7 +176,7 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
color="blue"
onClick={() =>
router.push(
`/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/${item.id}`
`/admin/kesehatan/data-kesehatan-warga/penderita_penyakit/${item.id}`
)
}
>
@@ -221,7 +221,7 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
{/* Chart */}
<Box mt="lg" style={{ width: '100%', minWidth: 300, height: 420, minHeight: 300 }}>
<Paper withBorder bg={colors['white-1']} p={'md'}>
<Title pb={10} order={4}>Grafik Hasil Kepuasan Masyarakat</Title>
<Title pb={10} order={4}>Penderita Penyakit</Title>
{mounted && diseaseChartData.length > 0 ? (
<Center>
<BarChart

View File

@@ -30,12 +30,13 @@ function Page() {
return (
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
<Stack gap="md">
<Grid align="center">
<Grid>
<GridCol span={{ base: 12, md: 11 }}>
<Title order={3} c={colors['blue-button']}>Preview Profil PPID</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button
w={{base: '100%', md: "110%"}}
c="green"
variant="light"
leftSection={<IconEdit size={18} stroke={2} />}

View File

@@ -91,8 +91,8 @@ export const devBar = [
children: [
{
id: "Desa_1",
name: "Profile",
path: "/admin/desa/profile/profile-desa"
name: "Profil",
path: "/admin/desa/profil/profil-desa"
},
{
id: "Desa_2",
@@ -495,8 +495,8 @@ export const navBar = [
children: [
{
id: "Desa_1",
name: "Profile",
path: "/admin/desa/profile/profile-desa"
name: "Profil",
path: "/admin/desa/profil/profil-desa"
},
{
id: "Desa_2",
@@ -899,8 +899,8 @@ export const role1 = [
children: [
{
id: "Desa_1",
name: "Profile",
path: "/admin/desa/profile/profile-desa"
name: "Profil",
path: "/admin/desa/profil/profil-desa"
},
{
id: "Desa_2",

View File

@@ -6,33 +6,24 @@ import path from "path";
const beritaDelete = async (context: Context) => {
const id = context.params?.id as string;
if (!id) {
return {
status: 400,
body: "ID tidak diberikan",
};
}
if (!id) return { status: 400, body: "ID tidak diberikan" };
const berita = await prisma.berita.findUnique({
where: { id },
include: {
image: true,
kategoriBerita: true, // pastikan relasi image sudah ada di prisma schema
},
include: { image: true, kategoriBerita: true },
});
if (!berita) {
return {
status: 404,
body: "Berita tidak ditemukan",
};
}
if (!berita) return { status: 404, body: "Berita tidak ditemukan" };
// Hapus file gambar dari filesystem jika ada
// 1. HAPUS BERITA DULU
await prisma.berita.delete({ where: { id } });
// 2. BARU HAPUS FILE
if (berita.image) {
try {
const filePath = path.join(berita.image.path, berita.image.name);
await fs.unlink(filePath);
await prisma.fileStorage.delete({
where: { id: berita.image.id },
});
@@ -41,15 +32,11 @@ const beritaDelete = async (context: Context) => {
}
}
// Hapus berita dari DB
await prisma.berita.delete({
where: { id },
});
return {
success: true,
message: "Berita dan file terkait berhasil dihapus",
};
};
export default beritaDelete;

View File

@@ -1,6 +1,5 @@
import Elysia from "elysia";
import DaftarInformasiPublik from "./daftar_informasi_publik";
import GrafikHasilKepuasanMasyarakat from "./ikm/grafik_hasil_kepuasan_masyarakat";
import GrafikBerdasarkanJenisKelamin from "./ikm/grafik_berdasarkan_jenis_kelamin";
import GrafikBerdasarkanResponden from "./ikm/grafik_responden";
import GrafikBerdasarkanUmur from "./ikm/grafik_berdasarkan_umur";
@@ -10,6 +9,7 @@ import ProfilePPID from "./profile_ppid";
import VisiMisiPPID from "./visi_misi_ppid/visi_misi_ppid";
import DasarHukumPPID from "./dasar_hukum";
import StrukturPPID from "./struktur_ppid";
import GrafikHasilKepuasanMasyarakat from "./ikm/grafik_hasil_kepuasan_masyarakat";

View File

@@ -3,39 +3,55 @@ import { Prisma } from "@prisma/client";
import { Context } from "elysia";
type FormCreate = Prisma.PermohonanInformasiPublikGetPayload<{
select: {
name: true;
nik: true;
email: true;
notelp: true;
alamat: true;
jenisInformasiDimintaId: true;
caraMemperolehInformasiId: true;
caraMemperolehSalinanInformasiId: true;
}
}>
export default async function permohonanInformasiPublikCreate(context: Context) {
const body = context.body as FormCreate;
await prisma.permohonanInformasiPublik.create({
data: {
name: body.name,
nik: body.nik,
email: body.email,
notelp: body.notelp,
alamat: body.alamat,
jenisInformasiDimintaId: body.jenisInformasiDimintaId,
caraMemperolehInformasiId: body.caraMemperolehInformasiId,
caraMemperolehSalinanInformasiId: body.caraMemperolehSalinanInformasiId,
}
})
select: {
name: true;
nik: true;
email: true;
notelp: true;
alamat: true;
jenisInformasiDimintaId: true;
caraMemperolehInformasiId: true;
caraMemperolehSalinanInformasiId: true;
};
}>;
export default async function permohonanInformasiPublikCreate(context: Context) {
const body = context.body as FormCreate;
// ========== VALIDASI NIK ==========
if (body.nik && body.nik.length > 16) {
return {
success: true,
message: "Permohonan Informasi Publik Berhasil Dibuat",
data: {
...body,
}
}
success: false,
status: 400,
message: "Maksimal NIK adalah 16 angka",
};
}
// ========== VALIDASI NOMOR TELEPON ==========
if (body.notelp && body.notelp.length > 15) {
return {
success: false,
status: 400,
message: "Maksimal nomor telepon adalah 15 angka",
};
}
await prisma.permohonanInformasiPublik.create({
data: {
name: body.name,
nik: body.nik,
email: body.email,
notelp: body.notelp,
alamat: body.alamat,
jenisInformasiDimintaId: body.jenisInformasiDimintaId,
caraMemperolehInformasiId: body.caraMemperolehInformasiId,
caraMemperolehSalinanInformasiId: body.caraMemperolehSalinanInformasiId,
},
});
return {
success: true,
message: "Permohonan Informasi Publik Berhasil Dibuat",
data: { ...body },
};
}

View File

@@ -3,31 +3,42 @@ import { Prisma } from "@prisma/client";
import { Context } from "elysia";
type FormCreate = Prisma.FormulirPermohonanKeberatanGetPayload<{
select: {
name: true;
email: true;
notelp: true;
alasan: true;
}
}>
select: {
name: true;
email: true;
notelp: true;
alasan: true;
};
}>;
export default async function permohonanKeberatanInformasiPublikCreate(context: Context) {
const body = context.body as FormCreate;
await prisma.formulirPermohonanKeberatan.create({
data: {
name: body.name,
email: body.email,
notelp: body.notelp,
alasan: body.alasan,
}
})
export default async function permohonanKeberatanInformasiPublikCreate(
context: Context
) {
const body = context.body as FormCreate;
// ========== VALIDASI NOMOR TELEPON ==========
if (body.notelp && body.notelp.length > 15) {
return {
success: true,
message: "Permohonan Keberatan Informasi Publik Berhasil Dibuat",
data: {
...body,
}
}
}
success: false,
status: 400,
message: "Maksimal nomor telepon adalah 15 angka",
};
}
await prisma.formulirPermohonanKeberatan.create({
data: {
name: body.name,
email: body.email,
notelp: body.notelp,
alasan: body.alasan,
},
});
return {
success: true,
message: "Permohonan Keberatan Informasi Publik Berhasil Dibuat",
data: {
...body,
},
};
}

View File

@@ -0,0 +1,43 @@
// app/api/news/latest/route.ts
import { NextResponse } from "next/server";
import prisma from "@/lib/prisma";
export async function GET() {
try {
const berita = await prisma.berita.findMany({
take: 3,
orderBy: { createdAt: "desc" },
include: { kategoriBerita: true },
});
const pengumuman = await prisma.pengumuman.findMany({
take: 3,
orderBy: { createdAt: "desc" },
include: { CategoryPengumuman: true },
});
const news = [
...berita.map((b) => ({
id: b.id,
type: "berita" as const,
title: b.judul,
content: b.content,
timestamp: b.createdAt,
kategoriBerita: b.kategoriBerita || undefined,
})),
...pengumuman.map((p) => ({
id: p.id,
type: "pengumuman" as const,
title: p.judul,
content: p.content,
timestamp: p.createdAt,
kategoriPengumuman: p.CategoryPengumuman || undefined,
})),
];
return NextResponse.json({ success: true, news }); // ✅ ganti 'data' jadi 'news'
} catch (error) {
console.error("API Error:", error);
return NextResponse.json({ success: false, error: "Gagal memuat data" }, { status: 500 });
}
}

View File

@@ -49,7 +49,7 @@ function Page() {
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"} px={{ base: "md", md: 0 }}>
<Stack pos={"relative"} bg={colors.Bg} pb={"xl"} gap={"xs"} px={{ base: "md", md: 0 }}>
<Group px={{ base: "md", md: 100 }}>
<NewsReader />
</Group>

View File

@@ -1,12 +1,44 @@
// app/desa/berita/BeritaLayoutClient.tsx
'use client'
import dynamic from 'next/dynamic';
// app/darmasaba/(pages)/desa/berita/layout.tsx
'use client';
import { usePathname } from 'next/navigation';
import { ReactNode } from 'react';
import dynamic from 'next/dynamic';
import { Box } from '@mantine/core';
import BackButton from '../layanan/_com/BackButto';
import colors from '@/con/colors';
const LayoutTabsBerita = dynamic(
() => import('./_lib/layoutTabs'),
{ ssr: false }
);
export default function BeritaLayoutClient({ children }: { children: React.ReactNode }) {
export default function BeritaLayoutClient({
children,
}: {
children: ReactNode;
}) {
const pathname = usePathname();
// Contoh path:
// - /darmasaba/desa/berita/semua → panjang 5 → list
// - /darmasaba/desa/berita/Pemerintahan → panjang 5 → list
// - /darmasaba/desa/berita/Pemerintahan/123 → panjang 6 → detail
const segments = pathname.split('/').filter(Boolean);
const isDetailPage = segments.length === 5; // [darmasaba, desa, berita, kategori, id]
if (isDetailPage) {
// Tampilkan tanpa tab menu
return (
<Box bg={colors.Bg}>
<Box pt={33} px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
{children}
</Box>
);
}
// Tampilkan dengan tab menu (untuk /semua atau /kategori)
return <LayoutTabsBerita>{children}</LayoutTabsBerita>;
}

View File

@@ -1,150 +0,0 @@
'use client';
import colors from '@/con/colors';
import { Box, Center, Image, Pagination, Paper, SimpleGrid, Stack, Text } from '@mantine/core';
import { useCallback, useEffect, useState } from 'react';
import ApiFetch from '@/lib/api-fetch';
interface FileItem {
id: string;
name: string;
link: string;
realName: string;
createdAt: string | Date;
category: string;
path: string;
mimeType: string;
}
export default function FotoContent() {
const [files, setFiles] = useState<FileItem[]>([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState('');
const [page, setPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const limit = 9; // ✅ ambil 12 data per page
const loadData = useCallback(async (pageNum: number, searchTerm: string) => {
setLoading(true);
try {
const query: Record<string, string> = {
category: 'image',
page: pageNum.toString(),
limit: limit.toString(),
};
if (searchTerm) query.search = searchTerm;
const response = await ApiFetch.api.fileStorage.findMany.get({ query });
if (response.status === 200 && response.data) {
setFiles(response.data.data || []);
setTotalPages(response.data.meta?.totalPages || 1);
} else {
setFiles([]);
}
} catch (err) {
console.error('Load error:', err);
setFiles([]);
} finally {
setLoading(false);
}
}, []);
// ✅ Initial load + update when URL/search changes
useEffect(() => {
const handleRouteChange = () => {
const urlParams = new URLSearchParams(window.location.search);
const urlSearch = urlParams.get('search') || '';
const urlPage = parseInt(urlParams.get('page') || '1');
setSearch(urlSearch);
setPage(urlPage);
loadData(urlPage, urlSearch);
};
const handleSearchUpdate = (e: Event) => {
const { search } = (e as CustomEvent).detail;
setSearch(search);
setPage(1);
loadData(1, search);
};
handleRouteChange();
window.addEventListener('popstate', handleRouteChange);
window.addEventListener('searchUpdate', handleSearchUpdate as EventListener);
return () => {
window.removeEventListener('popstate', handleRouteChange);
window.removeEventListener('searchUpdate', handleSearchUpdate as EventListener);
};
}, [loadData]);
// ✅ Update when page/search changes
useEffect(() => {
loadData(page, search);
}, [page, search, loadData]);
const updateURL = (newSearch: string, newPage: number) => {
const url = new URL(window.location.href);
if (newSearch) url.searchParams.set('search', newSearch);
else url.searchParams.delete('search');
if (newPage > 1) url.searchParams.set('page', newPage.toString());
else url.searchParams.delete('page');
window.history.pushState({}, '', url);
};
const handlePageChange = (newPage: number) => {
setPage(newPage);
updateURL(search, newPage);
};
if (loading && files.length === 0) {
return <Center>Memuat data...</Center>;
}
if (files.length === 0) {
return <Center>Tidak ada foto ditemukan</Center>;
}
return (
<Box pt={20} px={{ base: 'md', md: 100 }}>
<SimpleGrid cols={{ base: 1, md: 3 }}>
{files.map((file) => (
<Paper
key={file.id}
mb={50}
p="md"
radius={26}
bg={colors['white-trans-1']}
style={{ height: '100%' }}
>
<Box style={{ height: '250px', overflow: 'hidden', borderRadius: '12px' }}>
<Image
src={file.link}
alt={file.realName || file.name}
height={250}
width="100%"
style={{ objectFit: 'cover', height: '100%', width: '100%' }}
loading="lazy"
/>
</Box>
<Stack gap="sm" py={10}>
<Text fw="bold" fz={{ base: 'h4', md: 'h3' }}>
{file.realName || file.name}
</Text>
<Text fz="sm" c="dimmed">
{new Date(file.createdAt).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'long',
year: 'numeric',
})}
</Text>
</Stack>
</Paper>
))}
</SimpleGrid>
<Center mt="xl">
<Pagination total={totalPages} value={page} onChange={handlePageChange} />
</Center>
</Box>
);
}

View File

@@ -1,25 +1,168 @@
'use client'
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client';
import dynamic from 'next/dynamic';
import { Suspense } from 'react';
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
import colors from '@/con/colors';
import {
Box,
Center,
Grid,
Image,
Pagination,
Paper,
Skeleton,
Stack,
Text,
Title,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconPhoto } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
// ✅ Load komponen tanpa SSR
const FotoContent = dynamic(
() => import('./Content'),
{
ssr: false,
loading: () => <div>Memuat konten...</div>
}
);
// Komponen kartu foto
function FotoCard({ item }: { item: any }) {
const router = useRouter();
const handleClick = () => {
router.push(`/darmasaba/galeri/foto/${item.id}`);
};
function PageContent() {
return (
<Suspense fallback={<div>Memuat...</div>}>
<FotoContent />
</Suspense>
<Grid.Col span={{ base: 12, xs: 6, md: 4 }}>
<Paper
shadow="sm"
radius="md"
p={0}
onClick={handleClick}
style={{ cursor: 'pointer', transition: 'transform 0.2s' }}
onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.02)')}
onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')}
>
{item.imageGalleryFoto?.link ? (
<Box
pos="relative"
style={{
paddingBottom: '100%', // ✅ Ubah ke 1:1 (square) — atau sesuaikan
overflow: 'hidden',
borderRadius: '4px 4px 0 0',
backgroundColor: '#f9f9f9', // ✅ background netral
}}
>
<Image
radius="lg"
src={item.imageGalleryFoto.link}
alt={item.name || 'Foto Galeri'}
p={10}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
objectFit: 'contain', // ✅ Tampilkan utuh, jangan crop
objectPosition: 'center', // rata tengah
}}
loading="lazy"
/>
</Box>
) : (
<Center h={180} bg="gray.1">
<IconPhoto size={40} color="gray" />
</Center>
)}
<Stack p="md" gap={4}>
<Text fw={600} lineClamp={1}>
{item.name || 'Tanpa Judul'}
</Text>
{item.deskripsi && (
<Text fz="sm" c="dimmed" lineClamp={2} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
)}
<Text fz="xs" c="dimmed">
{new Date(item.createdAt).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'short',
year: 'numeric',
})}
</Text>
</Stack>
</Paper>
</Grid.Col>
);
}
export default function Page() {
return <PageContent />;
// Komponen utama
export default function GaleriFotoUser() {
const [search] = useState('');
return (
<Box py="xl" px={{ base: 'md', md: 'lg' }}>
{/* Header */}
<Title order={2} c={colors['blue-button']} mb="lg">
Galeri Foto Desa Darmasaba
</Title>
{/* Daftar Foto */}
<FotoList search={search} />
</Box>
);
}
function FotoList({ search }: { search: string }) {
const FotoState = useProxy(stateGallery.foto);
const { data, page, totalPages, loading, load } = FotoState.findMany;
useShallowEffect(() => {
load(page, 3, search); // ✅ 9 item per halaman
}, [page, search]);
if (loading) {
return (
<Grid mt="md">
{Array.from({ length: 3 }).map((_, i) => (
<Grid.Col key={i} span={{ base: 12, xs: 6, md: 4 }}>
<Skeleton height={280} radius="md" />
</Grid.Col>
))}
</Grid>
);
}
if (!data || data.length === 0) {
return (
<Center py="xl">
<Stack align="center" c="dimmed">
<IconPhoto size={48} />
<Text>Tidak ada foto ditemukan</Text>
</Stack>
</Center>
);
}
return (
<Stack mt="md" gap="xl">
<Grid>
{data.map((item) => (
<FotoCard key={item.id} item={item} />
))}
</Grid>
{/* Pagination */}
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 3, search);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
color="blue"
radius="md"
/>
</Center>
</Stack>
);
}

View File

@@ -1,9 +1,36 @@
'use client'
import { usePathname } from "next/navigation";
import { ReactNode } from "react";
import LayoutTabsGalery from "./_lib/layoutTabs";
export default function LayoutGalery({ children }: { children: React.ReactNode }) {
return (
<LayoutTabsGalery>
{children}
</LayoutTabsGalery>
)
// export default function LayoutGalery({ children }: { children: React.ReactNode }) {
// return (
// <LayoutTabsGalery>
// {children}
// </LayoutTabsGalery>
// )
// }
export default function BeritaLayoutClient({
children,
}: {
children: ReactNode;
}) {
const pathname = usePathname();
// Contoh path:
// - /darmasaba/desa/berita/semua → panjang 5 → list
// - /darmasaba/desa/berita/Pemerintahan → panjang 5 → list
// - /darmasaba/desa/berita/Pemerintahan/123 → panjang 6 → detail
const segments = pathname.split('/').filter(Boolean);
const isDetailPage = segments.length === 5; // [darmasaba, desa, berita, kategori, id]
if (isDetailPage) {
// Tampilkan tanpa tab menu
return <>{children}</>
}
// Tampilkan dengan tab menu (untuk /semua atau /kategori)
return <LayoutTabsGalery>{children}</LayoutTabsGalery>;
}

View File

@@ -4,26 +4,27 @@ import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
import colors from '@/con/colors';
import {
Box,
Button,
Center,
Group,
Pagination,
Paper,
SimpleGrid,
Spoiler,
Stack,
Text,
Text
} from '@mantine/core';
import { useCallback, useEffect, useState } from 'react';
import { useTransitionRouter } from 'next-view-transitions';
import { useCallback, useEffect } from 'react';
import { useSnapshot } from 'valtio';
export default function VideoContent() {
// ✅ expanded state per index
const [expandedMap, setExpandedMap] = useState<Record<number, boolean>>({});
const videoState = useSnapshot(stateGallery.video);
const router = useTransitionRouter()
const { data, page, totalPages, loading } = videoState.findMany;
// Handle search and pagination changes
const loadData = useCallback((pageNum: number, searchTerm: string) => {
stateGallery.video.findMany.load(pageNum, 10, searchTerm.trim());
stateGallery.video.findMany.load(pageNum, 3, searchTerm.trim());
}, []);
// Initial load and URL change handler
@@ -56,12 +57,6 @@ export default function VideoContent() {
loadData(newPage, search);
};
const toggleExpanded = (index: number, value: boolean) => {
setExpandedMap((prev) => ({
...prev,
[index]: value,
}));
};
const dataVideo = data || [];
@@ -110,27 +105,22 @@ export default function VideoContent() {
<Text fw="bold" fz="sm" lineClamp={1}>
{v.name}
</Text>
<Spoiler
showLabel={
<Text fw="bold" fz="sm" c={colors['blue-button']}>
Show more
</Text>
}
hideLabel={
<Text fw="bold" fz="sm" c={colors['blue-button']}>
Hide details
</Text>
}
expanded={expandedMap[k] || false}
onExpandedChange={(val) => toggleExpanded(k, val)}
<Text
ta="justify"
fz="sm"
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
lineClamp={3}
truncate="end"
/>
<Group justify={"right"}>
<Button
onClick={() => router.push(`/darmasaba/desa/galery/video/${v.id}`)}
bg={colors['blue-button']}
>
<Text
ta="justify"
fz="sm"
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
style={{wordBreak: "break-word", whiteSpace: "normal"}}
/>
</Spoiler>
Detail
</Button>
</Group>
</Stack>
</Box>
</Paper>

View File

@@ -0,0 +1,197 @@
'use client';
import colors from '@/con/colors';
import {
Alert,
Box,
Button,
Card,
Group,
Paper,
Skeleton,
Stack,
Text,
ThemeIcon,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconInfoCircle, IconVideo } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery'; // pastikan state bisa dipakai di publik
import BackButton from '../../../layanan/_com/BackButto';
// Fungsi helper: aman dan tanpa spasi
function convertToEmbedUrl(youtubeUrl: string): string {
try {
const url = new URL(youtubeUrl);
let videoId = '';
if (url.hostname === 'youtu.be') {
videoId = url.pathname.slice(1);
} else if (url.hostname.includes('youtube.com')) {
videoId = url.searchParams.get('v') || '';
}
return videoId ? `https://www.youtube.com/embed/${videoId}` : youtubeUrl;
} catch {
return youtubeUrl;
}
}
export default function DetailVideoUser() {
const params = useParams<{ id: string }>();
const router = useRouter();
const videoState = useProxy(stateGallery.video);
const [videoError, setVideoError] = useState(false);
const id = Array.isArray(params.id) ? params.id[0] : params.id;
useShallowEffect(() => {
if (id) {
videoState.findUnique.load(id);
}
}, [id]);
const data = videoState.findUnique.data;
if (!videoState.findUnique && !id) {
return (
<Box py="xl" px={{ base: 'md', md: 'lg' }}>
<Skeleton height={400} radius="md" />
</Box>
);
}
if (!data) {
return (
<Box py="xl" px={{ base: 'md', md: 'lg' }}>
<Alert
icon={<IconInfoCircle size={20} />}
title="Video tidak ditemukan"
color="red"
radius="md"
>
Video yang Anda cari tidak tersedia.
</Alert>
<Button
leftSection={<IconArrowBack size={16} />}
mt="md"
onClick={() => router.push('/darmasaba/galeri/video')}
variant="outline"
>
Kembali ke Galeri
</Button>
</Box>
);
}
const embedUrl = data.linkVideo ? convertToEmbedUrl(data.linkVideo) : null;
return (
<Box py="xl" px={{ base: 'md', md: 100 }}>
{/* Tombol Kembali */}
<Box >
<BackButton />
</Box>
{/* Header */}
<Text
ta="center"
fz={{ base: 'xl', md: '2xl' }}
fw={700}
c={colors['blue-button']}
mb="lg"
>
{data.name || 'Video Galeri Desa'}
</Text>
{/* Konten Utama */}
<Card
shadow="sm"
radius="md"
p={{ base: 'md', md: 'xl' }}
bg={colors['white-1']}
>
<Stack gap="lg">
{/* Video */}
{embedUrl ? (
<Box
pos="relative"
style={{ paddingBottom: '56.25%', height: 0, overflow: 'hidden' }} // 16:9 aspect ratio
>
<iframe
src={embedUrl}
title={data.name}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
borderRadius: 8,
border: 'none',
}}
onError={() => setVideoError(true)}
/>
</Box>
) : videoError ? (
<Alert
color="orange"
icon={<IconVideo size={20} />}
title="Gagal memuat video"
radius="md"
>
Mohon maaf, video tidak dapat diputar.
</Alert>
) : (
<Alert
color="gray"
icon={<IconInfoCircle size={20} />}
title="Tidak ada video"
radius="md"
>
Konten video belum tersedia.
</Alert>
)}
{/* Informasi Tambahan */}
{data.createdAt && (
<Group gap="xs" justify="center" wrap="nowrap">
<ThemeIcon variant="light" size="sm" radius="xl">
<IconInfoCircle size={14} />
</ThemeIcon>
<Text fz="sm" c="dimmed">
Diunggah pada{' '}
{new Date(data.createdAt).toLocaleDateString('id-ID', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</Text>
</Group>
)}
{/* Deskripsi */}
{data.deskripsi && (
<Paper p="md" bg="gray.0" radius="md">
<Text
fz="md"
c="dark"
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
style={{
wordBreak: 'break-word',
whiteSpace: 'normal',
lineHeight: 1.6,
}}
/>
</Paper>
)}
</Stack>
</Card>
</Box>
);
}

View File

@@ -100,7 +100,7 @@ function Page() {
{data.name}
</Text>
</Container>
<Box px={{ base: "md", md: 100 }}>
<Box px={{ base: "35", md: 100 }}>
<Stack gap="md">
<Text
dangerouslySetInnerHTML={{ __html: data.deskripsi }}

View File

@@ -1,96 +0,0 @@
import colors from '@/con/colors';
import { Stack, Box, Container, Text, Paper, Group } from '@mantine/core';
import React from 'react';
import BackButton from '../../../layanan/_com/BackButto';
import { IconCalendar, IconClock, IconMapPin } from '@tabler/icons-react';
const dataPengumuman = [
{
id: 1,
judul: 'Sosialisasi Perarem Konservasi Adat di Darmasaba',
tanggal: 'Jumat, 26 April 2025',
jam: '16:00 WITA',
lokasi: 'Wantilan Adat Desa',
deskripsi: 'Para krama diundang hadir dalam sosialisasi perarem (aturan adat) baru yang berkaitan dengan konservasi lingkungan berbasis budaya, termasuk larangan alih fungsi lahan pekarangan suci.'
},
{
id: 2,
judul: 'Sosialisasi Perarem Konservasi Adat di Darmasaba',
tanggal: 'Jumat, 26 April 2025',
jam: '16:00 WITA',
lokasi: 'Wantilan Adat Desa',
deskripsi: 'Para krama diundang hadir dalam sosialisasi perarem (aturan adat) baru yang berkaitan dengan konservasi lingkungan berbasis budaya, termasuk larangan alih fungsi lahan pekarangan suci.'
},
{
id: 3,
judul: 'Sosialisasi Perarem Konservasi Adat di Darmasaba',
tanggal: 'Jumat, 26 April 2025',
jam: '16:00 WITA',
lokasi: 'Wantilan Adat Desa',
deskripsi: 'Para krama diundang hadir dalam sosialisasi perarem (aturan adat) baru yang berkaitan dengan konservasi lingkungan berbasis budaya, termasuk larangan alih fungsi lahan pekarangan suci.'
},
{
id: 4,
judul: 'Sosialisasi Perarem Konservasi Adat di Darmasaba',
tanggal: 'Jumat, 26 April 2025',
jam: '16:00 WITA',
lokasi: 'Wantilan Adat Desa',
deskripsi: 'Para krama diundang hadir dalam sosialisasi perarem (aturan adat) baru yang berkaitan dengan konservasi lingkungan berbasis budaya, termasuk larangan alih fungsi lahan pekarangan suci.'
},
{
id: 5,
judul: 'Sosialisasi Perarem Konservasi Adat di Darmasaba',
tanggal: 'Jumat, 26 April 2025',
jam: '16:00 WITA',
lokasi: 'Wantilan Adat Desa',
deskripsi: 'Para krama diundang hadir dalam sosialisasi perarem (aturan adat) baru yang berkaitan dengan konservasi lingkungan berbasis budaya, termasuk larangan alih fungsi lahan pekarangan suci.'
},
]
function Page() {
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="md">
{/* Header */}
<Box px={{ base: "md", md: 100 }}>
<BackButton />
</Box>
<Container size="lg" px="md" >
<Stack align="center" gap="0" >
<Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" ta="center">
Pengumuman Adat & Budaya
</Text>
<Text ta="center" px="md" pb={10}>
Informasi dan pengumuman resmi terkait pengumuman adat & budaya di Desa Darmasaba.
</Text>
</Stack>
</Container>
<Box px={{ base: 'md', md: 100 }}>
{dataPengumuman.map((v, k) => {
return (
<Paper mb={10} key={k} withBorder p="lg" radius="md" shadow="md" bg={colors["white-1"]}>
<Text fz={'h3'}>{v.judul}</Text>
<Group style={{ color: 'black' }} pb={20}>
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">{v.tanggal}</Text>
</Group>
<Group gap="xs">
<IconClock size={18} />
<Text size="sm">{v.jam}</Text>
</Group>
<Group gap="xs">
<IconMapPin size={18} />
<Text size="sm">{v.lokasi}</Text>
</Group>
</Group>
<Text ta={'justify'}>
{v.deskripsi}
</Text>
</Paper>
)
})}
</Box>
</Stack>
);
}
export default Page;

View File

@@ -1,96 +0,0 @@
import colors from '@/con/colors';
import { Stack, Box, Container, Text, Paper, Group } from '@mantine/core';
import React from 'react';
import BackButton from '../../../layanan/_com/BackButto';
import { IconCalendar, IconClock, IconMapPin } from '@tabler/icons-react';
const dataPengumuman = [
{
id: 1,
judul: 'Pelatihan Dasar Komputer untuk Lansia dan Ibu Rumah Tangga',
tanggal: 'Selasa, 30 April 2025',
jam: '09:00 WITA',
lokasi: 'Perpustakaan Desa',
deskripsi: 'Belajar dari nol: cara menyalakan komputer, menulis di Word, membuat email, dan dasar penggunaan HP Android untuk komunikasi dan akses layanan desa online. Terbuka untuk 20 peserta.'
},
{
id: 2,
judul: 'Pelatihan Dasar Komputer untuk Lansia dan Ibu Rumah Tangga',
tanggal: 'Selasa, 30 April 2025',
jam: '09:00 WITA',
lokasi: 'Perpustakaan Desa',
deskripsi: 'Belajar dari nol: cara menyalakan komputer, menulis di Word, membuat email, dan dasar penggunaan HP Android untuk komunikasi dan akses layanan desa online. Terbuka untuk 20 peserta.'
},
{
id: 3,
judul: 'Pelatihan Dasar Komputer untuk Lansia dan Ibu Rumah Tangga',
tanggal: 'Selasa, 30 April 2025',
jam: '09:00 WITA',
lokasi: 'Perpustakaan Desa',
deskripsi: 'Belajar dari nol: cara menyalakan komputer, menulis di Word, membuat email, dan dasar penggunaan HP Android untuk komunikasi dan akses layanan desa online. Terbuka untuk 20 peserta.'
},
{
id: 4,
judul: 'Pelatihan Dasar Komputer untuk Lansia dan Ibu Rumah Tangga',
tanggal: 'Selasa, 30 April 2025',
jam: '09:00 WITA',
lokasi: 'Perpustakaan Desa',
deskripsi: 'Belajar dari nol: cara menyalakan komputer, menulis di Word, membuat email, dan dasar penggunaan HP Android untuk komunikasi dan akses layanan desa online. Terbuka untuk 20 peserta.'
},
{
id: 5,
judul: 'Pelatihan Dasar Komputer untuk Lansia dan Ibu Rumah Tangga',
tanggal: 'Selasa, 30 April 2025',
jam: '09:00 WITA',
lokasi: 'Perpustakaan Desa',
deskripsi: 'Belajar dari nol: cara menyalakan komputer, menulis di Word, membuat email, dan dasar penggunaan HP Android untuk komunikasi dan akses layanan desa online. Terbuka untuk 20 peserta.'
},
]
function Page() {
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="md">
{/* Header */}
<Box px={{ base: "md", md: 100 }}>
<BackButton />
</Box>
<Container size="lg" px="md" >
<Stack align="center" gap="0" >
<Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" ta="center">
Pengumuman Digitalisasi Desa
</Text>
<Text ta="center" px="md" pb={10}>
Informasi dan pengumuman resmi terkait pengumuman digitalisasi desa
</Text>
</Stack>
</Container>
<Box px={{ base: 'md', md: 100 }}>
{dataPengumuman.map((v, k) => {
return (
<Paper mb={10} key={k} withBorder p="lg" radius="md" shadow="md" bg={colors["white-1"]}>
<Text fz={'h3'}>{v.judul}</Text>
<Group style={{ color: 'black' }} pb={20}>
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">{v.tanggal}</Text>
</Group>
<Group gap="xs">
<IconClock size={18} />
<Text size="sm">{v.jam}</Text>
</Group>
<Group gap="xs">
<IconMapPin size={18} />
<Text size="sm">{v.lokasi}</Text>
</Group>
</Group>
<Text ta={'justify'}>
{v.deskripsi}
</Text>
</Paper>
)
})}
</Box>
</Stack>
);
}
export default Page;

View File

@@ -1,96 +0,0 @@
import colors from '@/con/colors';
import { Stack, Box, Container, Text, Paper, Group } from '@mantine/core';
import React from 'react';
import BackButton from '../../../layanan/_com/BackButto';
import { IconCalendar, IconClock, IconMapPin } from '@tabler/icons-react';
const dataPengumuman = [
{
id: 1,
judul: 'Pelatihan Digital Marketing untuk UMKM Desa',
tanggal: 'Rabu, 23 April 2025',
jam: '13:00 WITA',
lokasi: 'Aula Kantor Desa',
deskripsi: 'Para pelaku UMKM diundang untuk mengikuti pelatihan dasar digital marketing: mulai dari membuat akun bisnis, copywriting produk, hingga promosi melalui media sosial. Peserta akan mendapat sertifikat dan materi pelatihan.'
},
{
id: 2,
judul: 'Pelatihan Digital Marketing untuk UMKM Desa',
tanggal: 'Rabu, 23 April 2025',
jam: '13:00 WITA',
lokasi: 'Aula Kantor Desa',
deskripsi: 'Para pelaku UMKM diundang untuk mengikuti pelatihan dasar digital marketing: mulai dari membuat akun bisnis, copywriting produk, hingga promosi melalui media sosial. Peserta akan mendapat sertifikat dan materi pelatihan.'
},
{
id: 3,
judul: 'Pelatihan Digital Marketing untuk UMKM Desa',
tanggal: 'Rabu, 23 April 2025',
jam: '13:00 WITA',
lokasi: 'Aula Kantor Desa',
deskripsi: 'Para pelaku UMKM diundang untuk mengikuti pelatihan dasar digital marketing: mulai dari membuat akun bisnis, copywriting produk, hingga promosi melalui media sosial. Peserta akan mendapat sertifikat dan materi pelatihan.'
},
{
id: 4,
judul: 'Pelatihan Digital Marketing untuk UMKM Desa',
tanggal: 'Rabu, 23 April 2025',
jam: '13:00 WITA',
lokasi: 'Aula Kantor Desa',
deskripsi: 'Para pelaku UMKM diundang untuk mengikuti pelatihan dasar digital marketing: mulai dari membuat akun bisnis, copywriting produk, hingga promosi melalui media sosial. Peserta akan mendapat sertifikat dan materi pelatihan.'
},
{
id: 5,
judul: 'Pelatihan Digital Marketing untuk UMKM Desa',
tanggal: 'Rabu, 23 April 2025',
jam: '13:00 WITA',
lokasi: 'Aula Kantor Desa',
deskripsi: 'Para pelaku UMKM diundang untuk mengikuti pelatihan dasar digital marketing: mulai dari membuat akun bisnis, copywriting produk, hingga promosi melalui media sosial. Peserta akan mendapat sertifikat dan materi pelatihan.'
},
]
function Page() {
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="md">
{/* Header */}
<Box px={{ base: "md", md: 100 }}>
<BackButton />
</Box>
<Container size="lg" px="md" >
<Stack align="center" gap="0" >
<Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" ta="center">
Pengumuman Ekonomi & UMKM
</Text>
<Text ta="center" px="md" pb={10}>
Informasi dan pengumuman resmi terkait pengumuman ekonomi & umkm
</Text>
</Stack>
</Container>
<Box px={{ base: 'md', md: 100 }}>
{dataPengumuman.map((v, k) => {
return (
<Paper mb={10} key={k} withBorder p="lg" radius="md" shadow="md" bg={colors["white-1"]}>
<Text fz={'h3'}>{v.judul}</Text>
<Group style={{ color: 'black' }} pb={20}>
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">{v.tanggal}</Text>
</Group>
<Group gap="xs">
<IconClock size={18} />
<Text size="sm">{v.jam}</Text>
</Group>
<Group gap="xs">
<IconMapPin size={18} />
<Text size="sm">{v.lokasi}</Text>
</Group>
</Group>
<Text ta={'justify'}>
{v.deskripsi}
</Text>
</Paper>
)
})}
</Box>
</Stack>
);
}
export default Page;

View File

@@ -1,96 +0,0 @@
import colors from '@/con/colors';
import { Stack, Box, Container, Text, Paper, Group } from '@mantine/core';
import React from 'react';
import BackButton from '../../../layanan/_com/BackButto';
import { IconCalendar, IconClock, IconMapPin } from '@tabler/icons-react';
const dataPengumuman = [
{
id: 1,
judul: 'Gotong Royong Bersih Sungai dan Drainase',
tanggal: 'Minggu, 21 April 2025',
jam: '06:30 WITA',
lokasi: 'Titik Kumpul: Poskamling RW 02',
deskripsi: 'Seluruh warga RW 02 diimbau ikut serta dalam kegiatan bersih-bersih aliran sungai dan saluran air untuk mencegah banjir saat musim hujan. Disediakan alat kebersihan dan konsumsi ringan.'
},
{
id: 2,
judul: 'Gotong Royong Bersih Sungai dan Drainase',
tanggal: 'Minggu, 21 April 2025',
jam: '06:30 WITA',
lokasi: 'Titik Kumpul: Poskamling RW 02',
deskripsi: 'Seluruh warga RW 02 diimbau ikut serta dalam kegiatan bersih-bersih aliran sungai dan saluran air untuk mencegah banjir saat musim hujan. Disediakan alat kebersihan dan konsumsi ringan.'
},
{
id: 3,
judul: 'Gotong Royong Bersih Sungai dan Drainase',
tanggal: 'Minggu, 21 April 2025',
jam: '06:30 WITA',
lokasi: 'Titik Kumpul: Poskamling RW 02',
deskripsi: 'Seluruh warga RW 02 diimbau ikut serta dalam kegiatan bersih-bersih aliran sungai dan saluran air untuk mencegah banjir saat musim hujan. Disediakan alat kebersihan dan konsumsi ringan.'
},
{
id: 4,
judul: 'Gotong Royong Bersih Sungai dan Drainase',
tanggal: 'Minggu, 21 April 2025',
jam: '06:30 WITA',
lokasi: 'Titik Kumpul: Poskamling RW 02',
deskripsi: 'Seluruh warga RW 02 diimbau ikut serta dalam kegiatan bersih-bersih aliran sungai dan saluran air untuk mencegah banjir saat musim hujan. Disediakan alat kebersihan dan konsumsi ringan.'
},
{
id: 5,
judul: 'Gotong Royong Bersih Sungai dan Drainase',
tanggal: 'Minggu, 21 April 2025',
jam: '06:30 WITA',
lokasi: 'Titik Kumpul: Poskamling RW 02',
deskripsi: 'Seluruh warga RW 02 diimbau ikut serta dalam kegiatan bersih-bersih aliran sungai dan saluran air untuk mencegah banjir saat musim hujan. Disediakan alat kebersihan dan konsumsi ringan.'
},
]
function Page() {
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="md">
{/* Header */}
<Box px={{ base: "md", md: 100 }}>
<BackButton />
</Box>
<Container size="lg" px="md" >
<Stack align="center" gap="0" >
<Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" ta="center">
Pengumuman Lingkungan & Bencana
</Text>
<Text ta="center" px="md" pb={10}>
Informasi dan pengumuman resmi terkait pengumuman lingkungan & bencana
</Text>
</Stack>
</Container>
<Box px={{ base: 'md', md: 100 }}>
{dataPengumuman.map((v, k) => {
return (
<Paper mb={10} key={k} withBorder p="lg" radius="md" shadow="md" bg={colors["white-1"]}>
<Text fz={'h3'}>{v.judul}</Text>
<Group style={{ color: 'black' }} pb={20}>
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">{v.tanggal}</Text>
</Group>
<Group gap="xs">
<IconClock size={18} />
<Text size="sm">{v.jam}</Text>
</Group>
<Group gap="xs">
<IconMapPin size={18} />
<Text size="sm">{v.lokasi}</Text>
</Group>
</Group>
<Text ta={'justify'}>
{v.deskripsi}
</Text>
</Paper>
)
})}
</Box>
</Stack>
);
}
export default Page;

View File

@@ -1,96 +0,0 @@
import colors from '@/con/colors';
import { Stack, Box, Container, Text, Paper, Group } from '@mantine/core';
import React from 'react';
import BackButton from '../../../layanan/_com/BackButto';
import { IconCalendar, IconClock, IconMapPin } from '@tabler/icons-react';
const dataPengumuman = [
{
id: 1,
judul: 'Pendaftaran Bimbingan Belajar Gratis untuk Pelajar',
tanggal: 'Sabtu, 20 April 2025',
jam: '08:00 WITA',
lokasi: 'Balai Banjar Desa Darmasaba',
deskripsi: 'Dalam rangka meningkatkan kesadaran kesehatan, Pemerintah Desa Darmasaba bekerja sama dengan Puskesmas Abiansemal akan mengadakan pemeriksaan kesehatan gratis meliputi cek tekanan darah, kolesterol, gula darah, dan konsultasi gizi.'
},
{
id: 2,
judul: 'Lomba Video Pendek Hari Lingkungan',
tanggal: 'Deadline: 28 April 2025',
jam: '08:00 WITA',
lokasi: 'Online Submission',
deskripsi: 'Karang Taruna Desa mengadakan lomba video pendek bertema "Lingkunganku, Tanggung Jawabku". Pemenang akan diumumkan saat acara Hari Desa Hijau. Total hadiah Rp1.000.000.'
},
{
id: 3,
judul: 'Pendaftaran Bimbingan Belajar Gratis untuk Pelajar',
tanggal: 'Sabtu, 20 April 2025',
jam: '08:00 WITA',
lokasi: 'Balai Banjar Desa Darmasaba',
deskripsi: 'Dalam rangka meningkatkan kesadaran kesehatan, Pemerintah Desa Darmasaba bekerja sama dengan Puskesmas Abiansemal akan mengadakan pemeriksaan kesehatan gratis meliputi cek tekanan darah, kolesterol, gula darah, dan konsultasi gizi.'
},
{
id: 4,
judul: 'Lomba Video Pendek Hari Lingkungan',
tanggal: 'Deadline: 28 April 2025',
jam: '08:00 WITA',
lokasi: 'Online Submission',
deskripsi: 'Karang Taruna Desa mengadakan lomba video pendek bertema "Lingkunganku, Tanggung Jawabku". Pemenang akan diumumkan saat acara Hari Desa Hijau. Total hadiah Rp1.000.000.'
},
{
id: 5,
judul: 'Pendaftaran Bimbingan Belajar Gratis untuk Pelajar',
tanggal: 'Sabtu, 20 April 2025',
jam: '08:00 WITA',
lokasi: 'Balai Banjar Desa Darmasaba',
deskripsi: 'Dalam rangka meningkatkan kesadaran kesehatan, Pemerintah Desa Darmasaba bekerja sama dengan Puskesmas Abiansemal akan mengadakan pemeriksaan kesehatan gratis meliputi cek tekanan darah, kolesterol, gula darah, dan konsultasi gizi.'
},
]
function Page() {
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="md">
{/* Header */}
<Box px={{ base: "md", md: 100 }}>
<BackButton />
</Box>
<Container size="lg" px="md" >
<Stack align="center" gap="0" >
<Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" ta="center">
Pengumuman Pendidikan & Kepemudaan
</Text>
<Text ta="center" px="md" pb={10}>
Informasi dan pengumuman resmi terkait pengumuman pendidikan & kepemudaan
</Text>
</Stack>
</Container>
<Box px={{ base: 'md', md: 100 }}>
{dataPengumuman.map((v, k) => {
return (
<Paper mb={10} key={k} withBorder p="lg" radius="md" shadow="md" bg={colors["white-1"]}>
<Text fz={'h3'}>{v.judul}</Text>
<Group style={{ color: 'black' }} pb={20}>
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">{v.tanggal}</Text>
</Group>
<Group gap="xs">
<IconClock size={18} />
<Text size="sm">{v.jam}</Text>
</Group>
<Group gap="xs">
<IconMapPin size={18} />
<Text size="sm">{v.lokasi}</Text>
</Group>
</Group>
<Text ta={'justify'}>
{v.deskripsi}
</Text>
</Paper>
)
})}
</Box>
</Stack>
);
}
export default Page;

View File

@@ -1,96 +0,0 @@
import colors from '@/con/colors';
import { Stack, Box, Container, Text, Paper, Group } from '@mantine/core';
import React from 'react';
import BackButton from '../../../layanan/_com/BackButto';
import { IconCalendar, IconClock, IconMapPin } from '@tabler/icons-react';
const dataPengumuman = [
{
id: 1,
judul: 'Pemeriksaan Kesehatan Gratis Untuk Warga Desa',
tanggal: 'Sabtu, 20 April 2025',
jam: '08:00 WITA',
lokasi: 'Balai Banjar Desa Darmasaba',
deskripsi: 'Dalam rangka meningkatkan kesadaran kesehatan, Pemerintah Desa Darmasaba bekerja sama dengan Puskesmas Abiansemal akan mengadakan pemeriksaan kesehatan gratis meliputi cek tekanan darah, kolesterol, gula darah, dan konsultasi gizi.'
},
{
id: 2,
judul: 'Pemeriksaan Kesehatan Gratis Untuk Warga Desa',
tanggal: 'Sabtu, 20 April 2025',
jam: '08:00 WITA',
lokasi: 'Balai Banjar Desa Darmasaba',
deskripsi: 'Dalam rangka meningkatkan kesadaran kesehatan, Pemerintah Desa Darmasaba bekerja sama dengan Puskesmas Abiansemal akan mengadakan pemeriksaan kesehatan gratis meliputi cek tekanan darah, kolesterol, gula darah, dan konsultasi gizi.'
},
{
id: 3,
judul: 'Pemeriksaan Kesehatan Gratis Untuk Warga Desa',
tanggal: 'Sabtu, 20 April 2025',
jam: '08:00 WITA',
lokasi: 'Balai Banjar Desa Darmasaba',
deskripsi: 'Dalam rangka meningkatkan kesadaran kesehatan, Pemerintah Desa Darmasaba bekerja sama dengan Puskesmas Abiansemal akan mengadakan pemeriksaan kesehatan gratis meliputi cek tekanan darah, kolesterol, gula darah, dan konsultasi gizi.'
},
{
id: 4,
judul: 'Pemeriksaan Kesehatan Gratis Untuk Warga Desa',
tanggal: 'Sabtu, 20 April 2025',
jam: '08:00 WITA',
lokasi: 'Balai Banjar Desa Darmasaba',
deskripsi: 'Dalam rangka meningkatkan kesadaran kesehatan, Pemerintah Desa Darmasaba bekerja sama dengan Puskesmas Abiansemal akan mengadakan pemeriksaan kesehatan gratis meliputi cek tekanan darah, kolesterol, gula darah, dan konsultasi gizi.'
},
{
id: 5,
judul: 'Pemeriksaan Kesehatan Gratis Untuk Warga Desa',
tanggal: 'Sabtu, 20 April 2025',
jam: '08:00 WITA',
lokasi: 'Balai Banjar Desa Darmasaba',
deskripsi: 'Dalam rangka meningkatkan kesadaran kesehatan, Pemerintah Desa Darmasaba bekerja sama dengan Puskesmas Abiansemal akan mengadakan pemeriksaan kesehatan gratis meliputi cek tekanan darah, kolesterol, gula darah, dan konsultasi gizi.'
},
]
function Page() {
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="md">
{/* Header */}
<Box px={{ base: "md", md: 100 }}>
<BackButton />
</Box>
<Container size="lg" px="md" >
<Stack align="center" gap="0" >
<Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" ta="center">
Pengumuman Sosial & Kesehatan
</Text>
<Text ta="center" px="md" pb={10}>
Informasi dan pengumuman resmi terkait pengumuman sosial & kesehatan
</Text>
</Stack>
</Container>
<Box px={{ base: 'md', md: 100 }}>
{dataPengumuman.map((v, k) => {
return (
<Paper mb={10} key={k} withBorder p="lg" radius="md" shadow="md" bg={colors["white-1"]}>
<Text fz={'h3'}>{v.judul}</Text>
<Group style={{ color: 'black' }} pb={20}>
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">{v.tanggal}</Text>
</Group>
<Group gap="xs">
<IconClock size={18} />
<Text size="sm">{v.jam}</Text>
</Group>
<Group gap="xs">
<IconMapPin size={18} />
<Text size="sm">{v.lokasi}</Text>
</Group>
</Group>
<Text ta={'justify'}>
{v.deskripsi}
</Text>
</Paper>
)
})}
</Box>
</Stack>
);
}
export default Page;

View File

@@ -46,8 +46,8 @@ function Page() {
</Group>
</Group>
<Paper bg={colors["white-1"]} p="md">
<Text id='news-content' fz={"md"} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: detail.data?.content }} />
<Text fz={"md"} c={colors["blue-button"]} fw="bold" >
<Text px="lg" id='news-content' fz={"md"} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: detail.data?.content }} />
<Text px="lg" fz={"md"} c={colors["blue-button"]} fw="bold" >
{new Date(detail.data?.createdAt).toLocaleDateString('id-ID', {
weekday: 'long',
day: 'numeric',

View File

@@ -14,6 +14,9 @@ import {
Loader,
Paper,
Stack,
Tabs,
TabsList,
TabsTab,
Text,
TextInput,
Title,
@@ -35,6 +38,7 @@ import { useEffect, useRef, useState } from 'react'
import { useProxy } from 'valtio/utils'
import './struktur.css'
import BackButton from '../_com/BackButto'
import { useMediaQuery } from '@mantine/hooks'
export default function StrukturPerangkatDesa() {
return (
@@ -231,87 +235,121 @@ function StrukturPerangkatDesaNode() {
p="md"
radius="md"
style={{
background: colors['blue-button']
background: colors['blue-button'],
width: '100%', // ⬅️ penting
maxWidth: '100%', // ⬅️ penting
overflowX: 'auto' // ⬅️ untuk mencegah overflow
}}
>
<Group gap="sm" wrap="wrap" justify="center">
<TextInput
placeholder="Cari nama atau jabatan..."
leftSection={<IconSearch size={16} />}
onChange={(e) => debouncedSearch(e.target.value)}
<Stack gap="sm">
<Group justify='center'>
<TextInput
placeholder="Cari nama atau jabatan..."
leftSection={<IconSearch size={16} />}
onChange={(e) => debouncedSearch(e.target.value)}
styles={{
input: {
minWidth: 250,
},
}}
/>
</Group>
<Tabs
defaultValue="zoom-out"
variant="outline"
radius="md"
styles={{
input: {
minWidth: 250,
panel: { display: 'none' },
tab: {
color: colors['blue-button'],
backgroundColor: colors['blue-button-2'],
border: 'none',
fontWeight: 600,
fontSize: '0.875rem',
padding: '6px 12px',
minHeight: 'auto',
flexShrink: 0, // 👈 PENTING: mencegah tab mengecil
},
}}
/>
<Group gap="xs">
<Button
variant="light"
bg={colors['blue-button-2']}
size="sm"
onClick={handleZoomOut}
leftSection={<IconZoomOut size={16} />}
c={colors['blue-button']}
>
Zoom Out
</Button>
<Box
bg={colors['blue-button-2']}
c={colors['blue-button']}
px={16}
py={8}
>
<TabsList
style={{
fontSize: 14,
fontWeight: 700,
borderRadius: '8px',
minWidth: 70,
textAlign: 'center',
display: 'flex',
overflowX: 'auto',
overflowY: 'hidden', // 👈 tambahkan ini
gap: '4px',
paddingBottom: '4px',
flexWrap: 'nowrap',
WebkitOverflowScrolling: 'touch', // 👈 smooth scroll di iOS
scrollbarWidth: 'thin', // 👈 scrollbar tipis di Firefox
msOverflowStyle: '-ms-autohiding-scrollbar', // 👈 untuk IE/Edge
}}
>
{Math.round(scale * 100)}%
</Box>
<TabsTab
value="zoom-out"
onClick={handleZoomOut}
leftSection={<IconZoomOut size={16} />}
style={{ flexShrink: 0 }} // 👈 pastikan tidak mengecil
>
Zoom Out
</TabsTab>
<Button
bg={colors['blue-button-2']}
c={colors['blue-button']}
variant="light"
size="sm"
onClick={handleZoomIn}
leftSection={<IconZoomIn size={16} />}
>
Zoom In
</Button>
<Box
bg={colors['blue-button-2']}
c={colors['blue-button']}
px={12}
py={6}
style={{
fontSize: 14,
fontWeight: 700,
borderRadius: '6px',
minWidth: 60,
textAlign: 'center',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
whiteSpace: 'nowrap', // 👈 mencegah text wrap
}}
>
{Math.round(scale * 100)}%
</Box>
<Button
bg={colors['blue-button-2']}
c={colors['blue-button']}
variant="light"
size="sm"
onClick={resetZoom}
>
Reset
</Button>
<TabsTab
value="zoom-in"
onClick={handleZoomIn}
leftSection={<IconZoomIn size={16} />}
style={{ flexShrink: 0 }}
>
Zoom In
</TabsTab>
<Button
bg={colors['blue-button-2']}
c={colors['blue-button']}
size="sm"
onClick={toggleFullscreen}
leftSection={
isFullscreen ? (
<IconArrowsMinimize size={16} />
) : (
<IconArrowsMaximize size={16} />
)
}
>
Fullscreen
</Button>
</Group>
</Group>
<TabsTab
value="reset"
onClick={resetZoom}
style={{ flexShrink: 0 }}
>
Reset
</TabsTab>
<TabsTab
value="fullscreen"
onClick={toggleFullscreen}
leftSection={
isFullscreen ? (
<IconArrowsMinimize size={16} />
) : (
<IconArrowsMaximize size={16} />
)
}
style={{ flexShrink: 0 }}
>
{isFullscreen ? 'Exit' : 'Fullscreen'}
</TabsTab>
</TabsList>
</Tabs>
</Stack>
</Paper>
{/* 🧩 Chart Container */}
@@ -325,15 +363,20 @@ function StrukturPerangkatDesaNode() {
maxWidth: '100%',
padding: '32px 16px',
transition: 'transform 0.2s ease',
transform: `scale(${scale})`,
transformOrigin: 'center top',
}}
>
<OrganizationChart
value={chartData}
nodeTemplate={(node) => <NodeCard node={node} router={router} />}
className="p-organizationchart p-organizationchart-horizontal"
/>
<Box style={{
transform: `scale(${scale})`,
transformOrigin: 'center top',
display: 'inline-block', // 👈 agar tidak memenuhi lebar parent
minWidth: 'min-content', // 👈 penting agar chart tidak dipaksa muat di width 100%
}}>
<OrganizationChart
value={chartData}
nodeTemplate={(node) => <NodeCard node={node} router={router} />}
className="p-organizationchart p-organizationchart-horizontal"
/>
</Box>
</Box>
</Center>
</Stack>
@@ -345,6 +388,7 @@ function NodeCard({ node, router }: any) {
const name = node?.data?.name || 'Tanpa Nama'
const title = node?.data?.title || 'Tanpa Jabatan'
const hasId = Boolean(node?.data?.id)
const isMobile = useMediaQuery("(max-width: 768px)");
return (
<Transition mounted transition="pop" duration={300}>
@@ -355,9 +399,10 @@ function NodeCard({ node, router }: any) {
withBorder
style={{
...styles,
width: 240,
minHeight: 280,
padding: 20,
width: '100%',
maxWidth: isMobile ? 200 : 240, // lebih kecil di mobile
minHeight: isMobile ? 240 : 280,
padding: isMobile ? 16 : 20,
background: 'linear-gradient(135deg, rgba(28,110,164,0.15) 0%, rgba(255,255,255,0.95) 100%)',
borderColor: 'rgba(28, 110, 164, 0.3)',
borderWidth: 2,

View File

@@ -26,7 +26,7 @@ function Page() {
const state = useProxy(lowonganKerjaState)
const router = useRouter()
const [search, setSearch] = useState('')
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const {
data,

View File

@@ -14,7 +14,7 @@ function Page() {
const router = useRouter()
const state = useProxy(pasarDesaState.pasarDesa)
const [search, setSearch] = useState('');
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
const {
data,

View File

@@ -32,7 +32,7 @@ interface ProgramKemiskinanData {
function Page() {
const [search, setSearch] = useState('')
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const state = useProxy(programKemiskinanState)
// 🔧 Get valid statistics data with proper type checking

View File

@@ -11,7 +11,7 @@ import { useRouter } from 'next/navigation';
function Page() {
const [search, setSearch] = useState("")
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const state = useProxy(desaDigitalState)
const router = useRouter()
const {

View File

@@ -11,7 +11,7 @@ import { IconSearch } from '@tabler/icons-react';
function Page() {
const [search, setSearch] = useState("")
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const state = useProxy(infoTeknoState)
const {
data,

View File

@@ -45,7 +45,7 @@ import BackButton from '../../desa/layanan/_com/BackButto';
function Page() {
const listState = useProxy(programKreatifState);
const [search, setSearch] = useState("");
const [debouncedSearch] = useDebouncedValue(search, 500);
const [debouncedSearch] = useDebouncedValue(search, 1000);
const router = useTransitionRouter()
const {
data,

View File

@@ -14,7 +14,7 @@ function Page() {
const state = useProxy(keamananLingkunganState)
const router = useRouter()
const [search, setSearch] = useState('')
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const {
data,
page,

View File

@@ -12,7 +12,7 @@ import { IconKey, IconMapper } from '@/app/admin/(dashboard)/_com/iconMap';
function Page() {
const kontakState = useProxy(kontakDarurat.kontakDaruratKeamananState);
const [search, setSearch] = useState("");
const [debouncedSearch] = useDebouncedValue(search, 500);
const [debouncedSearch] = useDebouncedValue(search, 1000);
const {
data,
page,

View File

@@ -1,10 +1,26 @@
'use client'
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import laporanPublikState from '@/app/admin/(dashboard)/_state/keamanan/laporan-publik';
import colors from '@/con/colors';
import { Box, Button, Center, ColorSwatch, Flex, Group, Modal, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
import {
Box,
Button,
Center,
ColorSwatch,
Flex,
Group,
Modal,
Pagination,
Paper,
SimpleGrid,
Skeleton,
Stack,
Text,
TextInput,
} from '@mantine/core';
import { DateTimePicker } from '@mantine/dates';
import { useDebouncedValue, useDisclosure, useShallowEffect } from '@mantine/hooks';
import { useDebouncedValue, useDisclosure, useMediaQuery, useShallowEffect } from '@mantine/hooks';
import { IconArrowRight, IconPlus, IconSearch } from '@tabler/icons-react';
import { useTransitionRouter } from 'next-view-transitions';
import { useState } from 'react';
@@ -12,9 +28,10 @@ import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto';
function Page() {
const [search, setSearch] = useState("");
const router = useTransitionRouter()
const [debouncedSearch] = useDebouncedValue(search, 500);
const mobile = useMediaQuery('(max-width: 768px)');
const [search, setSearch] = useState('');
const router = useTransitionRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const [opened, { open, close }] = useDisclosure(false);
const stateLaporan = useProxy(laporanPublikState);
const {
@@ -49,143 +66,219 @@ function Page() {
const handleSubmit = async () => {
await stateLaporan.create.create();
resetForm();
close();
};
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
{/* Header: Back + Search */}
<Box px={{ base: 'md', md: 100 }}>
<Group justify="space-between" align="center">
<BackButton />
<TextInput
radius={"lg"}
placeholder='Cari Laporan Publik'
value={search}
onChange={(e) => setSearch(e.target.value)}
leftSection={<IconSearch size={20} />}
w={{ base: "100%", md: "30%" }}
/>
radius="lg"
placeholder="Cari Laporan Publik"
value={search}
onChange={(e) => setSearch(e.target.value)}
leftSection={<IconSearch size={20} />}
w={{ base: '100%', md: '30%' }}
size={mobile ? 'sm' : 'md'}
/>
</Group>
</Box>
{/* Title + Add Button */}
<Box px={{ base: 'md', md: 100 }}>
<Group justify="space-between">
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
<Group justify="space-between" align="flex-start">
<Text
ta="center"
fz={{ base: 'xl', sm: '2xl', md: '2.5rem' }}
c={colors['blue-button']}
fw="bold"
lineClamp={2}
style={{ wordBreak: 'break-word' }}
>
Laporan Keamanan Lingkungan
</Text>
<Button
onClick={open}
bg={colors['blue-button']}
size="md"
size={mobile ? 'sm' : 'md'}
radius="md"
rightSection={<IconPlus size={20} />}
>
Tambah Laporan
{mobile ? 'Tambah' : 'Tambah Laporan'}
</Button>
</Group>
</Box>
<Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'}>
<Flex justify={'space-between'} align={'center'}>
<Text fz={{ base: 'sm', md: 'h4' }} fw={'bold'}>Laporan Terbaru</Text>
<Box>
<Flex gap={'lg'}>
<Box>
<Flex gap={{ base: 2, md: 5 }} align={'center'}>
<Text fz={{ base: 'sm', md: 'h4' }}>Terselesaikan</Text>
<ColorSwatch color="#2A742D" size={20} />
</Flex>
</Box>
<Box>
<Flex gap={{ base: 2, md: 5 }} align={'center'}>
<Text fz={{ base: 'sm', md: 'h4' }}>Dalam Proses</Text>
<ColorSwatch color="#D1961F" size={20} />
</Flex>
</Box>
<Box>
<Flex gap={{ base: 2, md: 5 }} align={'center'}>
<Text fz={{ base: 'sm', md: 'h4' }}>Gagal</Text>
<ColorSwatch color="#A34437" size={20} />
</Flex>
</Box>
</Flex>
</Box>
</Flex>
<SimpleGrid
cols={{
base: 1,
md: 3
}}
{/* Legend Status */}
<Box px={{ base: 'md', md: 100 }}>
<Flex
justify="space-between"
align="center"
direction={mobile ? 'column' : 'row'}
gap={mobile ? 'xs' : 'lg'}
>
<Text fz={{ base: 'sm', md: 'h4' }} fw="bold">
Laporan Terbaru
</Text>
<Flex
gap={mobile ? 'xs' : 'lg'}
wrap="wrap"
justify={mobile ? 'center' : 'flex-start'}
align="center"
>
{data.map((v, k) => {
return (
<Paper radius={'lg'} key={k} bg={colors['white-trans-1']} p={'xl'}>
<Stack>
<Text c={colors['blue-button']} lineClamp={3} truncate="end" fz="h4" fw="bold">{v.judul}</Text>
<Text fs={'italic'} fz={'xl'}>
{v.tanggalWaktu
? new Date(v.tanggalWaktu).toLocaleString('id-ID')
: '-'}
</Text>
<Box>
<Text fw={'bold'}>Penanganan:</Text>
{v.penanganan?.length ? (
v.penanganan.map((item, index) => (
<Box key={index}>
<Text
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }}
style={{wordBreak: "break-word", whiteSpace: "normal"}}
/>
</Box>
))
) : (
<Text fz="sm" fs="italic" c="dimmed">
Belum ada penanganan
</Text>
)}
</Box>
<Box
style={{
display: 'inline-block',
padding: '4px 12px',
borderRadius: '16px',
backgroundColor:
v.status === 'Selesai' ? '#94EF95FF' :
v.status === 'Proses' ? '#F1D295FF' :
'#F38E8EFF',
color:
v.status === 'Selesai' ? '#01BA01FF' :
v.status === 'Proses' ? '#B67A00FF' :
'#AE1700FF',
fontWeight: 900,
fontSize: '0.75rem',
textAlign: 'center',
minWidth: '80px',
}}
>
{v.status}
</Box>
<Button
bg={colors['blue-button']}
rightSection={<IconArrowRight size={20} color={colors['white-1']} />}
onClick={() => router.push(`/darmasaba/keamanan/laporan-publik/${v.id}`)}
>Lihat Detail Kronologi
</Button>
</Stack>
</Paper>
)
})}
</SimpleGrid>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
total={totalPages}
my="md"
/>
</Center>
</Stack>
<Flex gap={2} align="center">
<ColorSwatch color="#2A742D" size={16} />
<Text fz={{ base: 'xs', md: 'sm' }}>Terselesaikan</Text>
</Flex>
<Flex gap={2} align="center">
<ColorSwatch color="#D1961F" size={16} />
<Text fz={{ base: 'xs', md: 'sm' }}>Dalam Proses</Text>
</Flex>
<Flex gap={2} align="center">
<ColorSwatch color="#A34437" size={16} />
<Text fz={{ base: 'xs', md: 'sm' }}>Gagal</Text>
</Flex>
</Flex>
</Flex>
</Box>
<Modal opened={opened} onClose={close} title="Tambah Laporan Publik">
{/* Cards Grid */}
<Box px={{ base: 'md', md: 100 }}>
<SimpleGrid
cols={{
base: 1,
md: 3,
}}
spacing="lg"
>
{data.map((v, k) => (
<Paper
key={k}
radius="lg"
bg={colors['white-trans-1']}
p="lg"
shadow="sm"
style={{
'&:hover': {
transform: 'translateY(-4px)',
boxShadow: '0 8px 20px rgba(0,0,0,0.1)',
},
transition: 'transform 0.2s ease, box-shadow 0.2s ease',
}}
>
<Stack gap="sm">
<Text
c={colors['blue-button']}
lineClamp={2}
fz={{ base: 'lg', md: 'xl' }}
fw="bold"
style={{ wordBreak: 'break-word' }}
>
{v.judul}
</Text>
<Text
fs="italic"
fz={{ base: 'sm', md: 'md' }}
c="dimmed"
>
{v.tanggalWaktu
? new Date(v.tanggalWaktu).toLocaleString('id-ID')
: '-'}
</Text>
<Box>
<Text fw="bold" fz="sm">
Penanganan:
</Text>
{v.penanganan?.length ? (
v.penanganan.map((item, index) => (
<Box key={index}>
<Text
fz="xs"
c="dimmed"
dangerouslySetInnerHTML={{
__html: item.deskripsi || '-',
}}
style={{
wordBreak: 'break-word',
whiteSpace: 'normal',
maxHeight: '80px',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
/>
</Box>
))
) : (
<Text fz="xs" fs="italic" c="dimmed">
Belum ada penanganan
</Text>
)}
</Box>
<Box
style={{
display: 'inline-block',
padding: '4px 8px',
borderRadius: '12px',
backgroundColor:
v.status === 'Selesai'
? '#94EF95FF'
: v.status === 'Proses'
? '#F1D295FF'
: '#F38E8EFF',
color:
v.status === 'Selesai'
? '#01BA01FF'
: v.status === 'Proses'
? '#B67A00FF'
: '#AE1700FF',
fontWeight: 700,
fontSize: '0.75rem',
textAlign: 'center',
minWidth: '70px',
}}
>
{v.status}
</Box>
<Button
bg={colors['blue-button']}
rightSection={
<IconArrowRight
size={18}
color={colors['white-1']}
/>
}
onClick={() => router.push(`/darmasaba/keamanan/laporan-publik/${v.id}`)}
size={mobile ? 'sm' : 'md'}
fullWidth
>
{mobile ? 'Detail' : 'Lihat Detail Kronologi'}
</Button>
</Stack>
</Paper>
))}
</SimpleGrid>
</Box>
{/* Pagination */}
<Center px={{ base: 'md', md: 100 }}>
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
total={totalPages}
my="md"
size={mobile ? 'sm' : 'md'}
/>
</Center>
{/* Modal Form */}
<Modal opened={opened} onClose={close} title="Tambah Laporan Publik" size="xl">
<Paper
bg={colors['white-1']}
p="lg"
@@ -196,18 +289,26 @@ function Page() {
<Stack gap="md">
<TextInput
value={stateLaporan.create.form.judul}
onChange={(e) => (stateLaporan.create.form.judul = e.target.value)}
onChange={(e) =>
(stateLaporan.create.form.judul = e.target.value)
}
label={<Text fw="bold" fz="sm">Judul Laporan Publik</Text>}
placeholder="Masukkan judul laporan publik"
required
w="100%"
size={mobile ? 'sm' : 'md'}
/>
<TextInput
value={stateLaporan.create.form.lokasi}
onChange={(e) => (stateLaporan.create.form.lokasi = e.target.value)}
onChange={(e) =>
(stateLaporan.create.form.lokasi = e.target.value)
}
label={<Text fw="bold" fz="sm">Lokasi Laporan Publik</Text>}
placeholder="Masukkan lokasi laporan publik"
required
w="100%"
size={mobile ? 'sm' : 'md'}
/>
<DateTimePicker
@@ -220,6 +321,8 @@ function Page() {
onChange={(val) => {
stateLaporan.create.form.tanggalWaktu = val ? val.toString() : '';
}}
w="100%"
size={mobile ? 'sm' : 'md'}
/>
<Box>
@@ -238,7 +341,7 @@ function Page() {
<Button
onClick={handleSubmit}
radius="md"
size="md"
size={mobile ? 'sm' : 'md'}
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
@@ -255,4 +358,4 @@ function Page() {
);
}
export default Page;
export default Page;

View File

@@ -13,7 +13,7 @@ import { useDebouncedValue } from '@mantine/hooks';
function Page() {
const state = useProxy(polsekTerdekatState);
const [search, setSearch] = useState('');
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const router = useRouter()
const {

View File

@@ -12,7 +12,7 @@ import { IconSearch } from '@tabler/icons-react';
function Page() {
const state = useProxy(tipsKeamananState)
const [search, setSearch] = useState('')
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const {
data,
page,

View File

@@ -2,7 +2,7 @@
import artikelKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/artikelKesehatan';
import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
import colors from '@/con/colors';
import { Box, Divider, Group, Image, List, ListItem, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { Box, Divider, Flex, Group, Image, List, ListItem, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconAlertCircle, IconCalendar, IconInfoCircle } from '@tabler/icons-react';
import { useParams } from 'next/navigation';
@@ -44,18 +44,18 @@ function Page() {
<Box p="lg">
<Box style={{ position: 'relative', width: '100%', maxWidth: '800px', margin: '0 auto' }}>
<Image
src={state.findUnique.data.image?.link}
alt={state.findUnique.data.title}
<Image
src={state.findUnique.data.image?.link}
alt={state.findUnique.data.title}
height={0}
style={{
style={{
height: 'auto',
width: '100%',
maxHeight: '500px',
objectFit: 'contain',
borderRadius: '8px'
}}
loading="lazy"
loading="lazy"
/>
</Box>
</Box>
@@ -78,25 +78,33 @@ function Page() {
<Box>
<Text fz="h4" fw="bold">Pendahuluan</Text>
<Divider my="xs" />
<Text fz="md" lh={1.6} ta="justify" dangerouslySetInnerHTML={{ __html: state.findUnique.data.introduction?.content }} />
<Box pl={20}>
<Text fz="md" lh={1.6} ta="justify" dangerouslySetInnerHTML={{ __html: state.findUnique.data.introduction?.content }} />
</Box>
</Box>
<Box>
<Text fz="h4" fw="bold">{state.findUnique.data.symptom?.title}</Text>
<Divider my="xs" />
<Text fz="md" lh={1.6} ta="justify" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: state.findUnique.data.symptom?.content }} />
<Box pl={20}>
<Text fz="md" lh={1.6} ta="justify" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.symptom?.content }} />
</Box>
</Box>
<Box>
<Text fz="h4" fw="bold">{state.findUnique.data.prevention?.title}</Text>
<Divider my="xs" />
<Text fz="md" lh={1.6} ta="justify" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: state.findUnique.data.prevention?.content }} />
<Box pl={20}>
<Text fz="md" lh={1.6} ta="justify" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.prevention?.content }} />
</Box>
</Box>
<Box>
<Text fz="h4" fw="bold">{state.findUnique.data.firstaid?.title}</Text>
<Divider my="xs" />
<Text fz="md" lh={1.6} ta="justify" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: state.findUnique.data.firstaid?.content }} />
<Box pl={20}>
<Text fz="md" lh={1.6} ta="justify" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.firstaid?.content }} />
</Box>
</Box>
<Box>
@@ -114,10 +122,14 @@ function Page() {
{state.findUnique.data?.mythvsfact ? (
<TableTr>
<TableTd>
<Text fz="sm" lh={1.6} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: state.findUnique.data.mythvsfact.mitos }} />
<Box pl={20}>
<Text fz="sm" lh={1.6} style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.mythvsfact.mitos }} />
</Box>
</TableTd>
<TableTd>
<Text fz="sm" lh={1.6} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: state.findUnique.data.mythvsfact.fakta }} />
<Box pl={20}>
<Text fz="sm" lh={1.6} style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.mythvsfact.fakta }} />
</Box>
</TableTd>
</TableTr>
) : (
@@ -133,17 +145,15 @@ function Page() {
<Box>
<Text fz="h4" fw="bold">Kapan Harus ke Dokter?</Text>
<Divider my="xs" />
<Group gap="xs" mb="xs">
<Flex justify={'flex-start'} gap={"xs"} align={"center"} mb="xs">
<IconAlertCircle size={18} color="red" />
<Text fz="md">Segera bawa penderita ke fasilitas kesehatan jika mengalami:</Text>
</Group>
<Text fz="md" lh={1.6} dangerouslySetInnerHTML={{ __html: state.findUnique.data.doctorsign.content }} />
</Flex>
<Box pl={20}>
<Text fz="md" lh={1.6} dangerouslySetInnerHTML={{ __html: state.findUnique.data.doctorsign.content }} /></Box>
</Box>
<Box>
<Text fz="h4" fw="bold">Kasus DBD di Wilayah Abiansemal</Text>
<Divider my="xs" />
<Paper p="lg" radius="md" bg={colors['blue-button-trans']} withBorder>
<Group gap="xs" mb="sm">
<IconInfoCircle size={20} color={colors['white-1']} />

View File

@@ -175,7 +175,9 @@ function Page() {
<Title order={4}>Layanan Unggulan</Title>
<Divider />
{layananUnggulan ? (
<Text fz="md" style={{ lineHeight: 1.7, wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: layananUnggulan }} />
<Box pl={"lg"}>
<Text fz="md" style={{ lineHeight: 1.7, wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: layananUnggulan }} />
</Box>
) : (
<Paper withBorder radius="md" p="md">
<Group gap="sm">
@@ -251,7 +253,9 @@ function Page() {
<Title order={3}>Fasilitas Pendukung</Title>
<Divider />
{fasilitasPendukungHtml ? (
<Text fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal", lineHeight: 1.7 }} dangerouslySetInnerHTML={{ __html: fasilitasPendukungHtml }} />
<Box pl="lg">
<Text fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal", lineHeight: 1.7 }} dangerouslySetInnerHTML={{ __html: fasilitasPendukungHtml }} />
</Box>
) : (
<Paper withBorder radius="md" p="md">
<Group gap="sm">
@@ -313,7 +317,7 @@ function Page() {
<Title order={3}>Prosedur Pendaftaran</Title>
<Divider />
{prosedur ? (
<Text fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal", lineHeight: 1.7 }} dangerouslySetInnerHTML={{ __html: prosedur }} />
<Box pl="lg"><Text fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal", lineHeight: 1.7 }} dangerouslySetInnerHTML={{ __html: prosedur }} /></Box>
) : (
<Text fz="md" c="dimmed">Belum ada prosedur pendaftaran</Text>
)}

View File

@@ -95,7 +95,7 @@ function GrafikPenyakit() {
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}>
<Paper bg={colors['white-1']} p={'md'}>
<Center>
<Title pb={10} order={3}>Grafik Hasil Kepuasan Masyarakat</Title>
<Title pb={10} order={2}>Penderita Penyakit</Title>
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text>
</Center>
</Paper>
@@ -103,7 +103,7 @@ function GrafikPenyakit() {
) : (
<Box style={{ width: '100%', minWidth: 300, height: 420, minHeight: 300 }}>
<Paper bg={colors["white-trans-1"]} p={'md'}>
<Title pb={10} order={4}>Grafik Hasil Kepuasan Masyarakat</Title>
<Title pb={10} order={2}>Penderita Penyakit</Title>
{mounted && diseaseChartData.length > 0 && (
<Center>
<BarChart width={isMobile ? 450 : isTablet ? 500 : 550} height={350} data={diseaseChartData} >

View File

@@ -69,25 +69,33 @@ function Page() {
<Stack gap="sm">
<Text fz="lg" fw="bold">Deskripsi Kegiatan</Text>
<Divider />
<Text ta="justify" fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.deskripsijadwalkegiatan.deskripsi }} />
<Box pl={20}>
<Text ta="justify" fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.deskripsijadwalkegiatan.deskripsi }} />
</Box>
</Stack>
<Stack gap="sm">
<Text fz="lg" fw="bold">Layanan yang Tersedia</Text>
<Divider />
<Text ta="justify" fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.layananjadwalkegiatan.content }} />
<Box pl={20}>
<Text ta="justify" fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.layananjadwalkegiatan.content }} />
</Box>
</Stack>
<Stack gap="sm">
<Text fz="lg" fw="bold">Syarat & Ketentuan</Text>
<Divider />
<Text ta="justify" fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.syaratketentuanjadwalkegiatan.content }} />
<Box pl={20}>
<Text ta="justify" fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.syaratketentuanjadwalkegiatan.content }} />
</Box>
</Stack>
<Stack gap="sm">
<Text fz="lg" fw="bold">Dokumen yang Perlu Dibawa</Text>
<Divider />
<Text ta="justify" fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.dokumenjadwalkegiatan.content }} />
<Box pl={20}>
<Text ta="justify" fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.dokumenjadwalkegiatan.content }} />
</Box>
</Stack>
<Stack gap="sm">

View File

@@ -71,11 +71,13 @@ function DetailInfoWabahPenyakitUser() {
{/* Deskripsi */}
<Box>
<Text fz="lg" fw="bold">Deskripsi</Text>
<Text
fz="md"
dangerouslySetInnerHTML={{ __html: data.deskripsiLengkap || '-' }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/>
<Box pl={20}>
<Text
fz="md"
dangerouslySetInnerHTML={{ __html: data.deskripsiLengkap || '-' }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/>
</Box>
</Box>
</Stack>
</Paper>

View File

@@ -30,7 +30,7 @@ function Page() {
const state = useProxy(infoWabahPenyakit);
const router = useTransitionRouter();
const [search, setSearch] = useState('');
const [debouncedSearch] = useDebouncedValue(search, 500)
const [debouncedSearch] = useDebouncedValue(search, 1000)
const { data, page, totalPages, loading, load } = state.findMany;
useShallowEffect(() => {

View File

@@ -0,0 +1,111 @@
'use client'
import kontakDarurat from '@/app/admin/(dashboard)/_state/kesehatan/kontak-darurat/kontakDarurat';
import colors from '@/con/colors';
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconBrandWhatsapp } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
function Page() {
const state = useProxy(kontakDarurat);
const router = useRouter();
const params = useParams();
useShallowEffect(() => {
state.findUnique.load(params?.id as string);
}, []);
if (!state.findUnique.data) {
return (
<Stack py={10}>
<Skeleton height={500} radius="md" />
</Stack>
);
}
const data = state.findUnique.data;
return (
<Box px={{base: 'md', md: 100}} py={10}>
{/* Tombol Back */}
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15}
>
Kembali
</Button>
{/* Wrapper Detail */}
<Paper
withBorder
w="100%"
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Kontak Darurat
</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.name || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Whatsapp</Text>
<Text fz="md" c="dimmed">{data.whatsapp || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Deskripsi</Text>
<Text
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/>
</Box>
<Box>
<Text fz="lg" fw="bold">Gambar</Text>
{data.image?.link ? (
<Image
src={data.image.link}
alt="gambar"
radius="md"
maw={300}
loading="lazy"
/>
) : (
<Text fz="md" c="dimmed">-</Text>
)}
</Box>
<Group>
<Button
variant="light"
leftSection={<IconBrandWhatsapp size={18} />}
component="a"
href={`https://wa.me/${data.whatsapp.replace(/\D/g, '')}`}
target="_blank"
aria-label="Hubungi WhatsApp"
>
WhatsApp
</Button>
</Group>
</Stack>
</Paper>
</Stack>
</Paper>
</Box>
);
}
export default Page;

View File

@@ -7,6 +7,7 @@ import {
Center,
Grid,
GridCol,
Group,
Image,
Pagination,
Paper,
@@ -17,17 +18,18 @@ import {
TextInput,
Tooltip
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconBrandWhatsapp, IconSearch } from '@tabler/icons-react';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconSearch } from '@tabler/icons-react';
import { useTransitionRouter } from 'next-view-transitions';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto';
import { useDebouncedValue } from '@mantine/hooks';
function Page() {
const state = useProxy(kontakDarurat);
const router = useTransitionRouter()
const [search, setSearch] = useState('');
const [debouncedSearch] = useDebouncedValue(search, 500)
const [debouncedSearch] = useDebouncedValue(search, 1000)
const { data, page, totalPages, loading, load } = state.findMany;
useShallowEffect(() => {
@@ -88,83 +90,79 @@ function Page() {
) : (
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="xl" mt="lg">
{data.map((v, k) => (
<Paper
key={k}
radius="xl"
shadow="md"
withBorder
p="lg"
bg={colors['white-trans-1']}
style={{
transition: 'all 200ms ease',
cursor: 'pointer',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between', // ✅ biar button selalu di bawah
height: '100%', // ✅ bikin tinggi seragam
}}
>
<Stack align="center" gap="sm" style={{ flexGrow: 1 }}>
<Box
style={{
width: '100%',
aspectRatio: '16/9',
borderRadius: '12px',
overflow: 'hidden',
position: 'relative',
}}
>
<Image
src={v.image.link}
alt={v.name}
fit="cover"
loading="lazy"
style={{
width: '100%',
height: '100%',
transition: 'transform 0.4s ease',
}}
onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.05)')}
onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')}
/>
</Box>
<Text ta="center" fw={700} fz="lg" c={colors['blue-button']}>
{v.name}
</Text>
<Text
fz="sm"
ta="center"
lineClamp={3}
lh={1.6}
style={{
minHeight: '4.8em', // tinggi tetap 3 baris
}}
>
<span
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
/>
</Text>
</Stack>
{/* ✅ Tombol selalu di bagian bawah card */}
<Center mt="md">
<Button
variant="light"
leftSection={<IconBrandWhatsapp size={18} />}
component="a"
href={`https://wa.me/${v.whatsapp.replace(/\D/g, '')}`}
target="_blank"
aria-label="Hubungi WhatsApp"
>
WhatsApp
</Button>
</Center>
</Paper>
<Paper
key={k}
radius="xl"
shadow="md"
withBorder
p="lg"
bg={colors['white-trans-1']}
style={{
transition: 'all 200ms ease',
cursor: 'pointer',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between', // ✅ biar button selalu di bawah
height: '100%', // ✅ bikin tinggi seragam
}}
>
<Stack align="center" gap="sm" style={{ flexGrow: 1 }}>
<Box
style={{
width: '100%',
aspectRatio: '16/9',
borderRadius: '12px',
overflow: 'hidden',
position: 'relative',
}}
>
<Image
src={v.image.link}
alt={v.name}
fit="cover"
loading="lazy"
style={{
width: '100%',
height: '100%',
transition: 'transform 0.4s ease',
}}
onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.05)')}
onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')}
/>
</Box>
<Text ta="center" fw={700} fz="lg" c={colors['blue-button']}>
{v.name}
</Text>
<Text
fz="sm"
ta="center"
lineClamp={3}
lh={1.6}
style={{
minHeight: '4.8em', // tinggi tetap 3 baris
}}
>
<span
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
/>
</Text>
</Stack>
{/* ✅ Tombol selalu di bagian bawah card */}
<Group mt="md" justify='center'>
<Button
bg={colors['blue-button']}
onClick={() => router.push(`/darmasaba/kesehatan/kontak-darurat/${v.id}`)}
>
Detail
</Button>
</Group>
</Paper>
))}
</SimpleGrid>
)}

View File

@@ -26,7 +26,7 @@ import BackButton from '../../desa/layanan/_com/BackButto'
function Page() {
const state = useProxy(penangananDarurat)
const [search, setSearch] = useState('')
const [debouncedSearch] = useDebouncedValue(search, 500)
const [debouncedSearch] = useDebouncedValue(search, 1000)
const { data, page, totalPages, loading, load } = state.findMany
useShallowEffect(() => {

View File

@@ -12,7 +12,7 @@ import { useTransitionRouter } from "next-view-transitions";
export default function Page() {
const state = useProxy(posyandustate);
const [search, setSearch] = useState("");
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const router = useTransitionRouter()
const { data, page, totalPages, loading, load } = state.findMany;

View File

@@ -57,7 +57,7 @@ export default function Page() {
const state = useProxy(programKesehatan);
const router = useRouter();
const [search, setSearch] = useState("");
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const { data, page, totalPages, loading, load } = state.findMany;
useShallowEffect(() => {

View File

@@ -7,10 +7,12 @@ import { IconSearch, IconMapPin, IconPhone, IconMail } from '@tabler/icons-react
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto';
import { useDebouncedValue } from '@mantine/hooks';
function Page() {
const state = useProxy(puskesmasState)
const [search, setSearch] = useState('')
const [debouncedSearch] = useDebouncedValue(search, 1000);
const {
data,
@@ -21,8 +23,8 @@ function Page() {
} = state.findMany;
useShallowEffect(() => {
load(page, 6, search)
}, [page, search])
load(page, 6, debouncedSearch)
}, [page, debouncedSearch])
if (loading || !data) {
return (
@@ -95,17 +97,17 @@ function Page() {
</Group>
<Stack gap={6}>
<Group gap="xs" align="flex-start" wrap="nowrap">
<Box pt={2}><IconMapPin size={16} /></Box>
<Box pt={2}><IconMapPin size={20} /></Box>
<Text fz="sm" c="dimmed">{v.alamat}</Text>
</Group>
<Group gap="xs" align="flex-start" wrap="nowrap">
<Box pt={2}><IconPhone size={16} /></Box>
<Box pt={2}><IconPhone size={20} /></Box>
<Text fz="sm" c="dimmed">{v.kontak.kontakPuskesmas}</Text>
</Group>
<Group gap="xs" align="flex-start" wrap="nowrap">
<Box pt={2}><IconMail size={16} /></Box>
<Box pt={2}><IconMail size={20} /></Box>
<Text fz="sm" c="dimmed">{v.kontak.email}</Text>
</Group>
</Stack>

View File

@@ -11,7 +11,7 @@ import BackButton from '../../desa/layanan/_com/BackButto';
function Page() {
const state = useProxy(dataLingkunganDesaState.findMany)
const [search, setSearch] = useState('')
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const {
data,

View File

@@ -49,6 +49,7 @@ export function EdukasiCard({ icon, title, description, color = '#1e88e5' }: Edu
</Stack>
<Text
size="sm"
pl={20}
style={{
wordBreak: 'break-word',
lineHeight: 1.6,

View File

@@ -21,7 +21,7 @@ function Page() {
const state2 = useProxy(pengelolaanSampahState.keteranganSampah)
const [search, setSearch] = useState('')
const [debouncedSearch] = useDebouncedValue(search, 500);
const [debouncedSearch] = useDebouncedValue(search, 1000);
const {
data,

View File

@@ -13,7 +13,7 @@ import { useTransitionRouter } from 'next-view-transitions';
function Page() {
const state = useProxy(programPenghijauanState);
const [search, setSearch] = useState("");
const [debouncedSearch] = useDebouncedValue(search, 500);
const [debouncedSearch] = useDebouncedValue(search, 1000);
const router = useTransitionRouter()
const { data, load, page, totalPages, loading } = state.findMany;

View File

@@ -14,7 +14,7 @@ interface PageProps {
function Page({ params }: PageProps) {
const [search, setSearch] = useState("")
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const stateList = useProxy(infoSekolahPaud.lembagaPendidikan)
const { jenjangPendidikan } = use(params);

View File

@@ -14,7 +14,7 @@ interface PageProps {
function Page({ params }: PageProps) {
const [search, setSearch] = useState("")
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const stateList = useProxy(infoSekolahPaud.pengajar)
const { jenjangPendidikan } = use(params);

View File

@@ -14,7 +14,7 @@ interface PageProps {
function Page({ params }: PageProps) {
const [search, setSearch] = useState("")
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const stateList = useProxy(infoSekolahPaud.siswa)
const { jenjangPendidikan } = use(params);

View File

@@ -9,7 +9,7 @@ import { useProxy } from 'valtio/utils';
function Page() {
const [search, setSearch] = useState("")
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const stateList = useProxy(infoSekolahPaud.lembagaPendidikan)
const {

View File

@@ -10,7 +10,7 @@ import { useProxy } from 'valtio/utils';
function Page() {
const stateList = useProxy(infoSekolahPaud.pengajar)
const [search, setSearch] = useState("")
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const {
data,
page,

View File

@@ -9,7 +9,7 @@ import { useState } from 'react';
function Page() {
const [search, setSearch] = useState("")
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const stateList = useProxy(infoSekolahPaud.siswa)
const {

View File

@@ -12,6 +12,7 @@ import {
Skeleton,
Stack,
Text,
Title,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -41,9 +42,9 @@ export default function DetailInformasiPublikUser() {
return (
<Center py="xl">
<Stack align="center" gap="sm">
<Text fz="lg" fw="bold">
<Title order={4} fz={{ base: 'lg', md: 'xl' }} lh={1.3}>
Informasi tidak ditemukan
</Text>
</Title>
<Button variant="light" onClick={() => router.push('/informasi-publik')}>
Kembali ke Daftar
</Button>
@@ -75,55 +76,66 @@ export default function DetailInformasiPublikUser() {
shadow="xs"
>
<Stack gap="xl">
<Text
fz={{ base: 'xl', md: '2xl' }}
fw="bold"
{/* MAIN TITLE */}
<Title
order={2}
lh={1.2}
ta="center"
c={colors['blue-button']}
>
Detail Informasi Publik
</Text>
</Title>
<Divider />
{/* CONTENT */}
<Stack gap="lg">
<Box>
<Text fz={{ base: 'md', md: 'lg' }} fw="bold" mb={4}>
{/* Jenis Informasi */}
<Box px="lg">
<Title order={5} fz={{ base: 'lg', md: 'xl' }} lh={1.3} mb={4}>
Jenis Informasi
</Text>
<Text fz={{ base: 'sm', md: 'md' }} c="dimmed">
</Title>
<Text fz={{ base: 'sm', md: 'md' }} lh={1.5} c="black">
{data.jenisInformasi || '-'}
</Text>
</Box>
<Box>
<Text fz={{ base: 'md', md: 'lg' }} fw="bold" mb={4}>
{/* Tanggal Publikasi */}
<Box px="lg">
<Title order={5} fz={{ base: 'lg', md: 'xl' }} lh={1.3} mb={4}>
Tanggal Publikasi
</Text>
<Text fz={{ base: 'sm', md: 'md' }} c="dimmed">
</Title>
<Text fz={{ base: 'sm', md: 'md' }} lh={1.5} c="black">
{data.tanggal
? new Date(data.tanggal).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric',
})
day: '2-digit',
month: 'long',
year: 'numeric',
})
: '-'}
</Text>
</Box>
<Box>
<Text fz={{ base: 'md', md: 'lg' }} fw="bold" mb={4}>
{/* Deskripsi */}
<Box px="lg">
<Title order={5} fz={{ base: 'lg', md: 'xl' }} lh={1.3} mb={4}>
Deskripsi
</Text>
<Box
className="prose max-w-none leading-relaxed"
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
style={{wordBreak: "break-word", whiteSpace: "normal"}}
/>
</Title>
<Box>
<Text
ta="justify"
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
fz={{ base: 'sm', md: 'md' }}
lh={1.6}
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
className="prose max-w-none"
/>
</Box>
</Box>
</Stack>
</Stack>
</Paper>
</Box>
);
}
}

View File

@@ -21,7 +21,8 @@ import {
TableTr,
Text,
TextInput,
Tooltip
Tooltip,
Title
} from '@mantine/core';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconBrandWhatsapp, IconDeviceImacCog, IconFileInfo, IconMail, IconSearch } from '@tabler/icons-react';
@@ -33,7 +34,7 @@ import { useTransitionRouter } from 'next-view-transitions';
function Page() {
const listData = useProxy(daftarInformasiPublik)
const [search, setSearch] = useState('')
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const [debouncedSearch] = useDebouncedValue(search, 1000); // 1000ms delay
const router = useTransitionRouter()
const {
data,
@@ -65,20 +66,49 @@ function Page() {
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Center>
<Image src="/darmasaba-icon.png" w={{ base: 70, md: 100 }} alt="Logo Desa Darmasaba" loading="lazy" />
<Image
src="/darmasaba-icon.png"
w={{ base: 70, md: 100 }}
alt="Logo Desa Darmasaba"
loading="lazy"
/>
</Center>
<Text ta="center" fz={{ base: "1.8rem", md: "2.5rem" }} c={colors["blue-button"]} fw="bold" lh={1.4}>
<Title
order={2}
ta="center"
fz={{ base: '1.6rem', md: '2.4rem' }}
c={colors['blue-button']}
lh={1.35}
style={{ fontWeight: 700 }}
>
Daftar Informasi Publik
</Text>
<Box px={{ base: "md", md: 100 }}>
</Title>
<Box px={{ base: 'md', md: 100 }}>
<Stack gap="lg">
<Paper p="lg" radius="xl" shadow="sm" withBorder>
<Stack gap="sm">
<Text ta={"center"} fz={{ base: 'lg', md: 'xl' }} fw="bold" c={colors["blue-button"]}>
<Title
order={4}
ta="center"
fz={{ base: 'lg', md: 'xl' }}
c={colors['blue-button']}
lh={1.2}
style={{ fontWeight: 700 }}
>
Tentang Informasi Publik
</Text>
<Text ta={"center"} fz={{ base: 'sm', md: 'md' }} c="dimmed">
</Title>
<Text
ta="center"
fz={{ base: 'sm', md: 'md' }}
c="black"
lh={1.6}
style={{ maxWidth: 900, margin: '0 auto' }}
>
Daftar Informasi Publik Desa Darmasaba adalah kumpulan data yang dapat diakses oleh masyarakat sesuai dengan ketentuan peraturan yang berlaku.
</Text>
</Stack>
@@ -97,8 +127,8 @@ function Page() {
{data.length === 0 ? (
<Center py="xl">
<Stack align="center" gap="sm">
<IconFileInfo size={48} stroke={1.5} color={colors["blue-button"]} />
<Text fz="md" c="dimmed">Tidak ada informasi publik yang ditemukan.</Text>
<IconFileInfo size={48} stroke={1.5} color={colors['blue-button']} />
<Text fz="md" c="dimmed" lh={1.5}>Tidak ada informasi publik yang ditemukan.</Text>
</Stack>
</Center>
) : (
@@ -113,27 +143,42 @@ function Page() {
<TableTh fz="sm" ta="center" w="15%">Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody bg={colors['white-1']}>
{data.map((item, index) => (
<TableTr key={item.id}>
<TableTd ta="center">{(page - 1) * 5 + index + 1}</TableTd>
<TableTd ta="center">
<Text fz="sm" lh={1.4}>
{(page - 1) * 5 + index + 1}
</Text>
</TableTd>
<TableTd>
<Box>
<Badge variant="light" size="lg" color="blue">
<Text fw={650} fz={"sm"} c={'blue'} lineClamp={1}>
<Text fw={650} fz="sm" c="blue" lineClamp={1} lh={1.2}>
{item.jenisInformasi}
</Text>
</Badge>
</Box>
</TableTd>
<TableTd>
<Box>
<Text lineClamp={1} fz="sm" c="dark" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
<Text
lineClamp={1}
fz={{ base: 'sm', md: 'md' }}
c="dark"
lh={1.5}
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
/>
</Box>
</TableTd>
<TableTd ta="center">
<Box>
<Text ta={"center"}>
<Text ta="center" fz="sm" lh={1.4}>
{item.tanggal ? new Date(item.tanggal).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
@@ -142,6 +187,7 @@ function Page() {
</Text>
</Box>
</TableTd>
<TableTd style={{ textAlign: 'center' }}>
<Box>
<Tooltip label="Lihat Detail" withArrow>
@@ -152,8 +198,9 @@ function Page() {
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() => router.push(`/darmasaba/ppid/daftar-informasi-publik/${item.id}`)}
aria-label={`Detail ${item.jenisInformasi}`}
>
Detail
<Text fz="xs" lh={1.2}>Detail</Text>
</Button>
</Tooltip>
</Box>
@@ -178,17 +225,27 @@ function Page() {
<Paper p="lg" radius="xl" shadow="xs" withBorder>
<Stack gap="xs">
<Text fz="lg" fw="bold" c={colors["blue-button"]}>Kontak PPID</Text>
<Title order={5} fz={{ base: 'lg', md: 'xl' }} fw="bold" c={colors['blue-button']} lh={1.2}>
Kontak PPID
</Title>
<Group>
<IconMail color='gray' size={16} style={{ marginRight: 6 }} />
<Text c={"dimmed"} fz="sm" lh={1.6}>
Email: <Text c={"dimmed"} span fw="500">ppid@desadarmasaba.id</Text>
<IconMail color="gray" size={16} style={{ marginRight: 6 }} />
<Text c="dimmed" fz="sm" lh={1.6}>
Email:{' '}
<Text c="dimmed" span fw={500} fz="sm" lh={1.6}>
ppid@desadarmasaba.id
</Text>
</Text>
</Group>
<Group>
<IconBrandWhatsapp color='gray' size={16} style={{ marginRight: 6 }} />
<Text c={"dimmed"} fz="sm" lh={1.6}>
WhatsApp: <Text c={"dimmed"} span fw="500">081-xxx-xxx-xxx</Text>
<IconBrandWhatsapp color="gray" size={16} style={{ marginRight: 6 }} />
<Text c="dimmed" fz="sm" lh={1.6}>
WhatsApp:{' '}
<Text c="dimmed" span fw={500} fz="sm" lh={1.6}>
081-xxx-xxx-xxx
</Text>
</Text>
</Group>
</Stack>

View File

@@ -1,7 +1,7 @@
'use client'
import stateDasarHukum from '@/app/admin/(dashboard)/_state/ppid/dasar_hukum/dasarHukum';
import colors from '@/con/colors';
import { Box, Paper, Skeleton, Stack, Text, Transition } from '@mantine/core';
import { Box, Paper, Skeleton, Stack, Text, Transition, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useProxy } from 'valtio/utils';
import { IconBook2 } from '@tabler/icons-react';
@@ -31,27 +31,39 @@ function Page() {
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Stack align="center" gap="xs">
{/* HEADER */}
<Stack align="center" gap="xs" px={{ base: 'md', md: 100 }}>
<IconBook2 size={42} stroke={1.5} color={colors["blue-button"]} />
<Text
<Title
order={1}
ta="center"
fz={{ base: "2rem", md: "2.5rem" }}
c={colors["blue-button"]}
fw="bold"
fz={{ base: "1.8rem", md: "2.3rem" }}
lh={1.2}
style={{ letterSpacing: "-0.5px" }}
>
Dasar Hukum
</Text>
<Text ta="center" fz="md" >
</Title>
<Text
ta="center"
fz={{ base: "sm", md: "md" }}
lh={1.6}
c="black"
>
Informasi regulasi dan kebijakan resmi yang menjadi dasar hukum
</Text>
</Stack>
{/* CONTENT */}
<Box px={{ base: "md", md: 100 }}>
<Stack gap="lg">
{dataArray.map((item, k) => (
<Transition
key={k}
mounted={true}
mounted
transition="fade-up"
duration={400}
timingFunction="ease"
@@ -69,16 +81,27 @@ function Page() {
}}
>
<Stack gap="md">
<Text
{/* JUDUL */}
<Title
order={3}
ta="center"
fw="bold"
fz={{ base: 'lg', md: 'xl' }}
style={{ lineHeight: 1.4 }}
c="black"
fz={{ base: "lg", md: "xl" }}
lh={1.3}
dangerouslySetInnerHTML={{ __html: item.judul }}
/>
{/* CONTENT */}
<Text
fz={{ base: 'sm', md: 'md' }}
style={{ lineHeight: 1.7, wordBreak: "break-word", whiteSpace: "normal" }}
c="black"
ta="justify"
fz={{ base: "sm", md: "md" }}
lh={1.7}
style={{
wordBreak: "break-word",
whiteSpace: "normal",
}}
dangerouslySetInnerHTML={{ __html: item.content }}
/>
</Stack>

View File

@@ -3,7 +3,22 @@
import indeksKepuasanState from "@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan";
import colors from "@/con/colors";
import { BarChart, PieChart } from '@mantine/charts';
import { Box, Button, Center, Container, Flex, Modal, Paper, Select, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from "@mantine/core";
import {
Box,
Button,
Center,
Container,
Flex,
Modal,
Paper,
Select,
SimpleGrid,
Skeleton,
Stack,
Text,
TextInput,
Title
} from "@mantine/core";
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
import { useState } from "react";
import { useProxy } from "valtio/utils";
@@ -15,16 +30,14 @@ interface ChartDataItem {
label?: string;
}
function Kepuasan() {
const state = useProxy(indeksKepuasanState.responden);
const state = useProxy(indeksKepuasanState.responden);
const { data, loading } = state.findMany;
const [donutDataJenisKelamin, setDonutDataJenisKelamin] = useState<ChartDataItem[]>([]);
const [donutDataRating, setDonutDataRating] = useState<ChartDataItem[]>([]);
const [donutDataKelompokUmur, setDonutDataKelompokUmur] = useState<ChartDataItem[]>([]);
const [barChartData, setBarChartData] = useState<Array<{ month: string; Responden: number }>>([]);
const [opened, { open, close }] = useDisclosure(false)
const [opened, { open, close }] = useDisclosure(false);
const resetForm = () => {
state.create.form = {
@@ -34,14 +47,14 @@ const state = useProxy(indeksKepuasanState.responden);
jenisKelaminId: "",
ratingId: "",
kelompokUmurId: "",
}
}
};
};
useShallowEffect(() => {
indeksKepuasanState.jenisKelaminResponden.findMany.load()
indeksKepuasanState.pilihanRatingResponden.findMany.load()
indeksKepuasanState.kelompokUmurResponden.findMany.load()
},[])
indeksKepuasanState.jenisKelaminResponden.findMany.load();
indeksKepuasanState.pilihanRatingResponden.findMany.load();
indeksKepuasanState.kelompokUmurResponden.findMany.load();
}, []);
const handleSubmit = async () => {
try {
@@ -51,11 +64,11 @@ const state = useProxy(indeksKepuasanState.responden);
await state.findUnique.load(idStr);
}
resetForm();
close()
close();
} catch (error) {
console.error('Error submitting form:', error);
}
}
};
// Load data on component mount
useShallowEffect(() => {
@@ -82,13 +95,13 @@ const state = useProxy(indeksKepuasanState.responden);
// Update gender chart data
setDonutDataJenisKelamin([
{ name: 'Laki-laki', value: totalLaki, color: colors['blue-button'] },
{ name: 'Laki-laki', value: totalLaki, color: '#52ABE3FF' },
{ name: 'Perempuan', value: totalPerempuan, color: '#10A85AFF' },
]);
// Update rating chart data
setDonutDataRating([
{ name: 'Sangat Baik', value: totalSangatBaik, color: colors['blue-button'] },
{ name: 'Sangat Baik', value: totalSangatBaik, color: '#52ABE3FF' },
{ name: 'Baik', value: totalBaik, color: '#10A85AFF' },
{ name: 'Kurang Baik', value: totalKurangBaik, color: '#FFA500' },
{ name: 'Sangat Kurang Baik', value: totalSangatKurangBaik, color: '#FF4500' },
@@ -96,7 +109,7 @@ const state = useProxy(indeksKepuasanState.responden);
// Update age group chart data
setDonutDataKelompokUmur([
{ name: 'Muda', value: totalMuda, color: colors['blue-button'] },
{ name: 'Muda', value: totalMuda, color: '#52ABE3FF' },
{ name: 'Dewasa', value: totalDewasa, color: '#10A85AFF' },
{ name: 'Lansia', value: totalLansia, color: '#FFA500' },
]);
@@ -154,33 +167,52 @@ const state = useProxy(indeksKepuasanState.responden);
if (data.length === 0) {
return (
<Stack p="sm">
<Container w={{ base: "100%", md: "80%" }} p={"xl"}>
<Container w={{ base: "100%", md: "80%" }} p="xl">
<Center>
<Text fw={"bold"} fz={{ base: "1.8rem", md: "3.4rem" }}>Indeks Kepuasan Masyarakat</Text>
{/* Main page title — converted to Title, use order (don't set fz according to rules) */}
<Title order={2} ta="center" c="dark">
Indeks Kepuasan Masyarakat
</Title>
</Center>
<Text ta={"center"} fz={{ base: "1rem", md: "1.3rem" }}>Ukur kebahagiaan warga, tingkatkan layanan desa! Dengan partisipasi aktif masyarakat, kami berkomitmen untuk terus memperbaiki layanan agar lebih transparan, efektif, dan sesuai dengan kebutuhan warga. Kepuasan Anda adalah prioritas utama kami dalam membangun desa yang lebih baik!</Text>
{/* Body lead text — responsive fz & lh */}
<Text ta="center" fz={{ base: "1rem", md: "1.25rem" }} lh={{ base: 1.4, md: 1.6 }} c="dimmed" mt="sm">
Ukur kebahagiaan warga, tingkatkan layanan desa! Dengan partisipasi aktif masyarakat, kami berkomitmen untuk terus memperbaiki layanan agar lebih transparan, efektif, dan sesuai dengan kebutuhan warga. Kepuasan Anda adalah prioritas utama kami dalam membangun desa yang lebih baik!
</Text>
<Center mt={10}>
<Button
radius={"lg"}
radius="lg"
onClick={open}
variant="gradient"
gradient={{ from: "#26667F", to: "#124170" }}
>Ajukan Responden</Button>
>
Ajukan Responden
</Button>
</Center>
</Container>
<Box px={"xl"}>
<Paper p={"lg"} bg={colors.Bg}>
<Paper p={"lg"}>
<Stack gap={"xs"}>
<Flex justify={"space-between"} align={"center"}>
<Text fw={"bold"}>Pelayanan Terhadap Publik Desa Darmasaba</Text>
<Box px="xl">
<Paper p="lg" bg={colors.Bg}>
<Paper p="lg">
<Stack gap="xs">
<Flex justify="space-between" align="center">
{/* Section heading — use Title order for hierarchy */}
<Title order={4}>
Pelayanan Terhadap Publik Desa Darmasaba
</Title>
<Box>
<Text fz={"sm"} fw={"bold"} c={colors["blue-button"]}>Total Responden</Text>
<Text ta={"end"} fz={"h1"} fw={"bold"} c={colors["blue-button"]}>
{state.findMany.total.toLocaleString('id-ID')}
<Text fz={{ base: "0.9rem", md: "1rem" }} fw="bold" c={colors["blue-button"]}>
Total Responden
</Text>
{/* Big number — use Title for emphasis */}
<Title order={3} ta="end" c={colors["blue-button"]} fw="bold" mt="xs">
{state.findMany.total.toLocaleString('id-ID')}
</Title>
</Box>
</Flex>
<BarChart
h={window.innerWidth < 480 ? 200 : 300}
data={barChartData}
@@ -194,18 +226,16 @@ const state = useProxy(indeksKepuasanState.responden);
/>
</Stack>
</Paper>
<Box py={"xl"}>
<SimpleGrid
cols={{ base: 1, sm: 2, lg: 3 }}
spacing="md"
verticalSpacing="md"
>
<Box py="xl">
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md" verticalSpacing="md">
{/* Chart Jenis Kelamin */}
<Paper bg={colors['white-1']} p="md" radius="md">
<Stack>
<Title order={4}>Jenis Kelamin</Title>
<Title order={5}>Jenis Kelamin</Title>
{donutDataJenisKelamin.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md">
<Text c="dimmed" ta="center" my="md" fz={{ base: "0.95rem", md: "1rem" }}>
Belum ada data untuk ditampilkan dalam grafik
</Text>
) : (
@@ -216,8 +246,9 @@ const state = useProxy(indeksKepuasanState.responden);
<PieChart
withLabels
withTooltip
labelsPosition="inside"
labelsType="percent"
size={250} // Fixed size in pixels
size={250}
data={donutDataJenisKelamin}
/>
</Center>
@@ -226,7 +257,7 @@ const state = useProxy(indeksKepuasanState.responden);
{donutDataJenisKelamin.map((entry) => (
<Flex key={entry.name} gap="md" align="center">
<Box bg={entry.color} w={20} h={20} style={{ flexShrink: 0 }} />
<Text size="sm">{entry.name}: {entry.value}</Text>
<Text fz={{ base: "0.95rem", md: "1rem" }}>{entry.name}: {entry.value}</Text>
</Flex>
))}
</Stack>
@@ -239,9 +270,10 @@ const state = useProxy(indeksKepuasanState.responden);
{/* Chart Rating */}
<Paper bg={colors['white-1']} p="md" radius="md">
<Stack>
<Title order={4}>Ulasan</Title>
<Title order={5}>Ulasan</Title>
{donutDataRating.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md">
<Text c="dimmed" ta="center" my="md" fz={{ base: "0.95rem", md: "1rem" }}>
Belum ada data untuk ditampilkan dalam grafik
</Text>
) : (
@@ -253,7 +285,7 @@ const state = useProxy(indeksKepuasanState.responden);
withTooltip
tooltipAnimationDuration={200}
withLabels
labelsPosition="outside"
labelsPosition="inside"
labelsType="percent"
withLabelsLine
size={250}
@@ -266,7 +298,7 @@ const state = useProxy(indeksKepuasanState.responden);
{donutDataRating.map((entry) => (
<Flex key={entry.name} gap="sm" align="center" style={{ overflow: 'hidden' }}>
<Box bg={entry.color} w={16} h={16} style={{ flexShrink: 0 }} />
<Text size="xs" lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text fz={{ base: "0.85rem", md: "0.95rem" }} lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{entry.name}: {entry.value}
</Text>
</Flex>
@@ -282,9 +314,10 @@ const state = useProxy(indeksKepuasanState.responden);
{/* Chart Kelompok Umur */}
<Paper bg={colors['white-1']} p="md" radius="md">
<Stack>
<Title order={4}>Umur</Title>
<Title order={5}>Umur</Title>
{donutDataKelompokUmur.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md">
<Text c="dimmed" ta="center" my="md" fz={{ base: "0.95rem", md: "1rem" }}>
Belum ada data untuk ditampilkan dalam grafik
</Text>
) : (
@@ -296,7 +329,7 @@ const state = useProxy(indeksKepuasanState.responden);
withTooltip
tooltipAnimationDuration={200}
withLabels
labelsPosition="outside"
labelsPosition="inside"
labelsType="percent"
withLabelsLine
size={250}
@@ -309,7 +342,7 @@ const state = useProxy(indeksKepuasanState.responden);
{donutDataKelompokUmur.map((entry) => (
<Flex key={entry.name} gap="sm" align="center" style={{ overflow: 'hidden' }}>
<Box bg={entry.color} w={16} h={16} style={{ flexShrink: 0 }} />
<Text size="xs" lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text fz={{ base: "0.85rem", md: "0.95rem" }} lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{entry.name}: {entry.value}
</Text>
</Flex>
@@ -325,18 +358,21 @@ const state = useProxy(indeksKepuasanState.responden);
</Box>
</Paper>
</Box>
{/* Modal */}
<Modal opened={opened} onClose={close} title="Ajukan Responden" centered>
<Paper bg={colors['white-1']} p={'md'}>
<Paper bg={colors['white-1']} p="md">
<Stack>
<TextInput
label="Nama"
type='text'
type="text"
placeholder="Masukkan nama"
value={state.create.form.name}
onChange={(val) => {
state.create.form.name = val.currentTarget.value;
}}
// label typography
labelProps={{ style: { fontSize: '0.95rem', lineHeight: '1.4' } } as any}
/>
<TextInput
label="Tanggal"
@@ -346,10 +382,11 @@ const state = useProxy(indeksKepuasanState.responden);
onChange={(val) => {
state.create.form.tanggal = val.currentTarget.value;
}}
labelProps={{ style: { fontSize: '0.95rem', lineHeight: '1.4' } } as any}
/>
<Select
key={"jenisKelamin"}
label={"Jenis Kelamin"}
key="jenisKelamin"
label="Jenis Kelamin"
placeholder={indeksKepuasanState.jenisKelaminResponden.findMany.loading ? 'Memuat...' : 'Pilih jenis kelamin'}
value={state.create.form.jenisKelaminId || ""}
onChange={(val) => {
@@ -357,17 +394,19 @@ const state = useProxy(indeksKepuasanState.responden);
}}
data={
(indeksKepuasanState.jenisKelaminResponden.findMany.data || [])
.filter(Boolean) // Hapus null, undefined, dll
.filter(Boolean)
.map((item) => ({
value: item.id,
label: item.name || 'Tanpa Nama',
}))
}
disabled={indeksKepuasanState.jenisKelaminResponden.findMany.loading}
// label typography
labelProps={{ style: { fontSize: '0.95rem', lineHeight: '1.4' } } as any}
/>
<Select
key={"rating_responden"}
label={"Rating"}
key="rating_responden"
label="Rating"
placeholder={indeksKepuasanState.pilihanRatingResponden.findMany.loading ? 'Memuat...' : 'Pilih rating'}
value={state.create.form.ratingId || ""}
onChange={(val) => {
@@ -375,17 +414,18 @@ const state = useProxy(indeksKepuasanState.responden);
}}
data={
(indeksKepuasanState.pilihanRatingResponden.findMany.data || [])
.filter(Boolean) // Hapus null, undefined, dll
.filter(Boolean)
.map((item) => ({
value: item.id,
label: item.name || 'Tanpa Nama',
}))
}
disabled={indeksKepuasanState.pilihanRatingResponden.findMany.loading}
labelProps={{ style: { fontSize: '0.95rem', lineHeight: '1.4' } } as any}
/>
<Select
key={"kelompokUmur"}
label={"Kelompok Umur"}
key="kelompokUmur"
label="Kelompok Umur"
placeholder={indeksKepuasanState.kelompokUmurResponden.findMany.loading ? 'Memuat...' : 'Pilih kelompok umur'}
value={state.create.form.kelompokUmurId || ""}
onChange={(val) => {
@@ -393,19 +433,16 @@ const state = useProxy(indeksKepuasanState.responden);
}}
data={
(indeksKepuasanState.kelompokUmurResponden.findMany.data || [])
.filter(Boolean) // Hapus null, undefined, dll
.filter(Boolean)
.map((item) => ({
value: item.id,
label: item.name || 'Tanpa Nama',
}))
}
disabled={indeksKepuasanState.kelompokUmurResponden.findMany.loading}
labelProps={{ style: { fontSize: '0.95rem', lineHeight: '1.4' } } as any}
/>
<Button
mt={10}
bg={colors['blue-button']}
onClick={handleSubmit}
>
<Button mt={10} bg={colors['blue-button']} onClick={handleSubmit}>
Submit
</Button>
</Stack>
@@ -414,36 +451,47 @@ const state = useProxy(indeksKepuasanState.responden);
</Stack>
);
}
return (
<Stack p={"sm"}>
<Container size="lg" px="md">
<Stack p="sm">
<Container size="lg" px="md">
<Center>
<Text ta={"center"} fz={{ base: "2.4rem", md: "3.4rem" }}>Indeks Kepuasan Masyarakat</Text>
{/* Main page title — Title with order */}
<Title order={2} ta="center" c="dark">
Indeks Kepuasan Masyarakat
</Title>
</Center>
<Text fz={{ base: "1.2rem", md: "1.4rem" }} ta={"center"}>Ukur kebahagiaan warga, tingkatkan layanan desa! Dengan partisipasi aktif masyarakat, kami berkomitmen untuk terus memperbaiki layanan agar lebih transparan, efektif, dan sesuai dengan kebutuhan warga. Kepuasan Anda adalah prioritas utama kami dalam membangun desa yang lebih baik!</Text>
<Text fz={{ base: "1rem", md: "1.125rem" }} lh={{ base: 1.4, md: 1.6 }} ta="center" c="dimmed" mt="sm">
Ukur kebahagiaan warga, tingkatkan layanan desa! Dengan partisipasi aktif masyarakat, kami berkomitmen untuk terus memperbaiki layanan agar lebih transparan, efektif, dan sesuai dengan kebutuhan warga. Kepuasan Anda adalah prioritas utama kami dalam membangun desa yang lebih baik!
</Text>
<Center mt={10}>
<Button radius={"lg"} bg={colors["blue-button"]} onClick={open}>Ajukan Responden</Button>
<Button radius="lg" bg={colors["blue-button"]} onClick={open}>Ajukan Responden</Button>
</Center>
</Container>
<Box px={"xl"}>
<Paper p={"lg"} bg={colors.Bg}>
<Paper p={"lg"}>
<Stack gap={"xs"}>
<Box px="xl">
<Paper p="lg" bg={colors.Bg}>
<Paper p="lg">
<Stack gap="xs">
<Flex
direction={{ base: "column", sm: "row" }}
justify="space-between"
align={{ base: "flex-start", sm: "center" }}
>
<Text fw="bold" ta={{ base: "center", sm: "left" }}>
<Title order={4} ta={{ base: "center", sm: "left" }}>
Pelayanan Terhadap Publik Desa Darmasaba
</Text>
</Title>
<Box mt={{ base: "sm", sm: 0 }}>
<Text fz={"sm"} fw={"bold"} c={colors["blue-button"]}>Total Responden</Text>
<Text ta={"end"} fz={"h1"} fw={"bold"} c={colors["blue-button"]}>
<Text fz={{ base: "0.9rem", md: "1rem" }} fw="bold" c={colors["blue-button"]}>Total Responden</Text>
<Title order={3} ta="end" c={colors["blue-button"]} fw="bold" mt="xs">
{state.findMany.total.toLocaleString('id-ID')}
</Text>
</Title>
</Box>
</Flex>
<BarChart
h={300}
data={barChartData}
@@ -457,21 +505,18 @@ const state = useProxy(indeksKepuasanState.responden);
/>
</Stack>
</Paper>
<Box py={"xl"}>
<Box py="xl">
<SimpleGrid
cols={{
base: 1,
md: 1,
lg: 1,
xl: 3
}}
cols={{ base: 1, md: 1, lg: 1, xl: 3 }}
>
{/* Chart Jenis Kelamin */}
<Paper bg={colors['white-1']} p="md" radius="md">
<Stack>
<Title order={4}>Jenis Kelamin</Title>
<Title order={5}>Jenis Kelamin</Title>
{donutDataJenisKelamin.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md">
<Text c="dimmed" ta="center" my="md" fz={{ base: "0.95rem", md: "1rem" }}>
Belum ada data untuk ditampilkan dalam grafik
</Text>
) : (
@@ -482,6 +527,7 @@ const state = useProxy(indeksKepuasanState.responden);
<PieChart
withLabels
withTooltip
labelsPosition="inside"
labelsType="percent"
size={200}
data={donutDataJenisKelamin}
@@ -492,7 +538,7 @@ const state = useProxy(indeksKepuasanState.responden);
{donutDataJenisKelamin.map((entry) => (
<Flex key={entry.name} gap="md" align="center">
<Box bg={entry.color} w={20} h={20} style={{ flexShrink: 0 }} />
<Text size="sm">{entry.name}: {entry.value}</Text>
<Text fz={{ base: "0.95rem", md: "1rem" }}>{entry.name}: {entry.value}</Text>
</Flex>
))}
</Stack>
@@ -505,9 +551,10 @@ const state = useProxy(indeksKepuasanState.responden);
{/* Chart Rating */}
<Paper bg={colors['white-1']} p="md" radius="md">
<Stack>
<Title order={4}>Ulasan</Title>
<Title order={5}>Ulasan</Title>
{donutDataRating.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md">
<Text c="dimmed" ta="center" my="md" fz={{ base: "0.95rem", md: "1rem" }}>
Belum ada data untuk ditampilkan dalam grafik
</Text>
) : (
@@ -519,7 +566,7 @@ const state = useProxy(indeksKepuasanState.responden);
withTooltip
tooltipAnimationDuration={200}
withLabels
labelsPosition="outside"
labelsPosition="inside"
labelsType="percent"
withLabelsLine
size={200}
@@ -532,7 +579,7 @@ const state = useProxy(indeksKepuasanState.responden);
{donutDataRating.map((entry) => (
<Flex key={entry.name} gap="sm" align="center" style={{ overflow: 'hidden' }}>
<Box bg={entry.color} w={16} h={16} style={{ flexShrink: 0 }} />
<Text size="xs" lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text fz={{ base: "0.85rem", md: "0.95rem" }} lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{entry.name}: {entry.value}
</Text>
</Flex>
@@ -548,9 +595,10 @@ const state = useProxy(indeksKepuasanState.responden);
{/* Chart Kelompok Umur */}
<Paper bg={colors['white-1']} p="md" radius="md">
<Stack>
<Title order={4}>Umur</Title>
<Title order={5}>Umur</Title>
{donutDataKelompokUmur.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md">
<Text c="dimmed" ta="center" my="md" fz={{ base: "0.95rem", md: "1rem" }}>
Belum ada data untuk ditampilkan dalam grafik
</Text>
) : (
@@ -562,7 +610,7 @@ const state = useProxy(indeksKepuasanState.responden);
withTooltip
tooltipAnimationDuration={200}
withLabels
labelsPosition="outside"
labelsPosition="inside"
labelsType="percent"
withLabelsLine
size={190}
@@ -575,7 +623,7 @@ const state = useProxy(indeksKepuasanState.responden);
{donutDataKelompokUmur.map((entry) => (
<Flex key={entry.name} gap="sm" align="center" style={{ overflow: 'hidden' }}>
<Box bg={entry.color} w={16} h={16} style={{ flexShrink: 0 }} />
<Text size="xs" lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text fz={{ base: "0.85rem", md: "0.95rem" }} lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{entry.name}: {entry.value}
</Text>
</Flex>
@@ -591,31 +639,34 @@ const state = useProxy(indeksKepuasanState.responden);
</Box>
</Paper>
</Box>
{/* Modal */}
<Modal opened={opened} onClose={close} title="Ajukan Responden" centered>
<Paper bg={colors['white-1']} p={'md'}>
<Paper bg={colors['white-1']} p="md">
<Stack>
<TextInput
label="Nama"
type='text'
placeholder="masukkan nama"
type="text"
placeholder="Masukkan nama"
value={state.create.form.name}
onChange={(val) => {
state.create.form.name = val.currentTarget.value;
}}
labelProps={{ style: { fontSize: '0.95rem', lineHeight: '1.4' } } as any}
/>
<TextInput
label="Tanggal Pengisian"
type="date"
placeholder="masukkan tanggal"
placeholder="Masukkan tanggal"
value={state.create.form.tanggal}
onChange={(val) => {
state.create.form.tanggal = val.currentTarget.value;
}}
labelProps={{ style: { fontSize: '0.95rem', lineHeight: '1.4' } } as any}
/>
<Select
key={"jenisKelamin"}
label={"Jenis Kelamin"}
key="jenisKelamin"
label="Jenis Kelamin"
placeholder={indeksKepuasanState.jenisKelaminResponden.findMany.loading ? 'Memuat...' : 'Pilih jenis kelamin'}
value={state.create.form.jenisKelaminId || ""}
onChange={(val) => {
@@ -623,17 +674,18 @@ const state = useProxy(indeksKepuasanState.responden);
}}
data={
(indeksKepuasanState.jenisKelaminResponden.findMany.data || [])
.filter(Boolean) // Hapus null, undefined, dll
.filter(Boolean)
.map((item) => ({
value: item.id,
label: item.name || 'Tanpa Nama',
}))
}
disabled={indeksKepuasanState.jenisKelaminResponden.findMany.loading}
labelProps={{ style: { fontSize: '0.95rem', lineHeight: '1.4' } } as any}
/>
<Select
key={"rating_responden"}
label={"Rating"}
key="rating_responden"
label="Rating"
placeholder={indeksKepuasanState.pilihanRatingResponden.findMany.loading ? 'Memuat...' : 'Pilih rating'}
value={state.create.form.ratingId || ""}
onChange={(val) => {
@@ -641,17 +693,18 @@ const state = useProxy(indeksKepuasanState.responden);
}}
data={
(indeksKepuasanState.pilihanRatingResponden.findMany.data || [])
.filter(Boolean) // Hapus null, undefined, dll
.filter(Boolean)
.map((item) => ({
value: item.id,
label: item.name || 'Tanpa Nama',
}))
}
disabled={indeksKepuasanState.pilihanRatingResponden.findMany.loading}
labelProps={{ style: { fontSize: '0.95rem', lineHeight: '1.4' } } as any}
/>
<Select
key={"kelompokUmur"}
label={"Kelompok Umur"}
key="kelompokUmur"
label="Kelompok Umur"
placeholder={indeksKepuasanState.kelompokUmurResponden.findMany.loading ? 'Memuat...' : 'Pilih kelompok umur'}
value={state.create.form.kelompokUmurId || ""}
onChange={(val) => {
@@ -659,19 +712,16 @@ const state = useProxy(indeksKepuasanState.responden);
}}
data={
(indeksKepuasanState.kelompokUmurResponden.findMany.data || [])
.filter(Boolean) // Hapus null, undefined, dll
.filter(Boolean)
.map((item) => ({
value: item.id,
label: item.name || 'Tanpa Nama',
}))
}
disabled={indeksKepuasanState.kelompokUmurResponden.findMany.loading}
labelProps={{ style: { fontSize: '0.95rem', lineHeight: '1.4' } } as any}
/>
<Button
mt={10}
bg={colors['blue-button']}
onClick={handleSubmit}
>
<Button mt={10} bg={colors['blue-button']} onClick={handleSubmit}>
Submit
</Button>
</Stack>

View File

@@ -12,7 +12,8 @@ import {
SimpleGrid,
Stack,
Text,
TextInput
TextInput,
Title
} from '@mantine/core';
import { IconSend2 } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
@@ -53,23 +54,11 @@ function Page() {
const permohonanInformasiPublikState = useProxy(statePermohonanInformasi);
const router = useRouter();
const submitForms = () => {
const submitForms = async () => {
const { create } = permohonanInformasiPublikState.statepermohonanInformasiPublik;
if (
create.form.name &&
create.form.nik &&
create.form.notelp &&
create.form.alamat &&
create.form.email &&
create.form.jenisInformasiDimintaId &&
create.form.caraMemperolehInformasiId &&
create.form.caraMemperolehSalinanInformasiId
) {
create.create();
const hasil = await create.create();
if (hasil) {
router.push('/darmasaba/permohonan/berhasil');
} else {
console.log('Validasi gagal, form tidak lengkap');
}
};
@@ -79,14 +68,17 @@ function Page() {
<BackButton />
</Box>
<Text
{/* MAIN PAGE TITLE */}
<Title
order={1}
ta="center"
fz={{ base: '2rem', md: '2.5rem' }}
fz={{ base: '1.8rem', sm: '2.2rem', md: '2.6rem' }}
lh={1.2}
c={colors['blue-button']}
fw="bold"
style={{ fontWeight: 700 }}
>
Permohonan Informasi Publik
</Text>
</Title>
<Box px={{ base: 'md', md: 100 }}>
<Stack gap="xl">
@@ -97,15 +89,18 @@ function Page() {
shadow="sm"
bg={colors['white-trans-1']}
>
<Text
{/* SUBTITLE */}
<Title
order={2}
pb={30}
ta="center"
fw="bold"
fz={{ base: 'h4', md: 'h3' }}
fz={{ base: '1.4rem', md: '1.8rem' }}
lh={1.3}
c={colors['blue-button']}
style={{ fontWeight: 700 }}
>
Tata Cara Permohonan
</Text>
</Title>
<SimpleGrid pb={30} cols={{ base: 1, sm: 2, lg: 4 }} spacing="lg">
{steps.map((v) => (
@@ -128,27 +123,38 @@ function Page() {
c={colors['blue-button']}
ta="center"
fw="bold"
fz="h3"
fz="h2"
lh={1}
>
{v.number}
</Text>
</ActionIcon>
</Center>
<Title
order={4}
ta="center"
c={colors['white-1']}
fz="lg"
lh={1.3}
style={{ fontWeight: 700 }}
>
{v.title}
</Title>
<Text
ta="center"
c={colors['white-1']}
fw="bold"
fz="lg"
fz="sm"
lh={1.4}
>
{v.title}
</Text>
<Text ta="center" c={colors['white-1']} fz="sm">
{v.desc}
</Text>
</Stack>
</Paper>
))}
</SimpleGrid>
<Group justify="center">
<Paper
p="xl"
@@ -160,15 +166,20 @@ function Page() {
maw={800}
>
<Stack gap="md">
<Text
fw="bold"
fz={{ base: 'h4', md: 'h3' }}
{/* FORM TITLE */}
<Title
order={2}
ta="center"
fz={{ base: '1.4rem', md: '1.8rem' }}
lh={1.3}
c={colors['blue-button']}
style={{ fontWeight: 700 }}
>
Formulir Permohonan Informasi
</Text>
</Title>
{/* INPUTS */}
<TextInput
label="Nama Lengkap"
placeholder="Masukkan nama lengkap Anda"
@@ -178,6 +189,7 @@ function Page() {
val.target.value;
}}
/>
<TextInput
label="Nomor Induk Kependudukan (NIK)"
placeholder="Masukkan NIK"
@@ -187,6 +199,7 @@ function Page() {
val.target.value;
}}
/>
<TextInput
label="Nomor Telepon"
placeholder="Masukkan nomor telepon aktif"
@@ -196,6 +209,7 @@ function Page() {
val.target.value;
}}
/>
<TextInput
label="Alamat Lengkap"
placeholder="Masukkan alamat sesuai identitas"
@@ -205,6 +219,7 @@ function Page() {
val.target.value;
}}
/>
<TextInput
label="Alamat Email"
placeholder="Masukkan alamat email aktif"
@@ -221,12 +236,14 @@ function Page() {
val.id;
}}
/>
<MemperolehInformasi
onChange={(val) => {
permohonanInformasiPublikState.statepermohonanInformasiPublik.create.form.caraMemperolehInformasiId =
val.id;
}}
/>
<MemperolehSalinan
onChange={(val) => {
permohonanInformasiPublikState.statepermohonanInformasiPublik.create.form.caraMemperolehSalinanInformasiId =
@@ -253,6 +270,7 @@ function Page() {
Kirim Permohonan
</Button>
</Group>
</Stack>
</Paper>
</Group>

Some files were not shown because too many files have changed in this diff Show More