Compare commits
3 Commits
nico/5-des
...
nico/9-des
| Author | SHA1 | Date | |
|---|---|---|---|
| ac2fc1a705 | |||
| 9dbe172165 | |||
| cc318d4d54 |
@@ -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;
|
||||
|
||||
|
||||
@@ -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} />}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -87,7 +87,7 @@ export default function DetailInformasiPublikUser() {
|
||||
<Divider />
|
||||
|
||||
<Stack gap="lg">
|
||||
<Box>
|
||||
<Box px="lg">
|
||||
<Text fz={{ base: 'md', md: 'lg' }} fw="bold" mb={4}>
|
||||
Jenis Informasi
|
||||
</Text>
|
||||
@@ -96,7 +96,7 @@ export default function DetailInformasiPublikUser() {
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Box px="lg">
|
||||
<Text fz={{ base: 'md', md: 'lg' }} fw="bold" mb={4}>
|
||||
Tanggal Publikasi
|
||||
</Text>
|
||||
@@ -111,15 +111,19 @@ export default function DetailInformasiPublikUser() {
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Box px="lg">
|
||||
<Text fz={{ base: 'md', md: 'lg' }} fw="bold" mb={4}>
|
||||
Deskripsi
|
||||
</Text>
|
||||
<Box
|
||||
className="prose max-w-none leading-relaxed"
|
||||
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
|
||||
style={{wordBreak: "break-word", whiteSpace: "normal"}}
|
||||
/>
|
||||
<Box>
|
||||
<Text
|
||||
ta={"justify"}
|
||||
className="prose max-w-none leading-relaxed"
|
||||
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
fz={{ base: 'md', md: 'lg' }}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
@@ -33,7 +33,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); // 500ms delay
|
||||
const router = useTransitionRouter()
|
||||
const {
|
||||
data,
|
||||
|
||||
@@ -31,7 +31,11 @@ function Page() {
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</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"]} />
|
||||
<Text
|
||||
ta="center"
|
||||
@@ -42,7 +46,7 @@ function Page() {
|
||||
>
|
||||
Dasar Hukum
|
||||
</Text>
|
||||
<Text ta="center" fz="md" >
|
||||
<Text ta="center" fz="md" c={"black"}>
|
||||
Informasi regulasi dan kebijakan resmi yang menjadi dasar hukum
|
||||
</Text>
|
||||
</Stack>
|
||||
@@ -71,12 +75,15 @@ function Page() {
|
||||
<Stack gap="md">
|
||||
<Text
|
||||
ta="center"
|
||||
c={"black"}
|
||||
fw="bold"
|
||||
fz={{ base: 'lg', md: 'xl' }}
|
||||
style={{ lineHeight: 1.4 }}
|
||||
dangerouslySetInnerHTML={{ __html: item.judul }}
|
||||
/>
|
||||
<Text
|
||||
c={"black"}
|
||||
ta={"justify"}
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
style={{ lineHeight: 1.7, wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
dangerouslySetInnerHTML={{ __html: item.content }}
|
||||
|
||||
@@ -82,13 +82,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 +96,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' },
|
||||
]);
|
||||
@@ -216,6 +216,7 @@ const state = useProxy(indeksKepuasanState.responden);
|
||||
<PieChart
|
||||
withLabels
|
||||
withTooltip
|
||||
labelsPosition="inside"
|
||||
labelsType="percent"
|
||||
size={250} // Fixed size in pixels
|
||||
data={donutDataJenisKelamin}
|
||||
@@ -253,7 +254,7 @@ const state = useProxy(indeksKepuasanState.responden);
|
||||
withTooltip
|
||||
tooltipAnimationDuration={200}
|
||||
withLabels
|
||||
labelsPosition="outside"
|
||||
labelsPosition="inside"
|
||||
labelsType="percent"
|
||||
withLabelsLine
|
||||
size={250}
|
||||
@@ -296,7 +297,7 @@ const state = useProxy(indeksKepuasanState.responden);
|
||||
withTooltip
|
||||
tooltipAnimationDuration={200}
|
||||
withLabels
|
||||
labelsPosition="outside"
|
||||
labelsPosition="inside"
|
||||
labelsType="percent"
|
||||
withLabelsLine
|
||||
size={250}
|
||||
@@ -482,6 +483,7 @@ const state = useProxy(indeksKepuasanState.responden);
|
||||
<PieChart
|
||||
withLabels
|
||||
withTooltip
|
||||
labelsPosition="inside"
|
||||
labelsType="percent"
|
||||
size={200}
|
||||
data={donutDataJenisKelamin}
|
||||
@@ -519,7 +521,7 @@ const state = useProxy(indeksKepuasanState.responden);
|
||||
withTooltip
|
||||
tooltipAnimationDuration={200}
|
||||
withLabels
|
||||
labelsPosition="outside"
|
||||
labelsPosition="inside"
|
||||
labelsType="percent"
|
||||
withLabelsLine
|
||||
size={200}
|
||||
@@ -562,7 +564,7 @@ const state = useProxy(indeksKepuasanState.responden);
|
||||
withTooltip
|
||||
tooltipAnimationDuration={200}
|
||||
withLabels
|
||||
labelsPosition="outside"
|
||||
labelsPosition="inside"
|
||||
labelsType="percent"
|
||||
withLabelsLine
|
||||
size={190}
|
||||
@@ -598,7 +600,7 @@ const state = useProxy(indeksKepuasanState.responden);
|
||||
<TextInput
|
||||
label="Nama"
|
||||
type='text'
|
||||
placeholder="masukkan nama"
|
||||
placeholder="Masukkan nama"
|
||||
value={state.create.form.name}
|
||||
onChange={(val) => {
|
||||
state.create.form.name = val.currentTarget.value;
|
||||
@@ -607,7 +609,7 @@ const state = useProxy(indeksKepuasanState.responden);
|
||||
<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;
|
||||
|
||||
@@ -53,23 +53,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(); // tunggu hasilnya
|
||||
if (hasil) {
|
||||
router.push('/darmasaba/permohonan/berhasil');
|
||||
} else {
|
||||
console.log('Validasi gagal, form tidak lengkap');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -55,17 +55,13 @@ function Page() {
|
||||
const stateKeberatan = useProxy(permohonanKeberatanInformasi);
|
||||
const router = useRouter();
|
||||
|
||||
const submit = () => {
|
||||
if (
|
||||
stateKeberatan.create.form.name &&
|
||||
stateKeberatan.create.form.email &&
|
||||
stateKeberatan.create.form.notelp &&
|
||||
stateKeberatan.create.form.alasan
|
||||
) {
|
||||
stateKeberatan.create.create();
|
||||
const submit = async () => {
|
||||
const { create } = stateKeberatan;
|
||||
|
||||
const hasil = await create.create(); // tunggu hasilnya
|
||||
|
||||
if (hasil) {
|
||||
router.push('/darmasaba/permohonan/berhasil');
|
||||
} else {
|
||||
console.log('Formulir belum lengkap');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -190,7 +186,7 @@ function Page() {
|
||||
|
||||
<TextInput
|
||||
label="Nomor Telepon"
|
||||
placeholder="Contoh: 0812-3456-7890"
|
||||
placeholder="Contoh: 081234567890"
|
||||
radius="md"
|
||||
size="md"
|
||||
withAsterisk
|
||||
|
||||
@@ -96,14 +96,20 @@ function Page() {
|
||||
<IconUser size={28} />
|
||||
<Text fz={{ base: "1.25rem", md: "1.5rem" }} fw="bold">Biografi</Text>
|
||||
</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>
|
||||
<Flex align="center" gap="sm" mb="sm">
|
||||
<IconTimeline size={28} />
|
||||
<Text fz={{ base: "1.25rem", md: "1.5rem" }} fw="bold">Riwayat Karir</Text>
|
||||
</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>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
@@ -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 BackButton from '../../desa/layanan/_com/BackButto'
|
||||
import './struktur.css'
|
||||
import { useMediaQuery } from '@mantine/hooks'
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
@@ -231,87 +235,121 @@ function StrukturOrganisasiPPID() {
|
||||
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 StrukturOrganisasiPPID() {
|
||||
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,
|
||||
@@ -411,6 +456,7 @@ function NodeCard({ node, router }: any) {
|
||||
c={colors['blue-button']}
|
||||
lineClamp={2}
|
||||
style={{
|
||||
// fontSize: 'clamp(12px, 4vw, 16px)', // 👈 responsif font size
|
||||
minHeight: 40,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
|
||||
@@ -75,7 +75,7 @@ function Page() {
|
||||
lh={1.7}
|
||||
ta="center"
|
||||
dangerouslySetInnerHTML={{ __html: item.visi }}
|
||||
style={{wordBreak: "break-word", whiteSpace: "normal"}}
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -86,12 +86,15 @@ function Page() {
|
||||
c={colors['blue-button']} mb="sm">
|
||||
Misi PPID
|
||||
</Text>
|
||||
<Text
|
||||
fz={{ base: 'md', md: 'lg' }}
|
||||
lh={1.7}
|
||||
dangerouslySetInnerHTML={{ __html: item.misi }}
|
||||
style={{wordBreak: "break-word", whiteSpace: "normal"}}
|
||||
/>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Text
|
||||
ta={"justify"}
|
||||
fz={{ base: 'md', md: 'lg' }}
|
||||
lh={1.7}
|
||||
dangerouslySetInnerHTML={{ __html: item.misi }}
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
@@ -55,8 +55,8 @@ function APBDesProgress({ apbdesData }: APBDesProgressProps) {
|
||||
|
||||
return (
|
||||
<Box key={label}>
|
||||
<Text fw={600} fz="sm">{label}</Text>
|
||||
<Text fw={700} mb="xs">
|
||||
<Text fw={600} fz={{base: "sm", md: "md"}}>{label}</Text>
|
||||
<Text fw={700} fz={{base: "sm", md: "md"}} mb="xs">
|
||||
{formatRupiah(dataset.realisasi)} | {formatRupiah(dataset.anggaran)}
|
||||
</Text>
|
||||
<Progress
|
||||
|
||||
@@ -39,22 +39,20 @@ function Slider() {
|
||||
const state = useProxy(penghargaanState);
|
||||
const router = useTransitionRouter();
|
||||
|
||||
// Refs for smooth animation
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const scrollPositionRef = useRef(0);
|
||||
const animationFrameRef = useRef<number>(0);
|
||||
const scrollPosRef = useRef(0);
|
||||
const animFrameRef = useRef<number>(0);
|
||||
const isHoveredRef = useRef(false);
|
||||
|
||||
// Refs for drag functionality
|
||||
const isDraggingRef = useRef(false);
|
||||
const startXRef = useRef(0);
|
||||
const scrollLeftRef = useRef(0);
|
||||
const velocityRef = useRef(0);
|
||||
const lastScrollTimeRef = useRef(0);
|
||||
const lastScrollRef = useRef(0);
|
||||
|
||||
// Speed configuration
|
||||
const normalSpeed = 1.0; // pixels per frame
|
||||
const hoverSpeed = 0.5; // slower speed on hover
|
||||
const SPEED_NORMAL = 1.0;
|
||||
const SPEED_HOVER = 0.5;
|
||||
const VELOCITY_DECAY = 0.95;
|
||||
const SCROLL_THRESHOLD = 100;
|
||||
|
||||
useEffect(() => {
|
||||
state.findMany.load();
|
||||
@@ -63,120 +61,114 @@ function Slider() {
|
||||
const data = state.findMany.data || [];
|
||||
const loading = state.findMany.loading;
|
||||
|
||||
// Duplicate slides for seamless infinite loop
|
||||
const slidesData = [...data, ...data, ...data];
|
||||
// Triple data untuk infinite loop (desktop only)
|
||||
const slidesData = mobile ? data : [...data, ...data, ...data];
|
||||
|
||||
// Auto-scroll animation untuk desktop
|
||||
useEffect(() => {
|
||||
if (loading || !containerRef.current || slidesData.length === 0) return;
|
||||
if (loading || !containerRef.current || data.length === 0 || mobile) return;
|
||||
|
||||
const container = containerRef.current;
|
||||
const slideWidth = container.scrollWidth / slidesData.length;
|
||||
const originalDataLength = data.length;
|
||||
const originalLength = data.length;
|
||||
|
||||
// Start from the middle set of slides
|
||||
scrollPositionRef.current = slideWidth * originalDataLength;
|
||||
container.scrollLeft = scrollPositionRef.current;
|
||||
// Start dari middle set
|
||||
scrollPosRef.current = slideWidth * originalLength;
|
||||
container.scrollLeft = scrollPosRef.current;
|
||||
|
||||
const animate = () => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const container = containerRef.current;
|
||||
const slideWidth = container.scrollWidth / slidesData.length;
|
||||
const timeSinceScroll = Date.now() - lastScrollRef.current;
|
||||
const isUserScrolling = timeSinceScroll < SCROLL_THRESHOLD;
|
||||
|
||||
// Check if user recently scrolled manually
|
||||
const timeSinceLastScroll = Date.now() - lastScrollTimeRef.current;
|
||||
const isUserScrolling = timeSinceLastScroll < 100;
|
||||
|
||||
// Only auto-scroll if user is not actively scrolling or dragging
|
||||
if (!isDraggingRef.current && !isUserScrolling) {
|
||||
const currentSpeed = isHoveredRef.current ? hoverSpeed : normalSpeed;
|
||||
scrollPositionRef.current += currentSpeed;
|
||||
const speed = isHoveredRef.current ? SPEED_HOVER : SPEED_NORMAL;
|
||||
scrollPosRef.current += speed;
|
||||
|
||||
// Reset position for infinite loop
|
||||
if (scrollPositionRef.current >= slideWidth * (originalDataLength * 2)) {
|
||||
scrollPositionRef.current -= slideWidth * originalDataLength;
|
||||
// Reset untuk infinite loop
|
||||
if (scrollPosRef.current >= slideWidth * (originalLength * 2)) {
|
||||
scrollPosRef.current -= slideWidth * originalLength;
|
||||
}
|
||||
if (scrollPosRef.current <= 0) {
|
||||
scrollPosRef.current += slideWidth * originalLength;
|
||||
}
|
||||
|
||||
if (scrollPositionRef.current <= 0) {
|
||||
scrollPositionRef.current += slideWidth * originalDataLength;
|
||||
}
|
||||
|
||||
container.scrollLeft = scrollPositionRef.current;
|
||||
container.scrollLeft = scrollPosRef.current;
|
||||
} else {
|
||||
// Sync scroll position when user is scrolling
|
||||
scrollPositionRef.current = container.scrollLeft;
|
||||
|
||||
// Apply momentum/velocity for smooth drag release
|
||||
scrollPosRef.current = container.scrollLeft;
|
||||
|
||||
// Momentum untuk drag release
|
||||
if (!isDraggingRef.current && Math.abs(velocityRef.current) > 0.1) {
|
||||
scrollPositionRef.current += velocityRef.current;
|
||||
velocityRef.current *= 0.95; // Decay velocity
|
||||
container.scrollLeft = scrollPositionRef.current;
|
||||
scrollPosRef.current += velocityRef.current;
|
||||
velocityRef.current *= VELOCITY_DECAY;
|
||||
container.scrollLeft = scrollPosRef.current;
|
||||
}
|
||||
}
|
||||
|
||||
animationFrameRef.current = requestAnimationFrame(animate);
|
||||
animFrameRef.current = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
animationFrameRef.current = requestAnimationFrame(animate);
|
||||
animFrameRef.current = requestAnimationFrame(animate);
|
||||
|
||||
return () => {
|
||||
if (animationFrameRef.current) {
|
||||
cancelAnimationFrame(animationFrameRef.current);
|
||||
if (animFrameRef.current) {
|
||||
cancelAnimationFrame(animFrameRef.current);
|
||||
}
|
||||
};
|
||||
}, [loading, slidesData.length, data.length, mobile]);
|
||||
}, [loading, data.length, mobile]);
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
isHoveredRef.current = true;
|
||||
if (!mobile) isHoveredRef.current = true;
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
isHoveredRef.current = false;
|
||||
isDraggingRef.current = false;
|
||||
if (!mobile) {
|
||||
isHoveredRef.current = false;
|
||||
isDraggingRef.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Mouse drag handlers
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
if (!containerRef.current || mobile) return;
|
||||
|
||||
isDraggingRef.current = true;
|
||||
startXRef.current = e.pageX - containerRef.current.offsetLeft;
|
||||
scrollLeftRef.current = containerRef.current.scrollLeft;
|
||||
velocityRef.current = 0;
|
||||
|
||||
containerRef.current.style.cursor = 'grabbing';
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: React.MouseEvent) => {
|
||||
if (!isDraggingRef.current || !containerRef.current) return;
|
||||
|
||||
if (!isDraggingRef.current || !containerRef.current || mobile) return;
|
||||
|
||||
e.preventDefault();
|
||||
const x = e.pageX - containerRef.current.offsetLeft;
|
||||
const walk = (x - startXRef.current) * 2;
|
||||
const newScrollLeft = scrollLeftRef.current - walk;
|
||||
|
||||
|
||||
velocityRef.current = containerRef.current.scrollLeft - newScrollLeft;
|
||||
|
||||
containerRef.current.scrollLeft = newScrollLeft;
|
||||
scrollPositionRef.current = newScrollLeft;
|
||||
lastScrollTimeRef.current = Date.now();
|
||||
scrollPosRef.current = newScrollLeft;
|
||||
lastScrollRef.current = Date.now();
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
if (!containerRef.current || mobile) return;
|
||||
|
||||
isDraggingRef.current = false;
|
||||
containerRef.current.style.cursor = 'grab';
|
||||
};
|
||||
|
||||
// Wheel scroll handler
|
||||
const handleWheel = (e: React.WheelEvent) => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
if (!containerRef.current || mobile) return;
|
||||
|
||||
e.preventDefault();
|
||||
containerRef.current.scrollLeft += e.deltaY;
|
||||
scrollPositionRef.current = containerRef.current.scrollLeft;
|
||||
lastScrollTimeRef.current = Date.now();
|
||||
scrollPosRef.current = containerRef.current.scrollLeft;
|
||||
lastScrollRef.current = Date.now();
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
@@ -211,37 +203,45 @@ function Slider() {
|
||||
onWheel={handleWheel}
|
||||
py="xl"
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
cursor: "grab",
|
||||
overflowX: mobile ? "auto" : "hidden",
|
||||
overflowY: "hidden",
|
||||
cursor: mobile ? "default" : "grab",
|
||||
userSelect: "none",
|
||||
position: "relative",
|
||||
WebkitOverflowScrolling: "touch",
|
||||
scrollbarWidth: "none",
|
||||
msOverflowStyle: "none",
|
||||
}}
|
||||
>
|
||||
{/* Blur edges effect */}
|
||||
<Box
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "120px",
|
||||
height: "100%",
|
||||
background: "linear-gradient(to right, rgba(249, 250, 251, 1), rgba(249, 250, 251, 0))",
|
||||
zIndex: 10,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
right: 0,
|
||||
width: "120px",
|
||||
height: "100%",
|
||||
background: "linear-gradient(to left, rgba(249, 250, 251, 1), rgba(249, 250, 251, 0))",
|
||||
zIndex: 10,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
/>
|
||||
{/* Blur edges - hanya untuk desktop */}
|
||||
{!mobile && (
|
||||
<>
|
||||
<Box
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "120px",
|
||||
height: "100%",
|
||||
background: "linear-gradient(to right, rgba(249, 250, 251, 1), rgba(249, 250, 251, 0))",
|
||||
zIndex: 10,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
right: 0,
|
||||
width: "120px",
|
||||
height: "100%",
|
||||
background: "linear-gradient(to left, rgba(249, 250, 251, 1), rgba(249, 250, 251, 0))",
|
||||
zIndex: 10,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Box
|
||||
style={{
|
||||
@@ -255,8 +255,8 @@ function Slider() {
|
||||
<Box
|
||||
key={`${item.id}-${index}`}
|
||||
style={{
|
||||
flex: `0 0 ${mobile ? "90%" : "calc(33.333% - 1rem)"}`,
|
||||
minWidth: mobile ? "90%" : "calc(33.333% - 1rem)",
|
||||
flex: `0 0 ${mobile ? "85%" : "calc(33.333% - 1rem)"}`,
|
||||
minWidth: mobile ? "85%" : "calc(33.333% - 1rem)",
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
@@ -272,12 +272,16 @@ function Slider() {
|
||||
overflow: "hidden",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.transform = "translateY(-8px) scale(1.02)";
|
||||
e.currentTarget.style.boxShadow = "0 12px 28px rgba(0,0,0,0.25)";
|
||||
if (!mobile) {
|
||||
e.currentTarget.style.transform = "translateY(-8px) scale(1.02)";
|
||||
e.currentTarget.style.boxShadow = "0 12px 28px rgba(0,0,0,0.25)";
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.transform = "translateY(0) scale(1)";
|
||||
e.currentTarget.style.boxShadow = "0 4px 12px rgba(0,0,0,0.15)";
|
||||
if (!mobile) {
|
||||
e.currentTarget.style.transform = "translateY(0) scale(1)";
|
||||
e.currentTarget.style.boxShadow = "0 4px 12px rgba(0,0,0,0.15)";
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
|
||||
@@ -13,7 +13,7 @@ function Page() {
|
||||
const state = useProxy(prestasiState.prestasiDesa);
|
||||
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;
|
||||
|
||||
|
||||
@@ -220,8 +220,9 @@ export default function ModernNewsNotification({
|
||||
...styles,
|
||||
position: "fixed",
|
||||
bottom: "100px",
|
||||
right: "24px",
|
||||
width: "380px",
|
||||
left: "24px",
|
||||
width: "90vw",
|
||||
maxWidth: 380,
|
||||
maxHeight: "500px",
|
||||
boxShadow: "0 8px 32px rgba(0,0,0,0.12)",
|
||||
borderRadius: "16px",
|
||||
@@ -290,7 +291,7 @@ export default function ModernNewsNotification({
|
||||
color={item.type === "berita" ? "blue" : "orange"}
|
||||
variant="light"
|
||||
>
|
||||
{item.type === "berita" ? "📰 Berita" : "📢 Pengumuman"}
|
||||
{item.type === "berita" ? "Berita" : "Pengumuman"}
|
||||
</Badge>
|
||||
<IconChevronRight size={16} color="#adb5bd" />
|
||||
</Group>
|
||||
@@ -321,8 +322,9 @@ export default function ModernNewsNotification({
|
||||
...styles,
|
||||
position: "fixed",
|
||||
bottom: "100px",
|
||||
right: "24px",
|
||||
width: "380px",
|
||||
left: "24px",
|
||||
width: "90vw",
|
||||
maxWidth: 380,
|
||||
boxShadow: "0 8px 32px rgba(0,0,0,0.15)",
|
||||
borderRadius: "12px",
|
||||
overflow: "hidden",
|
||||
@@ -350,7 +352,6 @@ export default function ModernNewsNotification({
|
||||
size="md"
|
||||
color={currentNews?.type === "berita" ? "blue" : "orange"}
|
||||
variant="light"
|
||||
leftSection={currentNews?.type === "berita" ? "📰" : "📢"}
|
||||
>
|
||||
{currentNews?.type === "berita"
|
||||
? "Berita Terbaru"
|
||||
|
||||
@@ -4,7 +4,7 @@ import indeksKepuasanState from "@/app/admin/(dashboard)/_state/landing-page/ind
|
||||
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 { useDisclosure, useShallowEffect } from "@mantine/hooks";
|
||||
import { useDisclosure, useMediaQuery, useShallowEffect } from "@mantine/hooks";
|
||||
import { useState } from "react";
|
||||
import { useProxy } from "valtio/utils";
|
||||
|
||||
@@ -25,6 +25,7 @@ function Kepuasan() {
|
||||
const [donutDataKelompokUmur, setDonutDataKelompokUmur] = useState<ChartDataItem[]>([]);
|
||||
const [barChartData, setBarChartData] = useState<Array<{ month: string; Responden: number }>>([]);
|
||||
const [opened, { open, close }] = useDisclosure(false)
|
||||
const isMobile = useMediaQuery("(max-width: 768px)");
|
||||
|
||||
const resetForm = () => {
|
||||
state.create.form = {
|
||||
@@ -41,7 +42,7 @@ function Kepuasan() {
|
||||
indeksKepuasanState.jenisKelaminResponden.findMany.load()
|
||||
indeksKepuasanState.pilihanRatingResponden.findMany.load()
|
||||
indeksKepuasanState.kelompokUmurResponden.findMany.load()
|
||||
},[])
|
||||
}, [])
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
@@ -82,13 +83,13 @@ function Kepuasan() {
|
||||
|
||||
// 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 +97,7 @@ function Kepuasan() {
|
||||
|
||||
// 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' },
|
||||
]);
|
||||
@@ -220,10 +221,13 @@ function Kepuasan() {
|
||||
<Box style={{ position: 'relative', width: '100%' }}>
|
||||
<Center>
|
||||
<PieChart
|
||||
withLabels
|
||||
withTooltip
|
||||
tooltipAnimationDuration={200}
|
||||
withLabels
|
||||
labelsPosition="inside" // 👈 ini yang penting!
|
||||
labelsType="percent"
|
||||
size={250} // Fixed size in pixels
|
||||
withLabelsLine
|
||||
size={isMobile ? 180 : 250} // 👈 kecilkan ukuran di mobile
|
||||
data={donutDataJenisKelamin}
|
||||
/>
|
||||
</Center>
|
||||
@@ -259,10 +263,10 @@ function Kepuasan() {
|
||||
withTooltip
|
||||
tooltipAnimationDuration={200}
|
||||
withLabels
|
||||
labelsPosition="outside"
|
||||
labelsPosition="inside" // 👈 ini yang penting!
|
||||
labelsType="percent"
|
||||
withLabelsLine
|
||||
size={250}
|
||||
size={isMobile ? 180 : 250} // 👈 kecilkan ukuran di mobile
|
||||
data={donutDataRating}
|
||||
/>
|
||||
</Center>
|
||||
@@ -302,10 +306,10 @@ function Kepuasan() {
|
||||
withTooltip
|
||||
tooltipAnimationDuration={200}
|
||||
withLabels
|
||||
labelsPosition="outside"
|
||||
labelsPosition="inside"// 👈 ini yang penting!
|
||||
labelsType="percent"
|
||||
withLabelsLine
|
||||
size={250}
|
||||
size={isMobile ? 180 : 250} // 👈 kecilkan ukuran di mobile
|
||||
data={donutDataKelompokUmur}
|
||||
/>
|
||||
</Center>
|
||||
@@ -494,6 +498,8 @@ function Kepuasan() {
|
||||
<PieChart
|
||||
withLabels
|
||||
withTooltip
|
||||
labelsPosition="inside"
|
||||
|
||||
labelsType="percent"
|
||||
size={200}
|
||||
data={donutDataJenisKelamin}
|
||||
@@ -531,7 +537,8 @@ function Kepuasan() {
|
||||
withTooltip
|
||||
tooltipAnimationDuration={200}
|
||||
withLabels
|
||||
labelsPosition="outside"
|
||||
|
||||
labelsPosition="inside"
|
||||
labelsType="percent"
|
||||
withLabelsLine
|
||||
size={200}
|
||||
@@ -574,7 +581,8 @@ function Kepuasan() {
|
||||
withTooltip
|
||||
tooltipAnimationDuration={200}
|
||||
withLabels
|
||||
labelsPosition="outside"
|
||||
|
||||
labelsPosition="inside"
|
||||
labelsType="percent"
|
||||
withLabelsLine
|
||||
size={190}
|
||||
@@ -610,7 +618,7 @@ function Kepuasan() {
|
||||
<TextInput
|
||||
label="Nama"
|
||||
type='text'
|
||||
placeholder="masukkan nama"
|
||||
placeholder="Masukkan nama"
|
||||
value={state.create.form.name}
|
||||
onChange={(val) => {
|
||||
state.create.form.name = val.currentTarget.value;
|
||||
@@ -619,7 +627,7 @@ function Kepuasan() {
|
||||
<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;
|
||||
|
||||
@@ -154,7 +154,7 @@ function LandingPage() {
|
||||
|
||||
return (
|
||||
<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">
|
||||
<Card radius="xl" bg={colors.grey[1]} p="lg" mt={10} shadow="xl">
|
||||
<Stack gap="xl">
|
||||
|
||||
@@ -25,25 +25,27 @@ const textHeading = {
|
||||
des: "Layanan adalah fitur yang membantu warga desa mengakses berbagai kebutuhan administrasi, informasi, dan bantuan secara cepat, mudah, dan transparan. Dengan fitur ini, semua layanan desa ada dalam genggaman Anda!",
|
||||
};
|
||||
|
||||
const HEIGHT = 720;
|
||||
|
||||
function Layanan() {
|
||||
return (
|
||||
<Stack pos={"relative"} bg={colors.grey[1]} gap={"xl"} py={"md"}>
|
||||
<Container w={{ base: "100%", md: "80%" }} p={"md"}>
|
||||
<Stack align="center" gap={"0"}>
|
||||
<Stack pos="relative" bg={colors.grey[1]} gap="xl" py="md">
|
||||
<Container w={{ base: "100%", md: "80%" }} p="md">
|
||||
<Stack align="center" gap="0">
|
||||
<Text
|
||||
fw={"bold"}
|
||||
fw="bold"
|
||||
c={colors["blue-button"]}
|
||||
fz={{ base: "1.8rem", md: "3.4rem" }}
|
||||
>
|
||||
{textHeading.title}
|
||||
</Text>
|
||||
<Text ta={"center"} fz={{ base: "1rem", md: "1.3rem" }}>
|
||||
<Text ta="center" fz={{ base: "1rem", md: "1.3rem" }}>
|
||||
{textHeading.des}
|
||||
</Text>
|
||||
<Box p={"md"}>
|
||||
<Box p="md">
|
||||
<Button
|
||||
component={Link}
|
||||
href={"/darmasaba/desa/layanan"}
|
||||
href="/darmasaba/desa/layanan"
|
||||
variant="filled"
|
||||
bg={colors["blue-button"]}
|
||||
radius={100}
|
||||
@@ -59,30 +61,26 @@ function Layanan() {
|
||||
);
|
||||
}
|
||||
|
||||
const height = 720;
|
||||
|
||||
function Slider() {
|
||||
const state = useProxy(stateLayananDesa);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const mobile = useMediaQuery("(max-width: 768px)", false);
|
||||
const router = useTransitionRouter();
|
||||
|
||||
// Refs for smooth animation
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const scrollPositionRef = useRef(0);
|
||||
const animationFrameRef = useRef<number>(0);
|
||||
const scrollPosRef = useRef(0);
|
||||
const animFrameRef = useRef<number>(0);
|
||||
const isHoveredRef = useRef(false);
|
||||
|
||||
// Refs for drag functionality
|
||||
const isDraggingRef = useRef(false);
|
||||
const startXRef = useRef(0);
|
||||
const scrollLeftRef = useRef(0);
|
||||
const velocityRef = useRef(0);
|
||||
const lastScrollTimeRef = useRef(0);
|
||||
const lastScrollRef = useRef(0);
|
||||
|
||||
// Speed configuration
|
||||
const normalSpeed = 1.0; // pixels per frame
|
||||
const hoverSpeed = 0.3; // slower speed on hover
|
||||
const SPEED_NORMAL = 1.0;
|
||||
const SPEED_HOVER = 0.3;
|
||||
const VELOCITY_DECAY = 0.95;
|
||||
const SCROLL_THRESHOLD = 100;
|
||||
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
@@ -100,126 +98,118 @@ function Slider() {
|
||||
|
||||
const data = state.suratKeterangan.findMany.data || [];
|
||||
|
||||
// Duplicate slides for seamless infinite loop
|
||||
// We need at least 3x the data for smooth infinite scrolling
|
||||
const slidesData = [...data, ...data, ...data];
|
||||
// Triple data untuk infinite loop (desktop only)
|
||||
const slidesData = mobile ? data : [...data, ...data, ...data];
|
||||
|
||||
// Auto-scroll animation untuk desktop
|
||||
useEffect(() => {
|
||||
if (loading || !containerRef.current || slidesData.length === 0) return;
|
||||
if (loading || !containerRef.current || data.length === 0 || mobile) return;
|
||||
|
||||
const container = containerRef.current;
|
||||
const slideWidth = container.scrollWidth / slidesData.length;
|
||||
const originalDataLength = data.length;
|
||||
const originalLength = data.length;
|
||||
|
||||
// Start from the middle set of slides
|
||||
scrollPositionRef.current = slideWidth * originalDataLength;
|
||||
container.scrollLeft = scrollPositionRef.current;
|
||||
// Start dari middle set
|
||||
scrollPosRef.current = slideWidth * originalLength;
|
||||
container.scrollLeft = scrollPosRef.current;
|
||||
|
||||
const animate = () => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const container = containerRef.current;
|
||||
const slideWidth = container.scrollWidth / slidesData.length;
|
||||
const timeSinceScroll = Date.now() - lastScrollRef.current;
|
||||
const isUserScrolling = timeSinceScroll < SCROLL_THRESHOLD;
|
||||
|
||||
// Check if user recently scrolled manually
|
||||
const timeSinceLastScroll = Date.now() - lastScrollTimeRef.current;
|
||||
const isUserScrolling = timeSinceLastScroll < 100;
|
||||
|
||||
// Only auto-scroll if user is not actively scrolling or dragging
|
||||
if (!isDraggingRef.current && !isUserScrolling) {
|
||||
const currentSpeed = isHoveredRef.current ? hoverSpeed : normalSpeed;
|
||||
scrollPositionRef.current += currentSpeed;
|
||||
const speed = isHoveredRef.current ? SPEED_HOVER : SPEED_NORMAL;
|
||||
scrollPosRef.current += speed;
|
||||
|
||||
// Reset position for infinite loop
|
||||
if (scrollPositionRef.current >= slideWidth * (originalDataLength * 2)) {
|
||||
scrollPositionRef.current -= slideWidth * originalDataLength;
|
||||
// Reset untuk infinite loop
|
||||
if (scrollPosRef.current >= slideWidth * (originalLength * 2)) {
|
||||
scrollPosRef.current -= slideWidth * originalLength;
|
||||
}
|
||||
if (scrollPosRef.current <= 0) {
|
||||
scrollPosRef.current += slideWidth * originalLength;
|
||||
}
|
||||
|
||||
if (scrollPositionRef.current <= 0) {
|
||||
scrollPositionRef.current += slideWidth * originalDataLength;
|
||||
}
|
||||
|
||||
container.scrollLeft = scrollPositionRef.current;
|
||||
container.scrollLeft = scrollPosRef.current;
|
||||
} else {
|
||||
// Sync scroll position when user is scrolling
|
||||
scrollPositionRef.current = container.scrollLeft;
|
||||
scrollPosRef.current = container.scrollLeft;
|
||||
|
||||
// Apply momentum/velocity for smooth drag release
|
||||
// Momentum untuk drag release
|
||||
if (!isDraggingRef.current && Math.abs(velocityRef.current) > 0.1) {
|
||||
scrollPositionRef.current += velocityRef.current;
|
||||
velocityRef.current *= 0.95; // Decay velocity
|
||||
container.scrollLeft = scrollPositionRef.current;
|
||||
scrollPosRef.current += velocityRef.current;
|
||||
velocityRef.current *= VELOCITY_DECAY;
|
||||
container.scrollLeft = scrollPosRef.current;
|
||||
}
|
||||
}
|
||||
|
||||
animationFrameRef.current = requestAnimationFrame(animate);
|
||||
animFrameRef.current = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
animationFrameRef.current = requestAnimationFrame(animate);
|
||||
animFrameRef.current = requestAnimationFrame(animate);
|
||||
|
||||
return () => {
|
||||
if (animationFrameRef.current) {
|
||||
cancelAnimationFrame(animationFrameRef.current);
|
||||
if (animFrameRef.current) {
|
||||
cancelAnimationFrame(animFrameRef.current);
|
||||
}
|
||||
};
|
||||
}, [loading, slidesData.length, data.length, mobile]);
|
||||
}, [loading, data.length, mobile]);
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
isHoveredRef.current = true;
|
||||
if (!mobile) isHoveredRef.current = true;
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
isHoveredRef.current = false;
|
||||
isDraggingRef.current = false;
|
||||
if (!mobile) {
|
||||
isHoveredRef.current = false;
|
||||
isDraggingRef.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Mouse drag handlers
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
if (!containerRef.current) return;
|
||||
if (!containerRef.current || mobile) return;
|
||||
|
||||
isDraggingRef.current = true;
|
||||
startXRef.current = e.pageX - containerRef.current.offsetLeft;
|
||||
scrollLeftRef.current = containerRef.current.scrollLeft;
|
||||
velocityRef.current = 0;
|
||||
|
||||
containerRef.current.style.cursor = 'grabbing';
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: React.MouseEvent) => {
|
||||
if (!isDraggingRef.current || !containerRef.current) return;
|
||||
if (!isDraggingRef.current || !containerRef.current || mobile) return;
|
||||
|
||||
e.preventDefault();
|
||||
const x = e.pageX - containerRef.current.offsetLeft;
|
||||
const walk = (x - startXRef.current) * 2; // Multiply for faster scroll
|
||||
const walk = (x - startXRef.current) * 2;
|
||||
const newScrollLeft = scrollLeftRef.current - walk;
|
||||
|
||||
// Calculate velocity for momentum
|
||||
velocityRef.current = containerRef.current.scrollLeft - newScrollLeft;
|
||||
|
||||
containerRef.current.scrollLeft = newScrollLeft;
|
||||
scrollPositionRef.current = newScrollLeft;
|
||||
lastScrollTimeRef.current = Date.now();
|
||||
scrollPosRef.current = newScrollLeft;
|
||||
lastScrollRef.current = Date.now();
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
if (!containerRef.current) return;
|
||||
if (!containerRef.current || mobile) return;
|
||||
|
||||
isDraggingRef.current = false;
|
||||
containerRef.current.style.cursor = 'grab';
|
||||
};
|
||||
|
||||
// Wheel scroll handler
|
||||
const handleWheel = (e: React.WheelEvent) => {
|
||||
if (!containerRef.current) return;
|
||||
if (!containerRef.current || mobile) return;
|
||||
|
||||
e.preventDefault();
|
||||
containerRef.current.scrollLeft += e.deltaY;
|
||||
scrollPositionRef.current = containerRef.current.scrollLeft;
|
||||
lastScrollTimeRef.current = Date.now();
|
||||
scrollPosRef.current = containerRef.current.scrollLeft;
|
||||
lastScrollRef.current = Date.now();
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <Skeleton height={height} />;
|
||||
return <Skeleton height={HEIGHT} />;
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
@@ -242,9 +232,13 @@ function Slider() {
|
||||
onMouseUp={handleMouseUp}
|
||||
onWheel={handleWheel}
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
cursor: "grab",
|
||||
overflowX: mobile ? "auto" : "hidden",
|
||||
overflowY: "hidden",
|
||||
cursor: mobile ? "default" : "grab",
|
||||
userSelect: "none",
|
||||
WebkitOverflowScrolling: "touch",
|
||||
scrollbarWidth: "none",
|
||||
msOverflowStyle: "none",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
@@ -259,13 +253,13 @@ function Slider() {
|
||||
<Box
|
||||
key={`${item.id}-${index}`}
|
||||
style={{
|
||||
flex: `0 0 ${mobile ? "100%" : "calc(33.333% - 1rem)"}`,
|
||||
minWidth: mobile ? "100%" : "calc(33.333% - 1rem)",
|
||||
flex: `0 0 ${mobile ? "90%" : "calc(33.333% - 1rem)"}`,
|
||||
minWidth: mobile ? "90%" : "calc(33.333% - 1rem)",
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
h={height}
|
||||
pos={"relative"}
|
||||
h={HEIGHT}
|
||||
pos="relative"
|
||||
style={{
|
||||
backgroundImage: `url(${item.image?.link})`,
|
||||
backgroundSize: "cover",
|
||||
@@ -280,23 +274,23 @@ function Slider() {
|
||||
borderRadius: 8,
|
||||
zIndex: 0,
|
||||
}}
|
||||
pos={"absolute"}
|
||||
w={"100%"}
|
||||
h={"100%"}
|
||||
pos="absolute"
|
||||
w="100%"
|
||||
h="100%"
|
||||
bg={colors.trans.dark[2]}
|
||||
/>
|
||||
<Stack
|
||||
justify="space-between"
|
||||
h={"100%"}
|
||||
h="100%"
|
||||
gap={0}
|
||||
p={"lg"}
|
||||
pos={"relative"}
|
||||
p="lg"
|
||||
pos="relative"
|
||||
>
|
||||
<Box p={"lg"}>
|
||||
<Box p="lg">
|
||||
<Text
|
||||
fw={"bold"}
|
||||
c={"white"}
|
||||
size={"3.5rem"}
|
||||
fw="bold"
|
||||
c="white"
|
||||
fz={{base: "xl", md: "3.5rem"}}
|
||||
style={{
|
||||
textAlign: "center",
|
||||
}}
|
||||
@@ -310,7 +304,7 @@ function Slider() {
|
||||
router.push(`/darmasaba/desa/layanan/${item.id}`)
|
||||
}
|
||||
px={46}
|
||||
radius={"100"}
|
||||
radius="100"
|
||||
size="md"
|
||||
bg={colors["blue-button"]}
|
||||
>
|
||||
|
||||
@@ -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 { IconAward, IconArrowRight, IconPlayerPlay } from "@tabler/icons-react";
|
||||
import { useTransitionRouter } from 'next-view-transitions';
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useProxy } from "valtio/utils";
|
||||
import { useMediaQuery } from "@mantine/hooks";
|
||||
|
||||
@@ -15,16 +15,35 @@ function Penghargaan() {
|
||||
const isMobile = useMediaQuery('(max-width: 768px)');
|
||||
const [isVideoLoaded, setIsVideoLoaded] = useState(false);
|
||||
const [showVideo, setShowVideo] = useState(true);
|
||||
const [videoError, setVideoError] = useState(false);
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
|
||||
// Opsional: deteksi iOS
|
||||
const isIOS = typeof window !== 'undefined' && /iPad|iPhone|iPod/.test(navigator.userAgent);
|
||||
// Deteksi iOS dengan lebih akurat
|
||||
const isIOS = typeof window !== 'undefined' && (
|
||||
/iPad|iPhone|iPod/.test(navigator.userAgent) ||
|
||||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1) // iPad dengan iPadOS 13+
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isIOS) {
|
||||
// Di iOS, jangan andalkan autoplay — tampilkan kontrol
|
||||
setShowVideo(false);
|
||||
// Di iOS, coba autoplay dulu, kalau gagal tampilkan fallback
|
||||
if (isIOS && videoRef.current) {
|
||||
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(() => {
|
||||
const loadData = async () => {
|
||||
@@ -38,42 +57,99 @@ function Penghargaan() {
|
||||
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
|
||||
const data = state.findMany.data?.slice(0, isMobile ? 1 : 3);
|
||||
|
||||
return (
|
||||
<Stack pos="relative" h="auto" mih={{ base: 500, md: 720 }}>
|
||||
{showVideo ? (
|
||||
<Stack pos="relative" h="auto" mih={{ base: 500, md: 720 }} style={{ overflow: 'hidden' }}>
|
||||
{/* Video Layer */}
|
||||
{showVideo && !videoError && (
|
||||
<video
|
||||
ref={videoRef}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
webkit-playsinline="true"
|
||||
preload="auto"
|
||||
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" />
|
||||
</video>
|
||||
) : (
|
||||
// Fallback: tampilkan poster + play button
|
||||
)}
|
||||
|
||||
{/* Fallback Image + Play Button */}
|
||||
{(!showVideo || videoError) && (
|
||||
<Box
|
||||
onClick={() => setShowVideo(true)}
|
||||
onClick={handlePlayVideo}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundImage: "url('/mangupuraaward.jpeg')",
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
cursor: 'pointer',
|
||||
zIndex: 0,
|
||||
}}
|
||||
>
|
||||
<Center h="100%">
|
||||
<ActionIcon size="lg" radius="xl" color="white">
|
||||
<IconPlayerPlay size={32} />
|
||||
<Center
|
||||
style={{
|
||||
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>
|
||||
</Center>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Overlay Gradient + Content */}
|
||||
<Box
|
||||
style={{
|
||||
width: "100%",
|
||||
@@ -152,4 +228,4 @@ function Penghargaan() {
|
||||
);
|
||||
}
|
||||
|
||||
export default Penghargaan;
|
||||
export default Penghargaan;
|
||||
@@ -38,15 +38,15 @@ export default function Page() {
|
||||
const [hasNewContent, setHasNewContent] = useState(false);
|
||||
const [newItemCount, setNewItemCount] = useState(0);
|
||||
|
||||
const lastBeritaId = useRef<string | null>(null);
|
||||
const lastPengumumanId = useRef<string | null>(null);
|
||||
const lastBeritaTimestamp = useRef<string | null>(null);
|
||||
const lastPengumumanTimestamp = useRef<string | null>(null);
|
||||
|
||||
// Inisialisasi dari localStorage
|
||||
useEffect(() => {
|
||||
const savedBerita = localStorage.getItem("lastSeenBeritaId");
|
||||
const savedPengumuman = localStorage.getItem("lastSeenPengumumanId");
|
||||
if (savedBerita) lastBeritaId.current = savedBerita;
|
||||
if (savedPengumuman) lastPengumumanId.current = savedPengumuman;
|
||||
const savedBeritaTs = localStorage.getItem("lastSeenBeritaTs");
|
||||
const savedPengumumanTs = localStorage.getItem("lastSeenPengumumanTs");
|
||||
if (savedBeritaTs) lastBeritaTimestamp.current = savedBeritaTs;
|
||||
if (savedPengumumanTs) lastPengumumanTimestamp.current = savedPengumumanTs;
|
||||
}, []);
|
||||
|
||||
// Load data utama (untuk card)
|
||||
@@ -70,28 +70,49 @@ export default function Page() {
|
||||
if (result.success && Array.isArray(result.news)) {
|
||||
const news = result.news as NewsItem[];
|
||||
|
||||
// Ambil ID terbaru
|
||||
const latestBerita = news.find((n) => n.type === "berita");
|
||||
const latestPengumuman = news.find((n) => n.type === "pengumuman");
|
||||
|
||||
const isNewBerita = latestBerita && lastBeritaId.current !== null && latestBerita.id !== lastBeritaId.current;
|
||||
const isNewPengumuman = latestPengumuman && lastPengumumanId.current !== null && latestPengumuman.id !== lastPengumumanId.current;
|
||||
const latestBeritaTs = latestBerita?.timestamp
|
||||
? new Date(latestBerita.timestamp).toISOString()
|
||||
: null;
|
||||
const latestPengumumanTs = latestPengumuman?.timestamp
|
||||
? new Date(latestPengumuman.timestamp).toISOString()
|
||||
: null;
|
||||
|
||||
// Simpan ID terbaru ke ref
|
||||
if (latestBerita) lastBeritaId.current = (latestBerita.id);
|
||||
if (latestPengumuman) lastPengumumanId.current = (latestPengumuman.id);
|
||||
// Inisialisasi flag
|
||||
let isNewBerita = false;
|
||||
let isNewPengumuman = false;
|
||||
|
||||
// Jika ini bukan inisialisasi pertama, tampilkan notifikasi
|
||||
if (lastBeritaId.current !== null || lastPengumumanId.current !== null) {
|
||||
if (isNewBerita || isNewPengumuman) {
|
||||
const count = (isNewBerita ? 1 : 0) + (isNewPengumuman ? 1 : 0);
|
||||
setNewItemCount(count);
|
||||
setHasNewContent(true);
|
||||
// Deteksi berita baru
|
||||
if (latestBeritaTs) {
|
||||
if (lastBeritaTimestamp.current === null) {
|
||||
// Pertama kali: simpan tanpa notifikasi
|
||||
lastBeritaTimestamp.current = latestBeritaTs;
|
||||
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));
|
||||
if (latestPengumuman) localStorage.setItem("lastSeenPengumumanId", (latestPengumuman.id));
|
||||
}
|
||||
|
||||
// Deteksi pengumuman baru
|
||||
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);
|
||||
@@ -113,13 +134,17 @@ export default function Page() {
|
||||
}, []);
|
||||
|
||||
const handleSeen = () => {
|
||||
setHasNewContent(false);
|
||||
setNewItemCount(0);
|
||||
const latestBerita = notificationNews.find(n => n.type === "berita");
|
||||
const latestPengumuman = notificationNews.find(n => n.type === "pengumuman");
|
||||
if (latestBerita) localStorage.setItem("lastSeenBeritaId", String(latestBerita.id));
|
||||
if (latestPengumuman) localStorage.setItem("lastSeenPengumumanId", String(latestPengumuman.id));
|
||||
};
|
||||
setHasNewContent(false);
|
||||
setNewItemCount(0);
|
||||
const latestBerita = notificationNews.find(n => n.type === "berita");
|
||||
const latestPengumuman = notificationNews.find(n => n.type === "pengumuman");
|
||||
if (latestBerita) {
|
||||
localStorage.setItem("lastSeenBeritaTs", new Date(latestBerita.timestamp!).toISOString());
|
||||
}
|
||||
if (latestPengumuman) {
|
||||
localStorage.setItem("lastSeenPengumumanTs", new Date(latestPengumuman.timestamp!).toISOString());
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box id="page-root">
|
||||
|
||||
Reference in New Issue
Block a user