Fix QC Kak Inno Tgl 4 & 5 Desember

Fix QC Kak Ayu Tgl 4 & 5 Desember
Fix QC Pak Jun Tgl 5 Desember
This commit is contained in:
2025-12-09 10:28:17 +08:00
parent dcb8017594
commit cc318d4d54
28 changed files with 816 additions and 1124 deletions

View File

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

View File

@@ -5,82 +5,99 @@ import { proxy } from "valtio";
import { z } from "zod"; import { z } from "zod";
const templateForm = z.object({ const templateForm = z.object({
name: z.string().min(3, "Nama minimal 3 karakter"), name: z.string().min(3, "Nama minimal 3 karakter"),
email: z.string().min(3, "Email minimal 3 karakter"), email: z.string().min(3, "Email minimal 3 karakter"),
notelp: z.string().min(3, "Nomor Telepon minimal 3 karakter"), notelp: z
alasan: z.string().min(3, "Alasan minimal 3 karakter"), .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: { select: {
name: true; name: true;
email: true; email: true;
notelp: true; notelp: true;
alasan: true; alasan: true;
}; };
}>; }>;
const permohonanKeberatanInformasi = proxy({ const permohonanKeberatanInformasi = proxy({
create: { create: {
form: {} as PermohonanKeberatanInformasiForm, form: {} as PermohonanKeberatanInformasiForm,
loading: false, loading: false,
async create(){ async create() {
const cek = templateForm.safeParse(permohonanKeberatanInformasi.create.form); const cek = templateForm.safeParse(
if(!cek.success) { permohonanKeberatanInformasi.create.form
const err = `[${cek.error.issues );
.map((v) => `${v.path.join(".")}`) if (!cek.success) {
.join("\n")}] required`; toast.error(cek.error.issues.map((i) => i.message).join("\n"));
return toast.error(err); return false; // ⬅️ tambahkan return false
}
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;
}
},
} }
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; export default permohonanKeberatanInformasi;

View File

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

View File

@@ -6,33 +6,24 @@ import path from "path";
const beritaDelete = async (context: Context) => { const beritaDelete = async (context: Context) => {
const id = context.params?.id as string; const id = context.params?.id as string;
if (!id) { if (!id) return { status: 400, body: "ID tidak diberikan" };
return {
status: 400,
body: "ID tidak diberikan",
};
}
const berita = await prisma.berita.findUnique({ const berita = await prisma.berita.findUnique({
where: { id }, where: { id },
include: { include: { image: true, kategoriBerita: true },
image: true,
kategoriBerita: true, // pastikan relasi image sudah ada di prisma schema
},
}); });
if (!berita) { if (!berita) return { status: 404, body: "Berita tidak ditemukan" };
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) { if (berita.image) {
try { try {
const filePath = path.join(berita.image.path, berita.image.name); const filePath = path.join(berita.image.path, berita.image.name);
await fs.unlink(filePath); await fs.unlink(filePath);
await prisma.fileStorage.delete({ await prisma.fileStorage.delete({
where: { id: berita.image.id }, 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 { return {
success: true, success: true,
message: "Berita dan file terkait berhasil dihapus", message: "Berita dan file terkait berhasil dihapus",
}; };
}; };
export default beritaDelete; export default beritaDelete;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -46,8 +46,8 @@ function Page() {
</Group> </Group>
</Group> </Group>
<Paper bg={colors["white-1"]} p="md"> <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 px="lg" 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" fz={"md"} c={colors["blue-button"]} fw="bold" >
{new Date(detail.data?.createdAt).toLocaleDateString('id-ID', { {new Date(detail.data?.createdAt).toLocaleDateString('id-ID', {
weekday: 'long', weekday: 'long',
day: 'numeric', day: 'numeric',

View File

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

View File

@@ -87,7 +87,7 @@ export default function DetailInformasiPublikUser() {
<Divider /> <Divider />
<Stack gap="lg"> <Stack gap="lg">
<Box> <Box px="lg">
<Text fz={{ base: 'md', md: 'lg' }} fw="bold" mb={4}> <Text fz={{ base: 'md', md: 'lg' }} fw="bold" mb={4}>
Jenis Informasi Jenis Informasi
</Text> </Text>
@@ -96,7 +96,7 @@ export default function DetailInformasiPublikUser() {
</Text> </Text>
</Box> </Box>
<Box> <Box px="lg">
<Text fz={{ base: 'md', md: 'lg' }} fw="bold" mb={4}> <Text fz={{ base: 'md', md: 'lg' }} fw="bold" mb={4}>
Tanggal Publikasi Tanggal Publikasi
</Text> </Text>
@@ -111,15 +111,19 @@ export default function DetailInformasiPublikUser() {
</Text> </Text>
</Box> </Box>
<Box> <Box px="lg">
<Text fz={{ base: 'md', md: 'lg' }} fw="bold" mb={4}> <Text fz={{ base: 'md', md: 'lg' }} fw="bold" mb={4}>
Deskripsi Deskripsi
</Text> </Text>
<Box <Box>
className="prose max-w-none leading-relaxed" <Text
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }} ta={"justify"}
style={{wordBreak: "break-word", whiteSpace: "normal"}} className="prose max-w-none leading-relaxed"
/> dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
fz={{ base: 'md', md: 'lg' }}
/>
</Box>
</Box> </Box>
</Stack> </Stack>
</Stack> </Stack>

View File

@@ -31,7 +31,11 @@ function Page() {
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
<BackButton /> <BackButton />
</Box> </Box>
<Stack align="center" gap="xs"> <Stack
align="center"
gap="xs"
px={{ base: 'md', md: 100 }}
>
<IconBook2 size={42} stroke={1.5} color={colors["blue-button"]} /> <IconBook2 size={42} stroke={1.5} color={colors["blue-button"]} />
<Text <Text
ta="center" ta="center"
@@ -42,7 +46,7 @@ function Page() {
> >
Dasar Hukum Dasar Hukum
</Text> </Text>
<Text ta="center" fz="md" > <Text ta="center" fz="md" c={"black"}>
Informasi regulasi dan kebijakan resmi yang menjadi dasar hukum Informasi regulasi dan kebijakan resmi yang menjadi dasar hukum
</Text> </Text>
</Stack> </Stack>
@@ -71,12 +75,15 @@ function Page() {
<Stack gap="md"> <Stack gap="md">
<Text <Text
ta="center" ta="center"
c={"black"}
fw="bold" fw="bold"
fz={{ base: 'lg', md: 'xl' }} fz={{ base: 'lg', md: 'xl' }}
style={{ lineHeight: 1.4 }} style={{ lineHeight: 1.4 }}
dangerouslySetInnerHTML={{ __html: item.judul }} dangerouslySetInnerHTML={{ __html: item.judul }}
/> />
<Text <Text
c={"black"}
ta={"justify"}
fz={{ base: 'sm', md: 'md' }} fz={{ base: 'sm', md: 'md' }}
style={{ lineHeight: 1.7, wordBreak: "break-word", whiteSpace: "normal" }} style={{ lineHeight: 1.7, wordBreak: "break-word", whiteSpace: "normal" }}
dangerouslySetInnerHTML={{ __html: item.content }} dangerouslySetInnerHTML={{ __html: item.content }}

View File

@@ -598,7 +598,7 @@ const state = useProxy(indeksKepuasanState.responden);
<TextInput <TextInput
label="Nama" label="Nama"
type='text' type='text'
placeholder="masukkan nama" placeholder="Masukkan nama"
value={state.create.form.name} value={state.create.form.name}
onChange={(val) => { onChange={(val) => {
state.create.form.name = val.currentTarget.value; state.create.form.name = val.currentTarget.value;
@@ -607,7 +607,7 @@ const state = useProxy(indeksKepuasanState.responden);
<TextInput <TextInput
label="Tanggal Pengisian" label="Tanggal Pengisian"
type="date" type="date"
placeholder="masukkan tanggal" placeholder="Masukkan tanggal"
value={state.create.form.tanggal} value={state.create.form.tanggal}
onChange={(val) => { onChange={(val) => {
state.create.form.tanggal = val.currentTarget.value; state.create.form.tanggal = val.currentTarget.value;

View File

@@ -53,23 +53,11 @@ function Page() {
const permohonanInformasiPublikState = useProxy(statePermohonanInformasi); const permohonanInformasiPublikState = useProxy(statePermohonanInformasi);
const router = useRouter(); const router = useRouter();
const submitForms = () => { const submitForms = async () => {
const { create } = permohonanInformasiPublikState.statepermohonanInformasiPublik; const { create } = permohonanInformasiPublikState.statepermohonanInformasiPublik;
const hasil = await create.create(); // tunggu hasilnya
if ( if (hasil) {
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();
router.push('/darmasaba/permohonan/berhasil'); router.push('/darmasaba/permohonan/berhasil');
} else {
console.log('Validasi gagal, form tidak lengkap');
} }
}; };

View File

@@ -55,17 +55,13 @@ function Page() {
const stateKeberatan = useProxy(permohonanKeberatanInformasi); const stateKeberatan = useProxy(permohonanKeberatanInformasi);
const router = useRouter(); const router = useRouter();
const submit = () => { const submit = async () => {
if ( const { create } = stateKeberatan;
stateKeberatan.create.form.name &&
stateKeberatan.create.form.email && const hasil = await create.create(); // tunggu hasilnya
stateKeberatan.create.form.notelp &&
stateKeberatan.create.form.alasan if (hasil) {
) {
stateKeberatan.create.create();
router.push('/darmasaba/permohonan/berhasil'); router.push('/darmasaba/permohonan/berhasil');
} else {
console.log('Formulir belum lengkap');
} }
}; };
@@ -190,7 +186,7 @@ function Page() {
<TextInput <TextInput
label="Nomor Telepon" label="Nomor Telepon"
placeholder="Contoh: 0812-3456-7890" placeholder="Contoh: 081234567890"
radius="md" radius="md"
size="md" size="md"
withAsterisk withAsterisk

View File

@@ -96,14 +96,20 @@ function Page() {
<IconUser size={28} /> <IconUser size={28} />
<Text fz={{ base: "1.25rem", md: "1.5rem" }} fw="bold">Biografi</Text> <Text fz={{ base: "1.25rem", md: "1.5rem" }} fw="bold">Biografi</Text>
</Flex> </Flex>
<Text fz={{ base: "1rem", md: "1.125rem", lg: "1.25rem" }} ta="justify" dangerouslySetInnerHTML={{ __html: item.biodata }} style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> <Box px={20}>
<Text fz={{ base: "1rem", md: "1.125rem", lg: "1.25rem" }} ta="justify" dangerouslySetInnerHTML={{ __html: item.biodata }} style={{ wordBreak: "break-word", whiteSpace: "normal" }} />
</Box>
</Box> </Box>
<Box> <Box>
<Flex align="center" gap="sm" mb="sm"> <Flex align="center" gap="sm" mb="sm">
<IconTimeline size={28} /> <IconTimeline size={28} />
<Text fz={{ base: "1.25rem", md: "1.5rem" }} fw="bold">Riwayat Karir</Text> <Text fz={{ base: "1.25rem", md: "1.5rem" }} fw="bold">Riwayat Karir</Text>
</Flex> </Flex>
<Text fz={{ base: "1rem", md: "1.125rem", lg: "1.25rem" }} dangerouslySetInnerHTML={{ __html: item.riwayat }} style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> <List spacing="xs" size="sm">
<Box px={20}>
<Text fz={{ base: "1rem", md: "1.125rem", lg: "1.25rem" }} dangerouslySetInnerHTML={{ __html: item.riwayat }} style={{ wordBreak: "break-word", whiteSpace: "normal" }} />
</Box>
</List>
</Box> </Box>
</Stack> </Stack>
</Box> </Box>

View File

@@ -14,6 +14,9 @@ import {
Loader, Loader,
Paper, Paper,
Stack, Stack,
Tabs,
TabsList,
TabsTab,
Text, Text,
TextInput, TextInput,
Title, Title,
@@ -35,6 +38,7 @@ import { useEffect, useRef, useState } from 'react'
import { useProxy } from 'valtio/utils' import { useProxy } from 'valtio/utils'
import BackButton from '../../desa/layanan/_com/BackButto' import BackButton from '../../desa/layanan/_com/BackButto'
import './struktur.css' import './struktur.css'
import { useMediaQuery } from '@mantine/hooks'
export default function Page() { export default function Page() {
return ( return (
@@ -231,87 +235,121 @@ function StrukturOrganisasiPPID() {
p="md" p="md"
radius="md" radius="md"
style={{ 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 <Stack gap="sm">
placeholder="Cari nama atau jabatan..." <Group justify='center'>
leftSection={<IconSearch size={16} />} <TextInput
onChange={(e) => debouncedSearch(e.target.value)} 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={{ styles={{
input: { panel: { display: 'none' },
minWidth: 250, 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
}, },
}} }}
/> >
<TabsList
<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}
style={{ style={{
fontSize: 14, display: 'flex',
fontWeight: 700, overflowX: 'auto',
borderRadius: '8px', overflowY: 'hidden', // 👈 tambahkan ini
minWidth: 70, gap: '4px',
textAlign: 'center', 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)}% <TabsTab
</Box> value="zoom-out"
onClick={handleZoomOut}
leftSection={<IconZoomOut size={16} />}
style={{ flexShrink: 0 }} // 👈 pastikan tidak mengecil
>
Zoom Out
</TabsTab>
<Button <Box
bg={colors['blue-button-2']} bg={colors['blue-button-2']}
c={colors['blue-button']} c={colors['blue-button']}
variant="light" px={12}
size="sm" py={6}
onClick={handleZoomIn} style={{
leftSection={<IconZoomIn size={16} />} fontSize: 14,
> fontWeight: 700,
Zoom In borderRadius: '6px',
</Button> minWidth: 60,
textAlign: 'center',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
whiteSpace: 'nowrap', // 👈 mencegah text wrap
}}
>
{Math.round(scale * 100)}%
</Box>
<Button <TabsTab
bg={colors['blue-button-2']} value="zoom-in"
c={colors['blue-button']} onClick={handleZoomIn}
variant="light" leftSection={<IconZoomIn size={16} />}
size="sm" style={{ flexShrink: 0 }}
onClick={resetZoom} >
> Zoom In
Reset </TabsTab>
</Button>
<Button <TabsTab
bg={colors['blue-button-2']} value="reset"
c={colors['blue-button']} onClick={resetZoom}
size="sm" style={{ flexShrink: 0 }}
onClick={toggleFullscreen} >
leftSection={ Reset
isFullscreen ? ( </TabsTab>
<IconArrowsMinimize size={16} />
) : ( <TabsTab
<IconArrowsMaximize size={16} /> value="fullscreen"
) onClick={toggleFullscreen}
} leftSection={
> isFullscreen ? (
Fullscreen <IconArrowsMinimize size={16} />
</Button> ) : (
</Group> <IconArrowsMaximize size={16} />
</Group> )
}
style={{ flexShrink: 0 }}
>
{isFullscreen ? 'Exit' : 'Fullscreen'}
</TabsTab>
</TabsList>
</Tabs>
</Stack>
</Paper> </Paper>
{/* 🧩 Chart Container */} {/* 🧩 Chart Container */}
@@ -325,15 +363,20 @@ function StrukturOrganisasiPPID() {
maxWidth: '100%', maxWidth: '100%',
padding: '32px 16px', padding: '32px 16px',
transition: 'transform 0.2s ease', transition: 'transform 0.2s ease',
transform: `scale(${scale})`,
transformOrigin: 'center top',
}} }}
> >
<OrganizationChart <Box style={{
value={chartData} transform: `scale(${scale})`,
nodeTemplate={(node) => <NodeCard node={node} router={router} />} transformOrigin: 'center top',
className="p-organizationchart p-organizationchart-horizontal" 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> </Box>
</Center> </Center>
</Stack> </Stack>
@@ -345,6 +388,7 @@ function NodeCard({ node, router }: any) {
const name = node?.data?.name || 'Tanpa Nama' const name = node?.data?.name || 'Tanpa Nama'
const title = node?.data?.title || 'Tanpa Jabatan' const title = node?.data?.title || 'Tanpa Jabatan'
const hasId = Boolean(node?.data?.id) const hasId = Boolean(node?.data?.id)
const isMobile = useMediaQuery("(max-width: 768px)");
return ( return (
<Transition mounted transition="pop" duration={300}> <Transition mounted transition="pop" duration={300}>
@@ -355,9 +399,10 @@ function NodeCard({ node, router }: any) {
withBorder withBorder
style={{ style={{
...styles, ...styles,
width: 240, width: '100%',
minHeight: 280, maxWidth: isMobile ? 200 : 240, // lebih kecil di mobile
padding: 20, 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%)', 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)', borderColor: 'rgba(28, 110, 164, 0.3)',
borderWidth: 2, borderWidth: 2,
@@ -411,6 +456,7 @@ function NodeCard({ node, router }: any) {
c={colors['blue-button']} c={colors['blue-button']}
lineClamp={2} lineClamp={2}
style={{ style={{
// fontSize: 'clamp(12px, 4vw, 16px)', // 👈 responsif font size
minHeight: 40, minHeight: 40,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',

View File

@@ -75,7 +75,7 @@ function Page() {
lh={1.7} lh={1.7}
ta="center" ta="center"
dangerouslySetInnerHTML={{ __html: item.visi }} dangerouslySetInnerHTML={{ __html: item.visi }}
style={{wordBreak: "break-word", whiteSpace: "normal"}} style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/> />
</Box> </Box>
@@ -86,12 +86,15 @@ function Page() {
c={colors['blue-button']} mb="sm"> c={colors['blue-button']} mb="sm">
Misi PPID Misi PPID
</Text> </Text>
<Text <Box px={{ base: 'md', md: 100 }}>
fz={{ base: 'md', md: 'lg' }} <Text
lh={1.7} ta={"justify"}
dangerouslySetInnerHTML={{ __html: item.misi }} fz={{ base: 'md', md: 'lg' }}
style={{wordBreak: "break-word", whiteSpace: "normal"}} lh={1.7}
/> dangerouslySetInnerHTML={{ __html: item.misi }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/>
</Box>
</Box> </Box>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -220,8 +220,9 @@ export default function ModernNewsNotification({
...styles, ...styles,
position: "fixed", position: "fixed",
bottom: "100px", bottom: "100px",
right: "24px", left: "24px",
width: "380px", width: "90vw",
maxWidth: 380,
maxHeight: "500px", maxHeight: "500px",
boxShadow: "0 8px 32px rgba(0,0,0,0.12)", boxShadow: "0 8px 32px rgba(0,0,0,0.12)",
borderRadius: "16px", borderRadius: "16px",
@@ -290,7 +291,7 @@ export default function ModernNewsNotification({
color={item.type === "berita" ? "blue" : "orange"} color={item.type === "berita" ? "blue" : "orange"}
variant="light" variant="light"
> >
{item.type === "berita" ? "📰 Berita" : "📢 Pengumuman"} {item.type === "berita" ? "Berita" : "Pengumuman"}
</Badge> </Badge>
<IconChevronRight size={16} color="#adb5bd" /> <IconChevronRight size={16} color="#adb5bd" />
</Group> </Group>
@@ -321,8 +322,9 @@ export default function ModernNewsNotification({
...styles, ...styles,
position: "fixed", position: "fixed",
bottom: "100px", bottom: "100px",
right: "24px", left: "24px",
width: "380px", width: "90vw",
maxWidth: 380,
boxShadow: "0 8px 32px rgba(0,0,0,0.15)", boxShadow: "0 8px 32px rgba(0,0,0,0.15)",
borderRadius: "12px", borderRadius: "12px",
overflow: "hidden", overflow: "hidden",
@@ -350,7 +352,6 @@ export default function ModernNewsNotification({
size="md" size="md"
color={currentNews?.type === "berita" ? "blue" : "orange"} color={currentNews?.type === "berita" ? "blue" : "orange"}
variant="light" variant="light"
leftSection={currentNews?.type === "berita" ? "📰" : "📢"}
> >
{currentNews?.type === "berita" {currentNews?.type === "berita"
? "Berita Terbaru" ? "Berita Terbaru"

View File

@@ -4,7 +4,7 @@ import indeksKepuasanState from "@/app/admin/(dashboard)/_state/landing-page/ind
import colors from "@/con/colors"; import colors from "@/con/colors";
import { BarChart, PieChart } from '@mantine/charts'; 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 { useDisclosure, useMediaQuery, useShallowEffect } from "@mantine/hooks";
import { useState } from "react"; import { useState } from "react";
import { useProxy } from "valtio/utils"; import { useProxy } from "valtio/utils";
@@ -25,6 +25,7 @@ function Kepuasan() {
const [donutDataKelompokUmur, setDonutDataKelompokUmur] = useState<ChartDataItem[]>([]); const [donutDataKelompokUmur, setDonutDataKelompokUmur] = useState<ChartDataItem[]>([]);
const [barChartData, setBarChartData] = useState<Array<{ month: string; Responden: number }>>([]); const [barChartData, setBarChartData] = useState<Array<{ month: string; Responden: number }>>([]);
const [opened, { open, close }] = useDisclosure(false) const [opened, { open, close }] = useDisclosure(false)
const isMobile = useMediaQuery("(max-width: 768px)");
const resetForm = () => { const resetForm = () => {
state.create.form = { state.create.form = {
@@ -41,7 +42,7 @@ function Kepuasan() {
indeksKepuasanState.jenisKelaminResponden.findMany.load() indeksKepuasanState.jenisKelaminResponden.findMany.load()
indeksKepuasanState.pilihanRatingResponden.findMany.load() indeksKepuasanState.pilihanRatingResponden.findMany.load()
indeksKepuasanState.kelompokUmurResponden.findMany.load() indeksKepuasanState.kelompokUmurResponden.findMany.load()
},[]) }, [])
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
@@ -82,13 +83,13 @@ function Kepuasan() {
// Update gender chart data // Update gender chart data
setDonutDataJenisKelamin([ setDonutDataJenisKelamin([
{ name: 'Laki-laki', value: totalLaki, color: colors['blue-button'] }, { name: 'Laki-laki', value: totalLaki, color: '#52ABE3FF' },
{ name: 'Perempuan', value: totalPerempuan, color: '#10A85AFF' }, { name: 'Perempuan', value: totalPerempuan, color: '#10A85AFF' },
]); ]);
// Update rating chart data // Update rating chart data
setDonutDataRating([ setDonutDataRating([
{ name: 'Sangat Baik', value: totalSangatBaik, color: colors['blue-button'] }, { name: 'Sangat Baik', value: totalSangatBaik, color: '#52ABE3FF' },
{ name: 'Baik', value: totalBaik, color: '#10A85AFF' }, { name: 'Baik', value: totalBaik, color: '#10A85AFF' },
{ name: 'Kurang Baik', value: totalKurangBaik, color: '#FFA500' }, { name: 'Kurang Baik', value: totalKurangBaik, color: '#FFA500' },
{ name: 'Sangat Kurang Baik', value: totalSangatKurangBaik, color: '#FF4500' }, { name: 'Sangat Kurang Baik', value: totalSangatKurangBaik, color: '#FF4500' },
@@ -96,7 +97,7 @@ function Kepuasan() {
// Update age group chart data // Update age group chart data
setDonutDataKelompokUmur([ setDonutDataKelompokUmur([
{ name: 'Muda', value: totalMuda, color: colors['blue-button'] }, { name: 'Muda', value: totalMuda, color: '#52ABE3FF' },
{ name: 'Dewasa', value: totalDewasa, color: '#10A85AFF' }, { name: 'Dewasa', value: totalDewasa, color: '#10A85AFF' },
{ name: 'Lansia', value: totalLansia, color: '#FFA500' }, { name: 'Lansia', value: totalLansia, color: '#FFA500' },
]); ]);
@@ -220,10 +221,13 @@ function Kepuasan() {
<Box style={{ position: 'relative', width: '100%' }}> <Box style={{ position: 'relative', width: '100%' }}>
<Center> <Center>
<PieChart <PieChart
withLabels
withTooltip withTooltip
tooltipAnimationDuration={200}
withLabels
labelsPosition="inside" // 👈 ini yang penting!
labelsType="percent" labelsType="percent"
size={250} // Fixed size in pixels withLabelsLine
size={isMobile ? 180 : 250} // 👈 kecilkan ukuran di mobile
data={donutDataJenisKelamin} data={donutDataJenisKelamin}
/> />
</Center> </Center>
@@ -259,10 +263,10 @@ function Kepuasan() {
withTooltip withTooltip
tooltipAnimationDuration={200} tooltipAnimationDuration={200}
withLabels withLabels
labelsPosition="outside" labelsPosition="inside" // 👈 ini yang penting!
labelsType="percent" labelsType="percent"
withLabelsLine withLabelsLine
size={250} size={isMobile ? 180 : 250} // 👈 kecilkan ukuran di mobile
data={donutDataRating} data={donutDataRating}
/> />
</Center> </Center>
@@ -302,10 +306,10 @@ function Kepuasan() {
withTooltip withTooltip
tooltipAnimationDuration={200} tooltipAnimationDuration={200}
withLabels withLabels
labelsPosition="outside" labelsPosition="inside"// 👈 ini yang penting!
labelsType="percent" labelsType="percent"
withLabelsLine withLabelsLine
size={250} size={isMobile ? 180 : 250} // 👈 kecilkan ukuran di mobile
data={donutDataKelompokUmur} data={donutDataKelompokUmur}
/> />
</Center> </Center>
@@ -494,6 +498,8 @@ function Kepuasan() {
<PieChart <PieChart
withLabels withLabels
withTooltip withTooltip
labelsPosition="inside"
labelsType="percent" labelsType="percent"
size={200} size={200}
data={donutDataJenisKelamin} data={donutDataJenisKelamin}
@@ -531,7 +537,8 @@ function Kepuasan() {
withTooltip withTooltip
tooltipAnimationDuration={200} tooltipAnimationDuration={200}
withLabels withLabels
labelsPosition="outside"
labelsPosition="inside"
labelsType="percent" labelsType="percent"
withLabelsLine withLabelsLine
size={200} size={200}
@@ -574,7 +581,8 @@ function Kepuasan() {
withTooltip withTooltip
tooltipAnimationDuration={200} tooltipAnimationDuration={200}
withLabels withLabels
labelsPosition="outside"
labelsPosition="inside"
labelsType="percent" labelsType="percent"
withLabelsLine withLabelsLine
size={190} size={190}
@@ -610,7 +618,7 @@ function Kepuasan() {
<TextInput <TextInput
label="Nama" label="Nama"
type='text' type='text'
placeholder="masukkan nama" placeholder="Masukkan nama"
value={state.create.form.name} value={state.create.form.name}
onChange={(val) => { onChange={(val) => {
state.create.form.name = val.currentTarget.value; state.create.form.name = val.currentTarget.value;
@@ -619,7 +627,7 @@ function Kepuasan() {
<TextInput <TextInput
label="Tanggal Pengisian" label="Tanggal Pengisian"
type="date" type="date"
placeholder="masukkan tanggal" placeholder="Masukkan tanggal"
value={state.create.form.tanggal} value={state.create.form.tanggal}
onChange={(val) => { onChange={(val) => {
state.create.form.tanggal = val.currentTarget.value; state.create.form.tanggal = val.currentTarget.value;

View File

@@ -154,7 +154,7 @@ function LandingPage() {
return ( return (
<Stack bg={colors.Bg} p="md" gap="lg"> <Stack bg={colors.Bg} p="md" gap="lg">
<Flex gap="lg" wrap={{ base: "wrap", md: "nowrap" }}> <Flex gap="lg" wrap={{ base: "wrap", md: "nowrap" }} pb={30}>
<Stack w={{ base: "100%", md: "65%" }} gap="lg"> <Stack w={{ base: "100%", md: "65%" }} gap="lg">
<Card radius="xl" bg={colors.grey[1]} p="lg" mt={10} shadow="xl"> <Card radius="xl" bg={colors.grey[1]} p="lg" mt={10} shadow="xl">
<Stack gap="xl"> <Stack gap="xl">

View File

@@ -4,7 +4,7 @@ import penghargaanState from "@/app/admin/(dashboard)/_state/desa/penghargaan";
import { Stack, Box, Container, Button, Text, Loader, Paper, Center, ActionIcon } from "@mantine/core"; import { Stack, Box, Container, Button, Text, Loader, Paper, Center, ActionIcon } from "@mantine/core";
import { IconAward, IconArrowRight, IconPlayerPlay } from "@tabler/icons-react"; import { IconAward, IconArrowRight, IconPlayerPlay } from "@tabler/icons-react";
import { useTransitionRouter } from 'next-view-transitions'; import { useTransitionRouter } from 'next-view-transitions';
import { useEffect, useState } from "react"; import { useEffect, useState, useRef } from "react";
import { useProxy } from "valtio/utils"; import { useProxy } from "valtio/utils";
import { useMediaQuery } from "@mantine/hooks"; import { useMediaQuery } from "@mantine/hooks";
@@ -15,16 +15,35 @@ function Penghargaan() {
const isMobile = useMediaQuery('(max-width: 768px)'); const isMobile = useMediaQuery('(max-width: 768px)');
const [isVideoLoaded, setIsVideoLoaded] = useState(false); const [isVideoLoaded, setIsVideoLoaded] = useState(false);
const [showVideo, setShowVideo] = useState(true); const [showVideo, setShowVideo] = useState(true);
const [videoError, setVideoError] = useState(false);
const videoRef = useRef<HTMLVideoElement>(null);
// Opsional: deteksi iOS // Deteksi iOS dengan lebih akurat
const isIOS = typeof window !== 'undefined' && /iPad|iPhone|iPod/.test(navigator.userAgent); const isIOS = typeof window !== 'undefined' && (
/iPad|iPhone|iPod/.test(navigator.userAgent) ||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1) // iPad dengan iPadOS 13+
);
useEffect(() => { useEffect(() => {
if (isIOS) { // Di iOS, coba autoplay dulu, kalau gagal tampilkan fallback
// Di iOS, jangan andalkan autoplay — tampilkan kontrol if (isIOS && videoRef.current) {
setShowVideo(false); const playPromise = videoRef.current.play();
if (playPromise !== undefined) {
playPromise
.then(() => {
// Autoplay berhasil
setShowVideo(true);
setIsVideoLoaded(true);
})
.catch(() => {
// Autoplay gagal, tampilkan fallback
setShowVideo(false);
setVideoError(true);
});
}
} }
}, []); }, [isIOS]);
useEffect(() => { useEffect(() => {
const loadData = async () => { const loadData = async () => {
@@ -38,42 +57,99 @@ function Penghargaan() {
loadData(); loadData();
}, []); }, []);
const handlePlayVideo = () => {
setShowVideo(true);
setVideoError(false);
// Paksa play video setelah user interaction
setTimeout(() => {
if (videoRef.current) {
videoRef.current.play().catch(err => {
console.error("Video play error:", err);
setVideoError(true);
});
}
}, 100);
};
// kalau mobile ambil 1 data aja, kalau desktop ambil 3 // kalau mobile ambil 1 data aja, kalau desktop ambil 3
const data = state.findMany.data?.slice(0, isMobile ? 1 : 3); const data = state.findMany.data?.slice(0, isMobile ? 1 : 3);
return ( return (
<Stack pos="relative" h="auto" mih={{ base: 500, md: 720 }}> <Stack pos="relative" h="auto" mih={{ base: 500, md: 720 }} style={{ overflow: 'hidden' }}>
{showVideo ? ( {/* Video Layer */}
{showVideo && !videoError && (
<video <video
ref={videoRef}
autoPlay autoPlay
muted muted
loop loop
playsInline playsInline
webkit-playsinline="true" preload="auto"
onLoadedData={() => setIsVideoLoaded(true)} onLoadedData={() => setIsVideoLoaded(true)}
style={{ opacity: isVideoLoaded ? 1 : 0, transition: 'opacity 0.5s' }} onError={() => {
console.error("Video load error");
setVideoError(true);
setShowVideo(false);
}}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
objectFit: 'cover',
opacity: isVideoLoaded ? 1 : 0,
transition: 'opacity 0.5s ease',
zIndex: 0,
}}
> >
<source src="/assets/videos/award.mp4" type="video/mp4" /> <source src="/assets/videos/award.mp4" type="video/mp4" />
</video> </video>
) : ( )}
// Fallback: tampilkan poster + play button
{/* Fallback Image + Play Button */}
{(!showVideo || videoError) && (
<Box <Box
onClick={() => setShowVideo(true)} onClick={handlePlayVideo}
style={{ style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
backgroundImage: "url('/mangupuraaward.jpeg')", backgroundImage: "url('/mangupuraaward.jpeg')",
backgroundSize: 'cover', backgroundSize: 'cover',
backgroundPosition: 'center', backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
cursor: 'pointer', cursor: 'pointer',
zIndex: 0,
}} }}
> >
<Center h="100%"> <Center
<ActionIcon size="lg" radius="xl" color="white"> style={{
<IconPlayerPlay size={32} /> width: '100%',
height: '100%',
background: 'rgba(0,0,0,0.3)', // overlay gelap agar icon terlihat
}}
>
<ActionIcon
size={80}
radius="xl"
variant="filled"
color="blue"
style={{
backgroundColor: 'rgba(255,255,255,0.9)',
boxShadow: '0 8px 32px rgba(0,0,0,0.3)',
}}
>
<IconPlayerPlay size={40} color="var(--mantine-color-blue-6)" />
</ActionIcon> </ActionIcon>
</Center> </Center>
</Box> </Box>
)} )}
{/* Overlay Gradient + Content */}
<Box <Box
style={{ style={{
width: "100%", width: "100%",
@@ -152,4 +228,4 @@ function Penghargaan() {
); );
} }
export default Penghargaan; export default Penghargaan;

View File

@@ -38,15 +38,15 @@ export default function Page() {
const [hasNewContent, setHasNewContent] = useState(false); const [hasNewContent, setHasNewContent] = useState(false);
const [newItemCount, setNewItemCount] = useState(0); const [newItemCount, setNewItemCount] = useState(0);
const lastBeritaId = useRef<string | null>(null); const lastBeritaTimestamp = useRef<string | null>(null);
const lastPengumumanId = useRef<string | null>(null); const lastPengumumanTimestamp = useRef<string | null>(null);
// Inisialisasi dari localStorage // Inisialisasi dari localStorage
useEffect(() => { useEffect(() => {
const savedBerita = localStorage.getItem("lastSeenBeritaId"); const savedBeritaTs = localStorage.getItem("lastSeenBeritaTs");
const savedPengumuman = localStorage.getItem("lastSeenPengumumanId"); const savedPengumumanTs = localStorage.getItem("lastSeenPengumumanTs");
if (savedBerita) lastBeritaId.current = savedBerita; if (savedBeritaTs) lastBeritaTimestamp.current = savedBeritaTs;
if (savedPengumuman) lastPengumumanId.current = savedPengumuman; if (savedPengumumanTs) lastPengumumanTimestamp.current = savedPengumumanTs;
}, []); }, []);
// Load data utama (untuk card) // Load data utama (untuk card)
@@ -70,28 +70,49 @@ export default function Page() {
if (result.success && Array.isArray(result.news)) { if (result.success && Array.isArray(result.news)) {
const news = result.news as NewsItem[]; const news = result.news as NewsItem[];
// Ambil ID terbaru
const latestBerita = news.find((n) => n.type === "berita"); const latestBerita = news.find((n) => n.type === "berita");
const latestPengumuman = news.find((n) => n.type === "pengumuman"); const latestPengumuman = news.find((n) => n.type === "pengumuman");
const isNewBerita = latestBerita && lastBeritaId.current !== null && latestBerita.id !== lastBeritaId.current; const latestBeritaTs = latestBerita?.timestamp
const isNewPengumuman = latestPengumuman && lastPengumumanId.current !== null && latestPengumuman.id !== lastPengumumanId.current; ? new Date(latestBerita.timestamp).toISOString()
: null;
const latestPengumumanTs = latestPengumuman?.timestamp
? new Date(latestPengumuman.timestamp).toISOString()
: null;
// Simpan ID terbaru ke ref // Inisialisasi flag
if (latestBerita) lastBeritaId.current = (latestBerita.id); let isNewBerita = false;
if (latestPengumuman) lastPengumumanId.current = (latestPengumuman.id); let isNewPengumuman = false;
// Jika ini bukan inisialisasi pertama, tampilkan notifikasi // Deteksi berita baru
if (lastBeritaId.current !== null || lastPengumumanId.current !== null) { if (latestBeritaTs) {
if (isNewBerita || isNewPengumuman) { if (lastBeritaTimestamp.current === null) {
const count = (isNewBerita ? 1 : 0) + (isNewPengumuman ? 1 : 0); // Pertama kali: simpan tanpa notifikasi
setNewItemCount(count); lastBeritaTimestamp.current = latestBeritaTs;
setHasNewContent(true); localStorage.setItem("lastSeenBeritaTs", latestBeritaTs);
} else if (latestBeritaTs > lastBeritaTimestamp.current) {
isNewBerita = true;
lastBeritaTimestamp.current = latestBeritaTs;
} }
} else { }
// Simpan ke localStorage saat pertama kali
if (latestBerita) localStorage.setItem("lastSeenBeritaId", (latestBerita.id)); // Deteksi pengumuman baru
if (latestPengumuman) localStorage.setItem("lastSeenPengumumanId", (latestPengumuman.id)); if (latestPengumumanTs) {
if (lastPengumumanTimestamp.current === null) {
// Pertama kali: simpan tanpa notifikasi
lastPengumumanTimestamp.current = latestPengumumanTs;
localStorage.setItem("lastSeenPengumumanTs", latestPengumumanTs);
} else if (latestPengumumanTs > lastPengumumanTimestamp.current) {
isNewPengumuman = true;
lastPengumumanTimestamp.current = latestPengumumanTs;
}
}
// 🔔 Trigger notifikasi hanya jika ada yang benar-benar BARU
if (isNewBerita || isNewPengumuman) {
const count = (isNewBerita ? 1 : 0) + (isNewPengumuman ? 1 : 0);
setNewItemCount(count);
setHasNewContent(true); // ✅ INI YANG KAMU LUPA!
} }
setNotificationNews(news); setNotificationNews(news);
@@ -113,13 +134,17 @@ export default function Page() {
}, []); }, []);
const handleSeen = () => { const handleSeen = () => {
setHasNewContent(false); setHasNewContent(false);
setNewItemCount(0); setNewItemCount(0);
const latestBerita = notificationNews.find(n => n.type === "berita"); const latestBerita = notificationNews.find(n => n.type === "berita");
const latestPengumuman = notificationNews.find(n => n.type === "pengumuman"); const latestPengumuman = notificationNews.find(n => n.type === "pengumuman");
if (latestBerita) localStorage.setItem("lastSeenBeritaId", String(latestBerita.id)); if (latestBerita) {
if (latestPengumuman) localStorage.setItem("lastSeenPengumumanId", String(latestPengumuman.id)); localStorage.setItem("lastSeenBeritaTs", new Date(latestBerita.timestamp!).toISOString());
}; }
if (latestPengumuman) {
localStorage.setItem("lastSeenPengumumanTs", new Date(latestPengumuman.timestamp!).toISOString());
}
};
return ( return (
<Box id="page-root"> <Box id="page-root">