Compare commits
8 Commits
nico/3-des
...
nico/10-de
| Author | SHA1 | Date | |
|---|---|---|---|
| 242ea86f77 | |||
| 99c2c9c6d7 | |||
| ac2fc1a705 | |||
| 9dbe172165 | |||
| cc318d4d54 | |||
| dcb8017594 | |||
| ec3ad12531 | |||
| dad44c0537 |
@@ -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
BIN
public/mangupuraaward.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 177 KiB |
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
303
src/app/admin/(dashboard)/desa/gallery/foto/[id]/edit/page.tsx
Normal file
303
src/app/admin/(dashboard)/desa/gallery/foto/[id]/edit/page.tsx
Normal 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;
|
||||
175
src/app/admin/(dashboard)/desa/gallery/foto/[id]/page.tsx
Normal file
175
src/app/admin/(dashboard)/desa/gallery/foto/[id]/page.tsx
Normal 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;
|
||||
228
src/app/admin/(dashboard)/desa/gallery/foto/create/page.tsx
Normal file
228
src/app/admin/(dashboard)/desa/gallery/foto/create/page.tsx
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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} />
|
||||
}
|
||||
];
|
||||
@@ -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)
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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');
|
||||
@@ -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"
|
||||
@@ -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');
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
@@ -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} />
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
@@ -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} />}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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 },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
43
src/app/api/news/latest/route.ts
Normal file
43
src/app/api/news/latest/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
197
src/app/darmasaba/(pages)/desa/galery/video/[id]/page.tsx
Normal file
197
src/app/darmasaba/(pages)/desa/galery/video/[id]/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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']} />
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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} >
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
111
src/app/darmasaba/(pages)/kesehatan/kontak-darurat/[id]/page.tsx
Normal file
111
src/app/darmasaba/(pages)/kesehatan/kontak-darurat/[id]/page.tsx
Normal 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;
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user