Compare commits
10 Commits
nico/3-des
...
nico/12-de
| Author | SHA1 | Date | |
|---|---|---|---|
| f6f77d9e35 | |||
| a00481152c | |||
| 242ea86f77 | |||
| 99c2c9c6d7 | |||
| ac2fc1a705 | |||
| 9dbe172165 | |||
| cc318d4d54 | |||
| dcb8017594 | |||
| ec3ad12531 | |||
| dad44c0537 |
@@ -828,11 +828,11 @@ model DokterdanTenagaMedis {
|
||||
name String
|
||||
specialist String
|
||||
jadwal String
|
||||
jadwalLibur String
|
||||
jamBukaOperasional String
|
||||
jamTutupOperasional String
|
||||
jamBukaLibur String
|
||||
jamTutupLibur String
|
||||
jadwalLibur String?
|
||||
jamBukaOperasional String?
|
||||
jamTutupOperasional String?
|
||||
jamBukaLibur String?
|
||||
jamTutupLibur String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime @default(now())
|
||||
|
||||
BIN
public/mangupuraaward.jpeg
Normal file
BIN
public/mangupuraaward.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 177 KiB |
@@ -6,145 +6,176 @@ import { z } from "zod";
|
||||
|
||||
const templateForm = z.object({
|
||||
name: z.string().min(3, "Nama minimal 3 karakter"),
|
||||
nik: z.string().min(3, "NIK minimal 3 karakter"),
|
||||
notelp: z.string().min(3, "Nomor Telepon minimal 3 karakter"),
|
||||
nik: z
|
||||
.string()
|
||||
.min(3, "NIK minimal 3 karakter")
|
||||
.max(16, "NIK maksimal 16 angka"),
|
||||
notelp: z
|
||||
.string()
|
||||
.min(3, "Nomor Telepon minimal 3 karakter")
|
||||
.max(15, "Nomor Telepon maksimal 15 angka"),
|
||||
alamat: z.string().min(3, "Alamat minimal 3 karakter"),
|
||||
email: z.string().min(3, "Email minimal 3 karakter"),
|
||||
jenisInformasiDimintaId: z.string().nonempty(),
|
||||
caraMemperolehInformasiId: z.string().nonempty(),
|
||||
caraMemperolehSalinanInformasiId: z.string().nonempty(),
|
||||
})
|
||||
});
|
||||
|
||||
const jenisInformasiDiminta = proxy({
|
||||
findMany: {
|
||||
data: null as
|
||||
| null
|
||||
| Prisma.JenisInformasiDimintaGetPayload<{ omit: { isActive: true } }>[],
|
||||
async load(){
|
||||
const res = await ApiFetch.api.ppid.permohonaninformasipublik.jenisInformasi["find-many"].get();
|
||||
if (res.status === 200) {
|
||||
jenisInformasiDiminta.findMany.data = res.data?.data ?? [];
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
findMany: {
|
||||
data: null as
|
||||
| null
|
||||
| Prisma.JenisInformasiDimintaGetPayload<{ omit: { isActive: true } }>[],
|
||||
async load() {
|
||||
const res =
|
||||
await ApiFetch.api.ppid.permohonaninformasipublik.jenisInformasi[
|
||||
"find-many"
|
||||
].get();
|
||||
if (res.status === 200) {
|
||||
jenisInformasiDiminta.findMany.data = res.data?.data ?? [];
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const caraMemperolehInformasi = proxy({
|
||||
findMany: {
|
||||
data: null as
|
||||
| null
|
||||
| Prisma.CaraMemperolehInformasiGetPayload<{ omit: { isActive: true } }>[],
|
||||
async load() {
|
||||
const res = await ApiFetch.api.ppid.permohonaninformasipublik.memperolehInformasi["find-many"].get();
|
||||
if (res.status === 200) {
|
||||
caraMemperolehInformasi.findMany.data = res.data?.data ?? [];
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
findMany: {
|
||||
data: null as
|
||||
| null
|
||||
| Prisma.CaraMemperolehInformasiGetPayload<{
|
||||
omit: { isActive: true };
|
||||
}>[],
|
||||
async load() {
|
||||
const res =
|
||||
await ApiFetch.api.ppid.permohonaninformasipublik.memperolehInformasi[
|
||||
"find-many"
|
||||
].get();
|
||||
if (res.status === 200) {
|
||||
caraMemperolehInformasi.findMany.data = res.data?.data ?? [];
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const caraMemperolehSalinanInformasi = proxy({
|
||||
findMany: {
|
||||
data: null as
|
||||
| null
|
||||
| Prisma.CaraMemperolehSalinanInformasiGetPayload<{ omit: { isActive: true } }>[],
|
||||
async load() {
|
||||
const res = await ApiFetch.api.ppid.permohonaninformasipublik.salinanInformasi["find-many"].get();
|
||||
if (res.status === 200) {
|
||||
caraMemperolehSalinanInformasi.findMany.data = res.data?.data ?? [];
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
console.log(caraMemperolehSalinanInformasi)
|
||||
findMany: {
|
||||
data: null as
|
||||
| null
|
||||
| Prisma.CaraMemperolehSalinanInformasiGetPayload<{
|
||||
omit: { isActive: true };
|
||||
}>[],
|
||||
async load() {
|
||||
const res =
|
||||
await ApiFetch.api.ppid.permohonaninformasipublik.salinanInformasi[
|
||||
"find-many"
|
||||
].get();
|
||||
if (res.status === 200) {
|
||||
caraMemperolehSalinanInformasi.findMany.data = res.data?.data ?? [];
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
console.log(caraMemperolehSalinanInformasi);
|
||||
|
||||
type PermohonanInformasiPublikForm = Prisma.PermohonanInformasiPublikGetPayload<{
|
||||
type PermohonanInformasiPublikForm =
|
||||
Prisma.PermohonanInformasiPublikGetPayload<{
|
||||
select: {
|
||||
name: true;
|
||||
nik: true;
|
||||
notelp: true;
|
||||
alamat: true;
|
||||
email: true;
|
||||
jenisInformasiDimintaId: true;
|
||||
caraMemperolehInformasiId: true;
|
||||
caraMemperolehSalinanInformasiId: true;
|
||||
name: true;
|
||||
nik: true;
|
||||
notelp: true;
|
||||
alamat: true;
|
||||
email: true;
|
||||
jenisInformasiDimintaId: true;
|
||||
caraMemperolehInformasiId: true;
|
||||
caraMemperolehSalinanInformasiId: true;
|
||||
};
|
||||
}>;
|
||||
}>;
|
||||
|
||||
const statepermohonanInformasiPublik = proxy({
|
||||
create: {
|
||||
form: {} as PermohonanInformasiPublikForm,
|
||||
loading: false,
|
||||
async create(){
|
||||
const cek = templateForm.safeParse(statepermohonanInformasiPublik.create.form);
|
||||
if(!cek.success) {
|
||||
const err = `[${cek.error.issues
|
||||
.map((v) => `${v.path.join(".")}`)
|
||||
.join("\n")}] required`;
|
||||
return toast.error(err);
|
||||
}
|
||||
try {
|
||||
statepermohonanInformasiPublik.create.loading = true;
|
||||
const res = await ApiFetch.api.ppid.permohonaninformasipublik["create"].post(statepermohonanInformasiPublik.create.form);
|
||||
if (res.status === 200) {
|
||||
statepermohonanInformasiPublik.findMany.load();
|
||||
return toast.success("Sukses menambahkan");
|
||||
}
|
||||
return toast.error("failed create");
|
||||
} catch (error) {
|
||||
console.log((error as Error).message);
|
||||
} finally {
|
||||
statepermohonanInformasiPublik.create.loading = false;
|
||||
}
|
||||
create: {
|
||||
form: {} as PermohonanInformasiPublikForm,
|
||||
loading: false,
|
||||
async create() {
|
||||
const cek = templateForm.safeParse(
|
||||
statepermohonanInformasiPublik.create.form
|
||||
);
|
||||
|
||||
if (!cek.success) {
|
||||
toast.error(cek.error.issues.map((i) => i.message).join("\n"));
|
||||
return false; // ⬅️ tambahkan return false
|
||||
}
|
||||
|
||||
try {
|
||||
statepermohonanInformasiPublik.create.loading = true;
|
||||
const res = await ApiFetch.api.ppid.permohonaninformasipublik[
|
||||
"create"
|
||||
].post(statepermohonanInformasiPublik.create.form);
|
||||
|
||||
if (res.data?.success === false) {
|
||||
toast.error(res.data?.message);
|
||||
return false; // ⬅️ gagal
|
||||
}
|
||||
|
||||
toast.success("Sukses menambahkan");
|
||||
return true; // ⬅️ sukses
|
||||
} catch {
|
||||
toast.error("Terjadi kesalahan server");
|
||||
return false;
|
||||
} finally {
|
||||
statepermohonanInformasiPublik.create.loading = false;
|
||||
}
|
||||
},
|
||||
findMany: {
|
||||
data: null as
|
||||
| Prisma.PermohonanInformasiPublikGetPayload<{ include: {
|
||||
caraMemperolehSalinanInformasi: true,
|
||||
jenisInformasiDiminta: true,
|
||||
caraMemperolehInformasi: true,
|
||||
} }>[]
|
||||
| null,
|
||||
async load() {
|
||||
const res = await ApiFetch.api.ppid.permohonaninformasipublik["find-many"].get();
|
||||
if (res.status === 200) {
|
||||
statepermohonanInformasiPublik.findMany.data = res.data?.data ?? [];
|
||||
}
|
||||
}
|
||||
},
|
||||
findUnique: {
|
||||
data: null as Prisma.PermohonanInformasiPublikGetPayload<{
|
||||
},
|
||||
findMany: {
|
||||
data: null as
|
||||
| Prisma.PermohonanInformasiPublikGetPayload<{
|
||||
include: {
|
||||
jenisInformasiDiminta: true,
|
||||
caraMemperolehInformasi: true,
|
||||
caraMemperolehSalinanInformasi: true,
|
||||
caraMemperolehSalinanInformasi: true;
|
||||
jenisInformasiDiminta: true;
|
||||
caraMemperolehInformasi: true;
|
||||
};
|
||||
}> | null,
|
||||
async load(id: string) {
|
||||
try {
|
||||
const res = await fetch(`/api/ppid/permohonaninformasipublik/${id}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
statepermohonanInformasiPublik.findUnique.data = data.data ?? null;
|
||||
} else {
|
||||
console.error("Failed to fetch program inovasi:", res.statusText);
|
||||
statepermohonanInformasiPublik.findUnique.data = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching program inovasi:", error);
|
||||
statepermohonanInformasiPublik.findUnique.data = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
})
|
||||
}>[]
|
||||
| null,
|
||||
async load() {
|
||||
const res = await ApiFetch.api.ppid.permohonaninformasipublik[
|
||||
"find-many"
|
||||
].get();
|
||||
if (res.status === 200) {
|
||||
statepermohonanInformasiPublik.findMany.data = res.data?.data ?? [];
|
||||
}
|
||||
},
|
||||
},
|
||||
findUnique: {
|
||||
data: null as Prisma.PermohonanInformasiPublikGetPayload<{
|
||||
include: {
|
||||
jenisInformasiDiminta: true;
|
||||
caraMemperolehInformasi: true;
|
||||
caraMemperolehSalinanInformasi: true;
|
||||
};
|
||||
}> | null,
|
||||
async load(id: string) {
|
||||
try {
|
||||
const res = await fetch(`/api/ppid/permohonaninformasipublik/${id}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
statepermohonanInformasiPublik.findUnique.data = data.data ?? null;
|
||||
} else {
|
||||
console.error("Failed to fetch program inovasi:", res.statusText);
|
||||
statepermohonanInformasiPublik.findUnique.data = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching program inovasi:", error);
|
||||
statepermohonanInformasiPublik.findUnique.data = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const statepermohonanInformasiPublikForm = proxy({
|
||||
statepermohonanInformasiPublik,
|
||||
jenisInformasiDiminta,
|
||||
caraMemperolehInformasi,
|
||||
caraMemperolehSalinanInformasi,
|
||||
})
|
||||
statepermohonanInformasiPublik,
|
||||
jenisInformasiDiminta,
|
||||
caraMemperolehInformasi,
|
||||
caraMemperolehSalinanInformasi,
|
||||
});
|
||||
|
||||
export default statepermohonanInformasiPublikForm;
|
||||
|
||||
@@ -5,82 +5,99 @@ import { proxy } from "valtio";
|
||||
import { z } from "zod";
|
||||
|
||||
const templateForm = z.object({
|
||||
name: z.string().min(3, "Nama minimal 3 karakter"),
|
||||
email: z.string().min(3, "Email minimal 3 karakter"),
|
||||
notelp: z.string().min(3, "Nomor Telepon minimal 3 karakter"),
|
||||
alasan: z.string().min(3, "Alasan minimal 3 karakter"),
|
||||
})
|
||||
name: z.string().min(3, "Nama minimal 3 karakter"),
|
||||
email: z.string().min(3, "Email minimal 3 karakter"),
|
||||
notelp: z
|
||||
.string()
|
||||
.min(3, "Nomor Telepon minimal 3 karakter")
|
||||
.max(15, "Nomor Telepon maksimal 15 angka"),
|
||||
alasan: z.string().min(3, "Alasan minimal 3 karakter"),
|
||||
});
|
||||
|
||||
type PermohonanKeberatanInformasiForm = Prisma.FormulirPermohonanKeberatanGetPayload<{
|
||||
type PermohonanKeberatanInformasiForm =
|
||||
Prisma.FormulirPermohonanKeberatanGetPayload<{
|
||||
select: {
|
||||
name: true;
|
||||
email: true;
|
||||
notelp: true;
|
||||
alasan: true;
|
||||
name: true;
|
||||
email: true;
|
||||
notelp: true;
|
||||
alasan: true;
|
||||
};
|
||||
}>;
|
||||
}>;
|
||||
|
||||
const permohonanKeberatanInformasi = proxy({
|
||||
create: {
|
||||
form: {} as PermohonanKeberatanInformasiForm,
|
||||
loading: false,
|
||||
async create(){
|
||||
const cek = templateForm.safeParse(permohonanKeberatanInformasi.create.form);
|
||||
if(!cek.success) {
|
||||
const err = `[${cek.error.issues
|
||||
.map((v) => `${v.path.join(".")}`)
|
||||
.join("\n")}] required`;
|
||||
return toast.error(err);
|
||||
}
|
||||
try {
|
||||
permohonanKeberatanInformasi.create.loading = true;
|
||||
const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik["create"].post(permohonanKeberatanInformasi.create.form);
|
||||
if (res.status === 200) {
|
||||
permohonanKeberatanInformasi.findMany.load();
|
||||
return toast.success("Sukses menambahkan");
|
||||
}
|
||||
return toast.error("failed create");
|
||||
} catch (error) {
|
||||
console.log((error as Error).message);
|
||||
} finally {
|
||||
permohonanKeberatanInformasi.create.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
findMany: {
|
||||
data: null as
|
||||
| Prisma.FormulirPermohonanKeberatanGetPayload<{omit: {isActive: true}}>[]
|
||||
| null,
|
||||
async load() {
|
||||
const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik["find-many"].get();
|
||||
if (res.status === 200) {
|
||||
permohonanKeberatanInformasi.findMany.data = res.data?.data ?? [];
|
||||
}
|
||||
}
|
||||
},
|
||||
findUnique: {
|
||||
data: null as Prisma.FormulirPermohonanKeberatanGetPayload<{
|
||||
omit: {
|
||||
isActive: true;
|
||||
};
|
||||
}> | null,
|
||||
async load(id: string) {
|
||||
try {
|
||||
const res = await fetch(`/api/ppid/permohonankeberataninformasipublik/${id}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
permohonanKeberatanInformasi.findUnique.data = data.data ?? null;
|
||||
} else {
|
||||
console.error("Failed to fetch permohonan keberatan informasi:", res.statusText);
|
||||
permohonanKeberatanInformasi.findUnique.data = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching permohonan keberatan informasi:", error);
|
||||
permohonanKeberatanInformasi.findUnique.data = null;
|
||||
}
|
||||
},
|
||||
create: {
|
||||
form: {} as PermohonanKeberatanInformasiForm,
|
||||
loading: false,
|
||||
async create() {
|
||||
const cek = templateForm.safeParse(
|
||||
permohonanKeberatanInformasi.create.form
|
||||
);
|
||||
if (!cek.success) {
|
||||
toast.error(cek.error.issues.map((i) => i.message).join("\n"));
|
||||
return false; // ⬅️ tambahkan return false
|
||||
}
|
||||
try {
|
||||
permohonanKeberatanInformasi.create.loading = true;
|
||||
const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik[
|
||||
"create"
|
||||
].post(permohonanKeberatanInformasi.create.form);
|
||||
if (res.data?.success === false) {
|
||||
toast.error(res.data?.message);
|
||||
return false; // ⬅️ gagal
|
||||
}
|
||||
|
||||
toast.success("Sukses menambahkan");
|
||||
return true; // ⬅️ sukses
|
||||
} catch {
|
||||
toast.error("Terjadi kesalahan server");
|
||||
return false;
|
||||
} finally {
|
||||
permohonanKeberatanInformasi.create.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
findMany: {
|
||||
data: null as
|
||||
| Prisma.FormulirPermohonanKeberatanGetPayload<{
|
||||
omit: { isActive: true };
|
||||
}>[]
|
||||
| null,
|
||||
async load() {
|
||||
const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik[
|
||||
"find-many"
|
||||
].get();
|
||||
if (res.status === 200) {
|
||||
permohonanKeberatanInformasi.findMany.data = res.data?.data ?? [];
|
||||
}
|
||||
},
|
||||
},
|
||||
findUnique: {
|
||||
data: null as Prisma.FormulirPermohonanKeberatanGetPayload<{
|
||||
omit: {
|
||||
isActive: true;
|
||||
};
|
||||
}> | null,
|
||||
async load(id: string) {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/ppid/permohonankeberataninformasipublik/${id}`
|
||||
);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
permohonanKeberatanInformasi.findUnique.data = data.data ?? null;
|
||||
} else {
|
||||
console.error(
|
||||
"Failed to fetch permohonan keberatan informasi:",
|
||||
res.statusText
|
||||
);
|
||||
permohonanKeberatanInformasi.findUnique.data = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching permohonan keberatan informasi:", error);
|
||||
permohonanKeberatanInformasi.findUnique.data = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default permohonanKeberatanInformasi;
|
||||
|
||||
|
||||
303
src/app/admin/(dashboard)/desa/gallery/foto/[id]/edit/page.tsx
Normal file
303
src/app/admin/(dashboard)/desa/gallery/foto/[id]/edit/page.tsx
Normal file
@@ -0,0 +1,303 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
"use client";
|
||||
|
||||
import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
|
||||
import stateGallery from "@/app/admin/(dashboard)/_state/desa/gallery";
|
||||
import colors from "@/con/colors";
|
||||
import ApiFetch from "@/lib/api-fetch";
|
||||
import {
|
||||
ActionIcon,
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Image,
|
||||
Loader,
|
||||
Paper,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title
|
||||
} from "@mantine/core";
|
||||
import { Dropzone } from "@mantine/dropzone";
|
||||
import {
|
||||
IconArrowBack,
|
||||
IconPhoto,
|
||||
IconUpload,
|
||||
IconX,
|
||||
} from "@tabler/icons-react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
import { useProxy } from "valtio/utils";
|
||||
|
||||
function EditFoto() {
|
||||
const FotoState = useProxy(stateGallery.foto);
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
|
||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
deskripsi: "",
|
||||
imagesId: "",
|
||||
});
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const [originalData, setOriginalData] = useState({
|
||||
name: "",
|
||||
deskripsi: "",
|
||||
imagesId: "",
|
||||
imageUrl: "",
|
||||
});
|
||||
|
||||
// Load kategori + Foto
|
||||
useEffect(() => {
|
||||
FotoState.findMany.load();
|
||||
|
||||
const loadFoto = async () => {
|
||||
const id = params?.id as string;
|
||||
if (!id) return;
|
||||
|
||||
try {
|
||||
const data = await FotoState.update.load(id);
|
||||
if (data) {
|
||||
setFormData({
|
||||
name: data.name || "",
|
||||
deskripsi: data.deskripsi || "",
|
||||
imagesId: data.imagesId || "",
|
||||
});
|
||||
|
||||
setOriginalData({
|
||||
name: data.name || "",
|
||||
deskripsi: data.deskripsi || "",
|
||||
imagesId: data.imagesId || "",
|
||||
imageUrl: data.imageGalleryFoto?.link || ""
|
||||
});
|
||||
|
||||
if (data?.imageGalleryFoto?.link) {
|
||||
setPreviewImage(data.imageGalleryFoto.link);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading Foto:", error);
|
||||
toast.error("Gagal memuat data Foto");
|
||||
}
|
||||
};
|
||||
|
||||
loadFoto();
|
||||
}, [params?.id]);
|
||||
|
||||
const handleChange = (field: string, value: string) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
// Update global state hanya sekali di sini
|
||||
FotoState.update.form = {
|
||||
...FotoState.update.form,
|
||||
...formData,
|
||||
};
|
||||
|
||||
if (file) {
|
||||
const res = await ApiFetch.api.fileStorage.create.post({
|
||||
file,
|
||||
name: file.name,
|
||||
});
|
||||
const uploaded = res.data?.data;
|
||||
|
||||
if (!uploaded?.id) {
|
||||
return toast.error("Gagal upload gambar");
|
||||
}
|
||||
|
||||
FotoState.update.form.imagesId = uploaded.id;
|
||||
}
|
||||
|
||||
await FotoState.update.update();
|
||||
toast.success("Foto berhasil diperbarui!");
|
||||
router.push("/admin/desa/gallery/foto");
|
||||
} catch (error) {
|
||||
console.error("Error updating foto:", error);
|
||||
toast.error("Terjadi kesalahan saat memperbarui foto");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetForm = () => {
|
||||
setFormData({
|
||||
name: originalData.name,
|
||||
deskripsi: originalData.deskripsi,
|
||||
imagesId: originalData.imagesId,
|
||||
});
|
||||
setPreviewImage(originalData.imageUrl || null);
|
||||
setFile(null);
|
||||
toast.info("Form dikembalikan ke data awal");
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: "sm", md: "lg" }} py="md">
|
||||
{/* Header */}
|
||||
<Group mb="md">
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
p="xs"
|
||||
radius="md"
|
||||
>
|
||||
<IconArrowBack color={colors["blue-button"]} size={24} />
|
||||
</Button>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Edit Foto
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
{/* Form */}
|
||||
<Paper
|
||||
w={{ base: "100%", md: "50%" }}
|
||||
bg={colors["white-1"]}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: "1px solid #e0e0e0" }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
label="Judul Foto"
|
||||
placeholder="Masukkan judul foto"
|
||||
value={formData.name}
|
||||
onChange={(e) => handleChange("name", e.target.value)}
|
||||
required
|
||||
/>
|
||||
|
||||
{/* Upload Gambar */}
|
||||
<Box>
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
Gambar Foto
|
||||
</Text>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const selectedFile = files[0];
|
||||
if (selectedFile) {
|
||||
setFile(selectedFile);
|
||||
setPreviewImage(URL.createObjectURL(selectedFile));
|
||||
}
|
||||
}}
|
||||
onReject={() =>
|
||||
toast.error("File tidak valid, gunakan format gambar")
|
||||
}
|
||||
maxSize={5 * 1024 ** 2}
|
||||
accept={{ "image/*": [] }}
|
||||
radius="md"
|
||||
p="xl"
|
||||
>
|
||||
<Group justify="center" gap="xl" mih={180}>
|
||||
<Dropzone.Accept>
|
||||
<IconUpload
|
||||
size={48}
|
||||
color={colors["blue-button"]}
|
||||
stroke={1.5}
|
||||
/>
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<IconX size={48} color="red" stroke={1.5} />
|
||||
</Dropzone.Reject>
|
||||
<Dropzone.Idle>
|
||||
<IconPhoto size={48} color="#868e96" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
<Stack gap="xs" align="center">
|
||||
<Text size="md" fw={500}>
|
||||
Seret gambar atau klik untuk memilih file
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
|
||||
{previewImage && (
|
||||
<Box mt="sm" pos="relative" style={{ textAlign: 'center' }}>
|
||||
<Image
|
||||
src={previewImage}
|
||||
alt="Preview Gambar"
|
||||
radius="md"
|
||||
style={{
|
||||
maxHeight: 220,
|
||||
objectFit: "contain",
|
||||
border: `1px solid ${colors["blue-button"]}`,
|
||||
}}
|
||||
loading="lazy"
|
||||
/>
|
||||
<ActionIcon
|
||||
variant="filled"
|
||||
color="red"
|
||||
radius="xl"
|
||||
size="sm"
|
||||
pos="absolute"
|
||||
top={5}
|
||||
right={5}
|
||||
onClick={() => {
|
||||
setPreviewImage(null);
|
||||
setFile(null);
|
||||
}}
|
||||
style={{
|
||||
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
|
||||
}}
|
||||
>
|
||||
<IconX size={14} />
|
||||
</ActionIcon>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Deskripsi */}
|
||||
<Box>
|
||||
<Text fz="sm" fw="bold">
|
||||
Deskripsi Foto
|
||||
</Text>
|
||||
<EditEditor
|
||||
value={formData.deskripsi}
|
||||
onChange={(htmlContent) =>
|
||||
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }))
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Action */}
|
||||
<Group justify="right">
|
||||
{/* Tombol Batal */}
|
||||
<Button
|
||||
variant="outline"
|
||||
color="gray"
|
||||
radius="md"
|
||||
size="md"
|
||||
onClick={handleResetForm}
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
|
||||
{/* Tombol Simpan */}
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
size="md"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||
}}
|
||||
>
|
||||
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditFoto;
|
||||
175
src/app/admin/(dashboard)/desa/gallery/foto/[id]/page.tsx
Normal file
175
src/app/admin/(dashboard)/desa/gallery/foto/[id]/page.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
'use client';
|
||||
|
||||
import { Box, Button, Group, Paper, Skeleton, Stack, Text, Alert } from '@mantine/core';
|
||||
import Image from 'next/image';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconArrowBack, IconEdit, IconTrash, IconPhoto } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||
import colors from '@/con/colors';
|
||||
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
|
||||
|
||||
function DetailFoto() {
|
||||
const FotoState = useProxy(stateGallery.foto);
|
||||
const [modalHapus, setModalHapus] = useState(false);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const [imageError, setImageError] = useState(false);
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
|
||||
useShallowEffect(() => {
|
||||
FotoState.findUnique.load(params?.id as string);
|
||||
}, []);
|
||||
|
||||
const handleHapus = () => {
|
||||
if (selectedId) {
|
||||
FotoState.delete.byId(selectedId);
|
||||
setModalHapus(false);
|
||||
setSelectedId(null);
|
||||
router.push("/admin/desa/gallery/foto");
|
||||
}
|
||||
};
|
||||
|
||||
if (!FotoState.findUnique.data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton height={500} radius="md" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
const data = FotoState.findUnique.data;
|
||||
const imageUrl = data.imageGalleryFoto?.link;
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
|
||||
mb={15}
|
||||
>
|
||||
Kembali
|
||||
</Button>
|
||||
|
||||
<Paper
|
||||
withBorder
|
||||
// Gunakan max-width agar tidak terlalu lebar di desktop
|
||||
maw={800}
|
||||
w="100%"
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Text fz={{ base: 'xl', md: '2xl' }} fw="bold" c={colors['blue-button']}>
|
||||
Detail Foto
|
||||
</Text>
|
||||
|
||||
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
|
||||
<Stack gap="sm">
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Judul Foto</Text>
|
||||
<Text fz="md" c="dimmed">{data.name || '-'}</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Deskripsi</Text>
|
||||
<Text
|
||||
fz="md"
|
||||
c="dimmed"
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Gambar</Text>
|
||||
{imageUrl ? (
|
||||
<Box
|
||||
pos="relative"
|
||||
style={{
|
||||
width: '100%',
|
||||
maxWidth: '600px', // Set a maximum width
|
||||
margin: '0 auto', // Center the container
|
||||
aspectRatio: '16/9', // Use 16:9 aspect ratio
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden',
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={imageUrl}
|
||||
alt={data.name || 'Gambar Foto'}
|
||||
fill
|
||||
style={{
|
||||
objectFit: 'contain', // Changed from 'cover' to 'contain' to show full image
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0
|
||||
}}
|
||||
loading="lazy"
|
||||
onError={() => setImageError(true)}
|
||||
/>
|
||||
</Box>
|
||||
) : imageError ? (
|
||||
<Alert
|
||||
color="orange"
|
||||
icon={<IconPhoto size={16} />}
|
||||
title="Gagal memuat gambar"
|
||||
radius="md"
|
||||
>
|
||||
Gambar tidak dapat ditampilkan.
|
||||
</Alert>
|
||||
) : (
|
||||
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<Group gap="sm" justify="flex-start">
|
||||
<Button
|
||||
color="red"
|
||||
onClick={() => {
|
||||
setSelectedId(data.id);
|
||||
setModalHapus(true);
|
||||
}}
|
||||
variant="light"
|
||||
radius="md"
|
||||
size="md"
|
||||
>
|
||||
<IconTrash size={20} />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
color="green"
|
||||
onClick={() => router.push(`/admin/desa/gallery/foto/${data.id}/edit`)}
|
||||
variant="light"
|
||||
radius="md"
|
||||
size="md"
|
||||
>
|
||||
<IconEdit size={20} />
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
<ModalKonfirmasiHapus
|
||||
opened={modalHapus}
|
||||
onClose={() => setModalHapus(false)}
|
||||
onConfirm={handleHapus}
|
||||
text="Apakah Anda yakin ingin menghapus foto ini?"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default DetailFoto;
|
||||
228
src/app/admin/(dashboard)/desa/gallery/foto/create/page.tsx
Normal file
228
src/app/admin/(dashboard)/desa/gallery/foto/create/page.tsx
Normal file
@@ -0,0 +1,228 @@
|
||||
'use client';
|
||||
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
||||
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
|
||||
import colors from '@/con/colors';
|
||||
import ApiFetch from '@/lib/api-fetch';
|
||||
import {
|
||||
ActionIcon,
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Paper,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
Loader,
|
||||
Image
|
||||
} from '@mantine/core';
|
||||
import { Dropzone } from '@mantine/dropzone';
|
||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
function CreateFoto() {
|
||||
const FotoState = useProxy(stateGallery.foto);
|
||||
const router = useRouter();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
|
||||
const resetForm = () => {
|
||||
FotoState.create.form = {
|
||||
name: '',
|
||||
deskripsi: '',
|
||||
imagesId: '',
|
||||
};
|
||||
setPreviewImage(null);
|
||||
setFile(null);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
if (!file) {
|
||||
return toast.warn('Silakan pilih file gambar terlebih dahulu');
|
||||
}
|
||||
|
||||
const res = await ApiFetch.api.fileStorage.create.post({
|
||||
file,
|
||||
name: file.name,
|
||||
});
|
||||
|
||||
const uploaded = res.data?.data;
|
||||
if (!uploaded?.id) {
|
||||
return toast.error('Gagal mengunggah gambar, silakan coba lagi');
|
||||
}
|
||||
|
||||
FotoState.create.form.imagesId = uploaded.id;
|
||||
|
||||
await FotoState.create.create();
|
||||
|
||||
resetForm();
|
||||
router.push('/admin/desa/gallery/foto');
|
||||
} catch (error) {
|
||||
console.error('Error creating foto:', error);
|
||||
toast.error('Terjadi kesalahan saat membuat foto');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
{/* Header Back Button + Title */}
|
||||
<Group mb="md">
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
p="xs"
|
||||
radius="md"
|
||||
>
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Tambah Foto
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
{/* Card Form */}
|
||||
<Paper
|
||||
w={{ base: '100%', md: '50%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
{/* Judul */}
|
||||
<TextInput
|
||||
label="Judul Foto"
|
||||
placeholder="Masukkan judul Foto"
|
||||
value={FotoState.create.form.name}
|
||||
onChange={(e) => {
|
||||
FotoState.create.form.name = e.currentTarget.value;
|
||||
}}
|
||||
required
|
||||
/>
|
||||
|
||||
<Box>
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
Gambar Berita
|
||||
</Text>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const selectedFile = files[0];
|
||||
if (selectedFile) {
|
||||
setFile(selectedFile);
|
||||
setPreviewImage(URL.createObjectURL(selectedFile));
|
||||
}
|
||||
}}
|
||||
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
||||
maxSize={5 * 1024 ** 2}
|
||||
accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
|
||||
radius="md"
|
||||
p="xl"
|
||||
>
|
||||
<Group justify="center" gap="xl" mih={180}>
|
||||
<Dropzone.Accept>
|
||||
<IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||
</Dropzone.Reject>
|
||||
<Dropzone.Idle>
|
||||
<IconPhoto size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
</Group>
|
||||
<Text ta="center" mt="sm" size="sm" color="dimmed">
|
||||
Seret gambar atau klik untuk memilih file (maks 5MB)
|
||||
</Text>
|
||||
</Dropzone>
|
||||
|
||||
{previewImage && (
|
||||
<Box mt="sm" pos="relative" style={{ textAlign: 'center' }}>
|
||||
<Image
|
||||
src={previewImage}
|
||||
alt="Preview Gambar"
|
||||
radius="md"
|
||||
style={{
|
||||
maxHeight: 200,
|
||||
objectFit: 'contain',
|
||||
border: '1px solid #ddd',
|
||||
}}
|
||||
loading="lazy"
|
||||
/>
|
||||
|
||||
{/* Tombol hapus (pojok kanan atas) */}
|
||||
<ActionIcon
|
||||
variant="filled"
|
||||
color="red"
|
||||
radius="xl"
|
||||
size="sm"
|
||||
pos="absolute"
|
||||
top={5}
|
||||
right={5}
|
||||
onClick={() => {
|
||||
setPreviewImage(null);
|
||||
setFile(null);
|
||||
}}
|
||||
style={{
|
||||
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
|
||||
}}
|
||||
>
|
||||
<IconX size={14} />
|
||||
</ActionIcon>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Deskripsi */}
|
||||
<Box>
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
Deskripsi Foto
|
||||
</Text>
|
||||
<CreateEditor
|
||||
value={FotoState.create.form.deskripsi}
|
||||
onChange={(val) => {
|
||||
FotoState.create.form.deskripsi = val;
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Button Submit */}
|
||||
<Group justify="right">
|
||||
<Button
|
||||
variant="outline"
|
||||
color="gray"
|
||||
radius="md"
|
||||
size="md"
|
||||
onClick={resetForm}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
|
||||
{/* Tombol Simpan */}
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
size="md"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||
}}
|
||||
>
|
||||
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateFoto;
|
||||
@@ -1,157 +1,163 @@
|
||||
"use client";
|
||||
import stateFileStorage from "@/state/state-list-image";
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
ActionIcon,
|
||||
Box,
|
||||
Card,
|
||||
Flex,
|
||||
Button,
|
||||
Center,
|
||||
Group,
|
||||
Image,
|
||||
Pagination,
|
||||
Paper,
|
||||
SimpleGrid,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Table,
|
||||
TableTbody,
|
||||
TableTd,
|
||||
TableTh,
|
||||
TableThead,
|
||||
TableTr,
|
||||
Text,
|
||||
TextInput,
|
||||
Title
|
||||
} from "@mantine/core";
|
||||
import { useShallowEffect } from "@mantine/hooks";
|
||||
import { IconSearch, IconTrash, IconX } from "@tabler/icons-react";
|
||||
import { motion } from "framer-motion";
|
||||
import toast from "react-simple-toasts";
|
||||
import { useSnapshot } from "valtio";
|
||||
|
||||
export default function ListImage() {
|
||||
const { list, total } = useSnapshot(stateFileStorage);
|
||||
|
||||
useShallowEffect(() => {
|
||||
stateFileStorage.load();
|
||||
}, []);
|
||||
|
||||
let timeOut: NodeJS.Timer;
|
||||
} from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '../../../_com/header';
|
||||
import stateGallery from '../../../_state/desa/gallery';
|
||||
|
||||
function Foto() {
|
||||
const [search, setSearch] = useState("");
|
||||
return (
|
||||
<Stack p="lg" gap="lg">
|
||||
<Flex justify="space-between" align="center" wrap="wrap" gap="md">
|
||||
<Title order={2} fw={700}>
|
||||
Galeri Foto
|
||||
</Title>
|
||||
<TextInput
|
||||
radius="xl"
|
||||
size="md"
|
||||
placeholder="Cari foto berdasarkan nama..."
|
||||
leftSection={<IconSearch size={18} />}
|
||||
rightSection={
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
color="gray"
|
||||
radius="xl"
|
||||
onClick={() => stateFileStorage.load()}
|
||||
>
|
||||
<IconX size={18} />
|
||||
</ActionIcon>
|
||||
}
|
||||
onChange={(e) => {
|
||||
if (timeOut) clearTimeout(timeOut);
|
||||
timeOut = setTimeout(() => {
|
||||
stateFileStorage.load({ search: e.target.value });
|
||||
}, 300);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Paper withBorder radius="lg" p="md" shadow="sm">
|
||||
{list && list.length > 0 ? (
|
||||
<SimpleGrid
|
||||
cols={{ base: 2, sm: 3, md: 5, lg: 8 }}
|
||||
spacing="md"
|
||||
verticalSpacing="md"
|
||||
>
|
||||
{list.map((v, k) => (
|
||||
<Card
|
||||
key={k}
|
||||
withBorder
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
className="hover:shadow-md transition-all duration-200"
|
||||
>
|
||||
<Stack gap="xs">
|
||||
<motion.div
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(v.url);
|
||||
toast("Tautan foto berhasil disalin");
|
||||
}}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<Image
|
||||
src={`${v.url}?size=200`}
|
||||
alt={v.name}
|
||||
radius="md"
|
||||
h={120}
|
||||
fit="cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
<Box>
|
||||
<Text size="sm" fw={500} lineClamp={2}>
|
||||
{v.name}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Group justify="space-between" align="center" pt="xs">
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="red"
|
||||
radius="md"
|
||||
onClick={() => {
|
||||
stateFileStorage
|
||||
.del({ id: v.id })
|
||||
.finally(() => toast("Foto berhasil dihapus"));
|
||||
}}
|
||||
>
|
||||
<IconTrash size={18} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Card>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
) : (
|
||||
<Stack align="center" justify="center" py="xl" gap="sm">
|
||||
<Image
|
||||
src="https://cdn-icons-png.flaticon.com/512/4076/4076549.png"
|
||||
alt="Kosong"
|
||||
w={120}
|
||||
h={120}
|
||||
fit="contain"
|
||||
opacity={0.7}
|
||||
loading="lazy"
|
||||
/>
|
||||
<Text c="dimmed" ta="center">
|
||||
Belum ada foto yang tersedia
|
||||
</Text>
|
||||
</Stack>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
{total && total > 1 && (
|
||||
<Flex justify="center">
|
||||
<Pagination
|
||||
total={total}
|
||||
value={stateFileStorage.page} // Changed from page to value
|
||||
size="md"
|
||||
radius="md"
|
||||
withEdges
|
||||
onChange={(page) => {
|
||||
stateFileStorage.load({ page });
|
||||
}}
|
||||
/>
|
||||
|
||||
</Flex>
|
||||
)}
|
||||
</Stack>
|
||||
<Box>
|
||||
<HeaderSearch
|
||||
title='Foto'
|
||||
placeholder='Cari judul atau deskripsi foto...'
|
||||
searchIcon={<IconSearch size={20} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
<ListFoto search={search} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function ListFoto({ search }: { search: string }) {
|
||||
const FotoState = useProxy(stateGallery.foto)
|
||||
const router = useRouter();
|
||||
|
||||
const {
|
||||
data,
|
||||
page,
|
||||
totalPages,
|
||||
loading,
|
||||
load,
|
||||
} = FotoState.findMany;
|
||||
|
||||
useShallowEffect(() => {
|
||||
load(page, 10, search)
|
||||
}, [page, search])
|
||||
|
||||
const filteredData = data || []
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton height={600} radius="md" />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={4}>Daftar Foto</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/desa/gallery/foto/create')}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Group>
|
||||
<Box style={{ overflowX: "auto" }}>
|
||||
<Table highlightOnHover>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh style={{ width: '25%' }}>Judul Foto</TableTh>
|
||||
<TableTh style={{ width: '20%' }}>Tanggal</TableTh>
|
||||
<TableTh style={{ width: '30%' }}>Deskripsi</TableTh>
|
||||
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd style={{ width: '25%' }}>
|
||||
<Box w={200}>
|
||||
<Text fw={500} truncate="end" lineClamp={1}>{item.name}</Text>
|
||||
</Box>
|
||||
</TableTd>
|
||||
<TableTd style={{ width: '20%' }}>
|
||||
<Box w={200}>
|
||||
<Text fz="sm" c="dimmed">
|
||||
{new Date(item.createdAt).toLocaleDateString('id-ID', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</Text>
|
||||
</Box>
|
||||
</TableTd>
|
||||
<TableTd style={{ width: '30%' }}>
|
||||
<Box w={200}>
|
||||
<Text fz="sm" truncate="end" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||
</Box>
|
||||
</TableTd>
|
||||
<TableTd style={{ width: '15%' }}>
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
onClick={() => router.push(`/admin/desa/gallery/foto/${item.id}`)}
|
||||
>
|
||||
<IconDeviceImac size={20} />
|
||||
<Text ml={5}>Detail</Text>
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={4}>
|
||||
<Center py={20}>
|
||||
<Text c="dimmed">Tidak ada foto yang cocok</Text>
|
||||
</Center>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
)}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
</Paper>
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10)
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}}
|
||||
total={totalPages}
|
||||
mt="md"
|
||||
mb="md"
|
||||
color="blue"
|
||||
radius="md"
|
||||
/>
|
||||
</Center>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default Foto;
|
||||
|
||||
@@ -11,21 +11,21 @@ function LayoutTabsDetail({ children }: { children: React.ReactNode }) {
|
||||
const pathname = usePathname()
|
||||
const tabs = [
|
||||
{
|
||||
label: "Profile Desa",
|
||||
value: "profiledesa",
|
||||
href: "/admin/desa/profile/profile-desa",
|
||||
label: "Profil Desa",
|
||||
value: "profildesa",
|
||||
href: "/admin/desa/profil/profil-desa",
|
||||
icon: <IconUser size={18} stroke={1.8} />
|
||||
},
|
||||
{
|
||||
label: "Profile Perbekel",
|
||||
value: "profileperbekel",
|
||||
href: "/admin/desa/profile/profile-perbekel",
|
||||
label: "Profil Perbekel",
|
||||
value: "profilperbekel",
|
||||
href: "/admin/desa/profil/profil-perbekel",
|
||||
icon: <IconUsers size={18} stroke={1.8} />
|
||||
},
|
||||
{
|
||||
label: "Profile Perbekel Dari Masa Ke Masa",
|
||||
value: "profile-perbekel-dari-masa-ke-masa",
|
||||
href: "/admin/desa/profile/profile-perbekel-dari-masa-ke-masa",
|
||||
label: "Profil Perbekel Dari Masa Ke Masa",
|
||||
value: "profilperbekeldarimasakemasa",
|
||||
href: "/admin/desa/profil/profil-perbekel-dari-masa-ke-masa",
|
||||
icon: <IconCalendar size={18} stroke={1.8} />
|
||||
}
|
||||
];
|
||||
@@ -12,22 +12,22 @@ function LayoutTabsEdit({ children }: { children: React.ReactNode }) {
|
||||
{
|
||||
label: "Sejarah Desa",
|
||||
value: "sejarahdesa",
|
||||
href: "/admin/desa/profile/edit/sejarah_desa"
|
||||
href: "/admin/desa/profil/edit/sejarah_desa"
|
||||
},
|
||||
{
|
||||
label: "Visi Misi Desa",
|
||||
value: "visimisidesa",
|
||||
href: "/admin/desa/profile/edit/visi_misi_desa"
|
||||
href: "/admin/desa/profil/edit/visi_misi_desa"
|
||||
},
|
||||
{
|
||||
label: "Lambang Desa",
|
||||
value: "lambangdesa",
|
||||
href: "/admin/desa/profile/edit/lambang_desa"
|
||||
href: "/admin/desa/profil/edit/lambang_desa"
|
||||
},
|
||||
{
|
||||
label: "Maskot Desa",
|
||||
value: "maskotdesa",
|
||||
href: "/admin/desa/profile/edit/maskot_desa"
|
||||
href: "/admin/desa/profil/edit/maskot_desa"
|
||||
},
|
||||
];
|
||||
const curentTab = tabs.find(tab => tab.href === pathname)
|
||||
@@ -43,7 +43,7 @@ function Page() {
|
||||
const id = params?.id as string;
|
||||
if (!id) {
|
||||
toast.error('ID tidak valid');
|
||||
router.push('/admin/desa/profile/profile-desa');
|
||||
router.push('/admin/desa/profil/profil-desa');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ function Page() {
|
||||
|
||||
if (success) {
|
||||
toast.success('Data berhasil disimpan');
|
||||
router.push('/admin/desa/profile/profile-desa');
|
||||
router.push('/admin/desa/profil/profil-desa');
|
||||
} else {
|
||||
toast.error('Gagal menyimpan data');
|
||||
}
|
||||
@@ -156,7 +156,7 @@ function Page() {
|
||||
<Alert icon={<IconAlertCircle size={20} />} color="red" title="Terjadi Kesalahan" radius="md">
|
||||
{loadError}
|
||||
</Alert>
|
||||
<Button onClick={() => router.push('/admin/desa/profile/profile-desa')} variant="outline">
|
||||
<Button onClick={() => router.push('/admin/desa/profil/profil-desa')} variant="outline">
|
||||
Kembali ke Halaman Utama
|
||||
</Button>
|
||||
</Stack>
|
||||
@@ -40,7 +40,7 @@ function Page() {
|
||||
const id = params?.id as string;
|
||||
if (!id) {
|
||||
toast.error("ID tidak valid");
|
||||
router.push("/admin/desa/profile/profile-desa");
|
||||
router.push("/admin/desa/profil/profil-desa");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ function Page() {
|
||||
|
||||
if (success) {
|
||||
toast.success("Maskot berhasil diperbarui!");
|
||||
router.push("/admin/desa/profile/profile-desa");
|
||||
router.push("/admin/desa/profil/profil-desa");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error update maskot:", error);
|
||||
@@ -50,7 +50,7 @@ function Page() {
|
||||
const id = params?.id as string;
|
||||
if (!id) {
|
||||
toast.error('ID tidak valid');
|
||||
router.push('/admin/desa/profile/profile-desa');
|
||||
router.push('/admin/desa/profil/profil-desa');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ function Page() {
|
||||
|
||||
if (success) {
|
||||
toast.success('Data berhasil disimpan');
|
||||
router.push('/admin/desa/profile/profile-desa');
|
||||
router.push('/admin/desa/profil/profil-desa');
|
||||
} else {
|
||||
toast.error('Gagal menyimpan data');
|
||||
}
|
||||
@@ -179,7 +179,7 @@ function Page() {
|
||||
{loadError}
|
||||
</Alert>
|
||||
<Button
|
||||
onClick={() => router.push('/admin/desa/profile/profile-desa')}
|
||||
onClick={() => router.push('/admin/desa/profil/profil-desa')}
|
||||
variant="outline"
|
||||
>
|
||||
Kembali ke Halaman Utama
|
||||
@@ -42,7 +42,7 @@ function Page() {
|
||||
const id = params?.id as string;
|
||||
if (!id) {
|
||||
toast.error('ID tidak valid');
|
||||
router.push('/admin/desa/profile/profile-desa');
|
||||
router.push('/admin/desa/profil/profil-desa');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ function Page() {
|
||||
|
||||
if (success) {
|
||||
toast.success('Data berhasil disimpan');
|
||||
router.push('/admin/desa/profile/profile-desa');
|
||||
router.push('/admin/desa/profil/profil-desa');
|
||||
} else {
|
||||
toast.error('Gagal menyimpan data');
|
||||
}
|
||||
@@ -156,7 +156,7 @@ function Page() {
|
||||
{loadError}
|
||||
</Alert>
|
||||
<Button
|
||||
onClick={() => router.push('/admin/desa/profile/profile-desa')}
|
||||
onClick={() => router.push('/admin/desa/profil/profil-desa')}
|
||||
variant="outline"
|
||||
>
|
||||
Kembali ke Halaman Utama
|
||||
@@ -27,7 +27,7 @@ function Page() {
|
||||
return (
|
||||
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
|
||||
<Stack gap="lg">
|
||||
<Title order={2} c={colors['blue-button']}>Preview Profile Desa</Title>
|
||||
<Title order={2} c={colors['blue-button']}>Preview Profil Desa</Title>
|
||||
|
||||
{/* Sejarah Desa */}
|
||||
{sejarah && (
|
||||
@@ -42,7 +42,7 @@ function Page() {
|
||||
variant="light"
|
||||
leftSection={<IconEdit size={18} stroke={2} />}
|
||||
radius="md"
|
||||
onClick={() => router.push(`/admin/desa/profile/profile-desa/${sejarah.id}/sejarah_desa`)}
|
||||
onClick={() => router.push(`/admin/desa/profil/profil-desa/${sejarah.id}/sejarah_desa`)}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
@@ -87,7 +87,7 @@ function Page() {
|
||||
variant="light"
|
||||
leftSection={<IconEdit size={18} stroke={2} />}
|
||||
radius="md"
|
||||
onClick={() => router.push(`/admin/desa/profile/profile-desa/${visiMisi.id}/visi_misi_desa`)}
|
||||
onClick={() => router.push(`/admin/desa/profil/profil-desa/${visiMisi.id}/visi_misi_desa`)}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
@@ -135,7 +135,7 @@ function Page() {
|
||||
variant="light"
|
||||
leftSection={<IconEdit size={18} stroke={2} />}
|
||||
radius="md"
|
||||
onClick={() => router.push(`/admin/desa/profile/profile-desa/${lambang.id}/lambang_desa`)}
|
||||
onClick={() => router.push(`/admin/desa/profil/profil-desa/${lambang.id}/lambang_desa`)}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
@@ -180,7 +180,7 @@ function Page() {
|
||||
variant="light"
|
||||
leftSection={<IconEdit size={18} stroke={2} />}
|
||||
radius="md"
|
||||
onClick={() => router.push(`/admin/desa/profile/profile-desa/${maskot.id}/maskot_desa`)}
|
||||
onClick={() => router.push(`/admin/desa/profil/profil-desa/${maskot.id}/maskot_desa`)}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
@@ -117,7 +117,7 @@ function EditPerbekelDariMasaKeMasa() {
|
||||
|
||||
await state.update.update();
|
||||
toast.success('Perbekel dari masa ke masa berhasil diperbarui!');
|
||||
router.push('/admin/desa/profile/profile-perbekel-dari-masa-ke-masa');
|
||||
router.push('/admin/desa/profil/profil-perbekel-dari-masa-ke-masa');
|
||||
} catch (error) {
|
||||
console.error('Error updating perbekel dari masa ke masa:', error);
|
||||
toast.error('Terjadi kesalahan saat memperbarui perbekel dari masa ke masa');
|
||||
@@ -25,7 +25,7 @@ function DetailPerbekelDariMasa() {
|
||||
state.delete.byId(selectedId);
|
||||
setModalHapus(false);
|
||||
setSelectedId(null);
|
||||
router.push("/admin/desa/profile/profile-perbekel-dari-masa-ke-masa");
|
||||
router.push("/admin/desa/profil/profil-perbekel-dari-masa-ke-masa");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -113,7 +113,7 @@ function DetailPerbekelDariMasa() {
|
||||
|
||||
<Button
|
||||
color="green"
|
||||
onClick={() => router.push(`/admin/desa/profile/profile-perbekel-dari-masa-ke-masa/${data.id}/edit`)}
|
||||
onClick={() => router.push(`/admin/desa/profil/profil-perbekel-dari-masa-ke-masa/${data.id}/edit`)}
|
||||
variant="light"
|
||||
radius="md"
|
||||
size="md"
|
||||
@@ -46,7 +46,7 @@ function CreatePerbekelDariMasaKeMasa() {
|
||||
state.create.form.imageId = uploaded.id;
|
||||
await state.create.create();
|
||||
resetForm();
|
||||
router.push('/admin/desa/profile/profile-perbekel-dari-masa-ke-masa');
|
||||
router.push('/admin/desa/profil/profil-perbekel-dari-masa-ke-masa');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error('Gagal menambahkan perbekel dari masa ke masa');
|
||||
@@ -53,7 +53,7 @@ function ListPerbekelDariMasaKeMasa({ search }: { search: string }) {
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/desa/profile/profile-perbekel-dari-masa-ke-masa/create')}
|
||||
onClick={() => router.push('/admin/desa/profil/profil-perbekel-dari-masa-ke-masa/create')}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
@@ -90,7 +90,7 @@ function ListPerbekelDariMasaKeMasa({ search }: { search: string }) {
|
||||
variant="light"
|
||||
color="blue"
|
||||
leftSection={<IconDeviceImacCog size={16} />}
|
||||
onClick={() => router.push(`/admin/desa/profile/profile-perbekel-dari-masa-ke-masa/${item.id}`)}
|
||||
onClick={() => router.push(`/admin/desa/profil/profil-perbekel-dari-masa-ke-masa/${item.id}`)}
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
@@ -25,7 +25,7 @@ function ProfilePerbekel() {
|
||||
const id = params?.id as string;
|
||||
if (!id) {
|
||||
toast.error("ID tidak valid");
|
||||
router.push("/admin/desa/profile/profile-perbekel");
|
||||
router.push("/admin/desa/profil/profil-perbekel");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ function ProfilePerbekel() {
|
||||
const success = await perbekelState.edit.submit()
|
||||
if (success) {
|
||||
toast.success("Data berhasil disimpan");
|
||||
router.push("/admin/desa/profile/profile-perbekel");
|
||||
router.push("/admin/desa/profil/profil-perbekel");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error update sejarah desa:", error);
|
||||
@@ -41,7 +41,7 @@ function Page() {
|
||||
variant="light"
|
||||
leftSection={<IconEdit size={18} stroke={2} />}
|
||||
radius="md"
|
||||
onClick={() => router.push(`/admin/desa/profile/profile-perbekel/${perbekel.id}`)}
|
||||
onClick={() => router.push(`/admin/desa/profil/profil-perbekel/${perbekel.id}`)}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
@@ -20,9 +20,9 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
||||
icon: <IconActivity size={18} stroke={1.8} />
|
||||
},
|
||||
{
|
||||
label: "Grafik Hasil Kepuasan Masyarakat",
|
||||
value: "grafikhasilkepuasan",
|
||||
href: "/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan",
|
||||
label: "Penderita Penyakit",
|
||||
value: "penderitapenyakit",
|
||||
href: "/admin/kesehatan/data-kesehatan-warga/penderita_penyakit",
|
||||
icon: <IconGauge size={18} stroke={1.8} />
|
||||
},
|
||||
{
|
||||
|
||||
@@ -70,8 +70,8 @@ function EditGrafikHasilKepuasan() {
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error loading grafik hasil kepuasan:", err);
|
||||
toast.error("Gagal memuat data grafik hasil kepuasan");
|
||||
console.error("Error loading penderita penyakit:", err);
|
||||
toast.error("Gagal memuat data penderita penyakit");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -99,11 +99,11 @@ function EditGrafikHasilKepuasan() {
|
||||
setIsSubmitting(true);
|
||||
editState.update.form = { ...editState.update.form, ...formData };
|
||||
await editState.update.submit();
|
||||
toast.success('Grafik hasil kepuasan berhasil diperbarui!');
|
||||
router.push('/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan');
|
||||
toast.success('penderita penyakit berhasil diperbarui!');
|
||||
router.push('/admin/kesehatan/data-kesehatan-warga/penderita_penyakit');
|
||||
} catch (err) {
|
||||
console.error('Error updating grafik hasil kepuasan:', err);
|
||||
toast.error('Terjadi kesalahan saat memperbarui grafik hasil kepuasan');
|
||||
console.error('Error updating penderita penyakit:', err);
|
||||
toast.error('Terjadi kesalahan saat memperbarui penderita penyakit');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
@@ -122,7 +122,7 @@ function EditGrafikHasilKepuasan() {
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Edit Grafik Hasil Kepuasan
|
||||
Edit Penderita Penyakit
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
@@ -26,7 +26,7 @@ function DetailGrafikHasilKepuasan() {
|
||||
state.delete.byId(selectedId);
|
||||
setModalHapus(false);
|
||||
setSelectedId(null);
|
||||
router.push("/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan");
|
||||
router.push("/admin/kesehatan/data-kesehatan-warga/penderita_penyakit");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -63,7 +63,7 @@ function DetailGrafikHasilKepuasan() {
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
|
||||
Detail Data Grafik Hasil Kepuasan
|
||||
Detail Data Penderita Penyakit
|
||||
</Text>
|
||||
|
||||
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
|
||||
@@ -118,7 +118,7 @@ function DetailGrafikHasilKepuasan() {
|
||||
color="green"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/${data.id}/edit`
|
||||
`/admin/kesehatan/data-kesehatan-warga/penderita_penyakit/${data.id}/edit`
|
||||
)
|
||||
}
|
||||
variant="light"
|
||||
@@ -40,7 +40,7 @@ function CreateGrafikHasilKepuasanMasyarakat() {
|
||||
setIsSubmitting(true);
|
||||
await stateGrafikKepuasan.create.create();
|
||||
resetForm();
|
||||
router.push("/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan");
|
||||
router.push("/admin/kesehatan/data-kesehatan-warga/penderita_penyakit");
|
||||
} catch (error) {
|
||||
console.error("Error creating grafik kepuasan:", error);
|
||||
toast.error("Terjadi kesalahan saat membuat grafik kepuasan");
|
||||
@@ -62,7 +62,7 @@ function CreateGrafikHasilKepuasanMasyarakat() {
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Tambah Grafik Hasil Kepuasan Masyarakat
|
||||
Tambah Penderita Penyakit
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
@@ -36,7 +36,7 @@ function GrafikHasilKepuasanMasyarakat() {
|
||||
<Box>
|
||||
{/* Header Search */}
|
||||
<HeaderSearch
|
||||
title='Grafik Hasil Kepuasan Masyarakat'
|
||||
title='Penderita Penyakit'
|
||||
placeholder='Cari nama atau alamat...'
|
||||
searchIcon={<IconSearch size={20} />}
|
||||
value={search}
|
||||
@@ -115,14 +115,14 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
|
||||
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
|
||||
{/* Judul + Tombol Tambah */}
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={4}>Daftar Grafik Hasil Kepuasan Masyarakat</Title>
|
||||
<Title order={4}>Daftar Penderita Penyakit</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
'/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/create'
|
||||
'/admin/kesehatan/data-kesehatan-warga/penderita_penyakit/create'
|
||||
)
|
||||
}
|
||||
>
|
||||
@@ -176,7 +176,7 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
|
||||
color="blue"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/${item.id}`
|
||||
`/admin/kesehatan/data-kesehatan-warga/penderita_penyakit/${item.id}`
|
||||
)
|
||||
}
|
||||
>
|
||||
@@ -221,7 +221,7 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
|
||||
{/* Chart */}
|
||||
<Box mt="lg" style={{ width: '100%', minWidth: 300, height: 420, minHeight: 300 }}>
|
||||
<Paper withBorder bg={colors['white-1']} p={'md'}>
|
||||
<Title pb={10} order={4}>Grafik Hasil Kepuasan Masyarakat</Title>
|
||||
<Title pb={10} order={4}>Penderita Penyakit</Title>
|
||||
{mounted && diseaseChartData.length > 0 ? (
|
||||
<Center>
|
||||
<BarChart
|
||||
@@ -30,12 +30,13 @@ function Page() {
|
||||
return (
|
||||
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
|
||||
<Stack gap="md">
|
||||
<Grid align="center">
|
||||
<Grid>
|
||||
<GridCol span={{ base: 12, md: 11 }}>
|
||||
<Title order={3} c={colors['blue-button']}>Preview Profil PPID</Title>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 1 }}>
|
||||
<Button
|
||||
w={{base: '100%', md: "110%"}}
|
||||
c="green"
|
||||
variant="light"
|
||||
leftSection={<IconEdit size={18} stroke={2} />}
|
||||
|
||||
@@ -91,8 +91,8 @@ export const devBar = [
|
||||
children: [
|
||||
{
|
||||
id: "Desa_1",
|
||||
name: "Profile",
|
||||
path: "/admin/desa/profile/profile-desa"
|
||||
name: "Profil",
|
||||
path: "/admin/desa/profil/profil-desa"
|
||||
},
|
||||
{
|
||||
id: "Desa_2",
|
||||
@@ -495,8 +495,8 @@ export const navBar = [
|
||||
children: [
|
||||
{
|
||||
id: "Desa_1",
|
||||
name: "Profile",
|
||||
path: "/admin/desa/profile/profile-desa"
|
||||
name: "Profil",
|
||||
path: "/admin/desa/profil/profil-desa"
|
||||
},
|
||||
{
|
||||
id: "Desa_2",
|
||||
@@ -899,8 +899,8 @@ export const role1 = [
|
||||
children: [
|
||||
{
|
||||
id: "Desa_1",
|
||||
name: "Profile",
|
||||
path: "/admin/desa/profile/profile-desa"
|
||||
name: "Profil",
|
||||
path: "/admin/desa/profil/profil-desa"
|
||||
},
|
||||
{
|
||||
id: "Desa_2",
|
||||
|
||||
@@ -6,33 +6,24 @@ import path from "path";
|
||||
const beritaDelete = async (context: Context) => {
|
||||
const id = context.params?.id as string;
|
||||
|
||||
if (!id) {
|
||||
return {
|
||||
status: 400,
|
||||
body: "ID tidak diberikan",
|
||||
};
|
||||
}
|
||||
if (!id) return { status: 400, body: "ID tidak diberikan" };
|
||||
|
||||
const berita = await prisma.berita.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
image: true,
|
||||
kategoriBerita: true, // pastikan relasi image sudah ada di prisma schema
|
||||
},
|
||||
include: { image: true, kategoriBerita: true },
|
||||
});
|
||||
|
||||
if (!berita) {
|
||||
return {
|
||||
status: 404,
|
||||
body: "Berita tidak ditemukan",
|
||||
};
|
||||
}
|
||||
if (!berita) return { status: 404, body: "Berita tidak ditemukan" };
|
||||
|
||||
// Hapus file gambar dari filesystem jika ada
|
||||
// 1. HAPUS BERITA DULU
|
||||
await prisma.berita.delete({ where: { id } });
|
||||
|
||||
// 2. BARU HAPUS FILE
|
||||
if (berita.image) {
|
||||
try {
|
||||
const filePath = path.join(berita.image.path, berita.image.name);
|
||||
await fs.unlink(filePath);
|
||||
|
||||
await prisma.fileStorage.delete({
|
||||
where: { id: berita.image.id },
|
||||
});
|
||||
@@ -41,15 +32,11 @@ const beritaDelete = async (context: Context) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Hapus berita dari DB
|
||||
await prisma.berita.delete({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Berita dan file terkait berhasil dihapus",
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
export default beritaDelete;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import Elysia from "elysia";
|
||||
import DaftarInformasiPublik from "./daftar_informasi_publik";
|
||||
import GrafikHasilKepuasanMasyarakat from "./ikm/grafik_hasil_kepuasan_masyarakat";
|
||||
import GrafikBerdasarkanJenisKelamin from "./ikm/grafik_berdasarkan_jenis_kelamin";
|
||||
import GrafikBerdasarkanResponden from "./ikm/grafik_responden";
|
||||
import GrafikBerdasarkanUmur from "./ikm/grafik_berdasarkan_umur";
|
||||
@@ -10,6 +9,7 @@ import ProfilePPID from "./profile_ppid";
|
||||
import VisiMisiPPID from "./visi_misi_ppid/visi_misi_ppid";
|
||||
import DasarHukumPPID from "./dasar_hukum";
|
||||
import StrukturPPID from "./struktur_ppid";
|
||||
import GrafikHasilKepuasanMasyarakat from "./ikm/grafik_hasil_kepuasan_masyarakat";
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -3,39 +3,55 @@ import { Prisma } from "@prisma/client";
|
||||
import { Context } from "elysia";
|
||||
|
||||
type FormCreate = Prisma.PermohonanInformasiPublikGetPayload<{
|
||||
select: {
|
||||
name: true;
|
||||
nik: true;
|
||||
email: true;
|
||||
notelp: true;
|
||||
alamat: true;
|
||||
jenisInformasiDimintaId: true;
|
||||
caraMemperolehInformasiId: true;
|
||||
caraMemperolehSalinanInformasiId: true;
|
||||
}
|
||||
}>
|
||||
export default async function permohonanInformasiPublikCreate(context: Context) {
|
||||
const body = context.body as FormCreate;
|
||||
|
||||
await prisma.permohonanInformasiPublik.create({
|
||||
data: {
|
||||
name: body.name,
|
||||
nik: body.nik,
|
||||
email: body.email,
|
||||
notelp: body.notelp,
|
||||
alamat: body.alamat,
|
||||
jenisInformasiDimintaId: body.jenisInformasiDimintaId,
|
||||
caraMemperolehInformasiId: body.caraMemperolehInformasiId,
|
||||
caraMemperolehSalinanInformasiId: body.caraMemperolehSalinanInformasiId,
|
||||
}
|
||||
})
|
||||
select: {
|
||||
name: true;
|
||||
nik: true;
|
||||
email: true;
|
||||
notelp: true;
|
||||
alamat: true;
|
||||
jenisInformasiDimintaId: true;
|
||||
caraMemperolehInformasiId: true;
|
||||
caraMemperolehSalinanInformasiId: true;
|
||||
};
|
||||
}>;
|
||||
|
||||
export default async function permohonanInformasiPublikCreate(context: Context) {
|
||||
const body = context.body as FormCreate;
|
||||
|
||||
// ========== VALIDASI NIK ==========
|
||||
if (body.nik && body.nik.length > 16) {
|
||||
return {
|
||||
success: true,
|
||||
message: "Permohonan Informasi Publik Berhasil Dibuat",
|
||||
data: {
|
||||
...body,
|
||||
}
|
||||
}
|
||||
success: false,
|
||||
status: 400,
|
||||
message: "Maksimal NIK adalah 16 angka",
|
||||
};
|
||||
}
|
||||
|
||||
// ========== VALIDASI NOMOR TELEPON ==========
|
||||
if (body.notelp && body.notelp.length > 15) {
|
||||
return {
|
||||
success: false,
|
||||
status: 400,
|
||||
message: "Maksimal nomor telepon adalah 15 angka",
|
||||
};
|
||||
}
|
||||
|
||||
await prisma.permohonanInformasiPublik.create({
|
||||
data: {
|
||||
name: body.name,
|
||||
nik: body.nik,
|
||||
email: body.email,
|
||||
notelp: body.notelp,
|
||||
alamat: body.alamat,
|
||||
jenisInformasiDimintaId: body.jenisInformasiDimintaId,
|
||||
caraMemperolehInformasiId: body.caraMemperolehInformasiId,
|
||||
caraMemperolehSalinanInformasiId: body.caraMemperolehSalinanInformasiId,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Permohonan Informasi Publik Berhasil Dibuat",
|
||||
data: { ...body },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,31 +3,42 @@ import { Prisma } from "@prisma/client";
|
||||
import { Context } from "elysia";
|
||||
|
||||
type FormCreate = Prisma.FormulirPermohonanKeberatanGetPayload<{
|
||||
select: {
|
||||
name: true;
|
||||
email: true;
|
||||
notelp: true;
|
||||
alasan: true;
|
||||
}
|
||||
}>
|
||||
select: {
|
||||
name: true;
|
||||
email: true;
|
||||
notelp: true;
|
||||
alasan: true;
|
||||
};
|
||||
}>;
|
||||
|
||||
export default async function permohonanKeberatanInformasiPublikCreate(context: Context) {
|
||||
const body = context.body as FormCreate;
|
||||
|
||||
await prisma.formulirPermohonanKeberatan.create({
|
||||
data: {
|
||||
name: body.name,
|
||||
email: body.email,
|
||||
notelp: body.notelp,
|
||||
alasan: body.alasan,
|
||||
}
|
||||
})
|
||||
export default async function permohonanKeberatanInformasiPublikCreate(
|
||||
context: Context
|
||||
) {
|
||||
const body = context.body as FormCreate;
|
||||
|
||||
// ========== VALIDASI NOMOR TELEPON ==========
|
||||
if (body.notelp && body.notelp.length > 15) {
|
||||
return {
|
||||
success: true,
|
||||
message: "Permohonan Keberatan Informasi Publik Berhasil Dibuat",
|
||||
data: {
|
||||
...body,
|
||||
}
|
||||
}
|
||||
}
|
||||
success: false,
|
||||
status: 400,
|
||||
message: "Maksimal nomor telepon adalah 15 angka",
|
||||
};
|
||||
}
|
||||
|
||||
await prisma.formulirPermohonanKeberatan.create({
|
||||
data: {
|
||||
name: body.name,
|
||||
email: body.email,
|
||||
notelp: body.notelp,
|
||||
alasan: body.alasan,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Permohonan Keberatan Informasi Publik Berhasil Dibuat",
|
||||
data: {
|
||||
...body,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
43
src/app/api/news/latest/route.ts
Normal file
43
src/app/api/news/latest/route.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
// app/api/news/latest/route.ts
|
||||
import { NextResponse } from "next/server";
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const berita = await prisma.berita.findMany({
|
||||
take: 3,
|
||||
orderBy: { createdAt: "desc" },
|
||||
include: { kategoriBerita: true },
|
||||
});
|
||||
|
||||
const pengumuman = await prisma.pengumuman.findMany({
|
||||
take: 3,
|
||||
orderBy: { createdAt: "desc" },
|
||||
include: { CategoryPengumuman: true },
|
||||
});
|
||||
|
||||
const news = [
|
||||
...berita.map((b) => ({
|
||||
id: b.id,
|
||||
type: "berita" as const,
|
||||
title: b.judul,
|
||||
content: b.content,
|
||||
timestamp: b.createdAt,
|
||||
kategoriBerita: b.kategoriBerita || undefined,
|
||||
})),
|
||||
...pengumuman.map((p) => ({
|
||||
id: p.id,
|
||||
type: "pengumuman" as const,
|
||||
title: p.judul,
|
||||
content: p.content,
|
||||
timestamp: p.createdAt,
|
||||
kategoriPengumuman: p.CategoryPengumuman || undefined,
|
||||
})),
|
||||
];
|
||||
|
||||
return NextResponse.json({ success: true, news }); // ✅ ganti 'data' jadi 'news'
|
||||
} catch (error) {
|
||||
console.error("API Error:", error);
|
||||
return NextResponse.json({ success: false, error: "Gagal memuat data" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
'use client';
|
||||
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
|
||||
import {
|
||||
Badge,
|
||||
@@ -51,10 +51,14 @@ export default function Content({ kategori }: { kategori: string }) {
|
||||
<Container size="xl" px={{ base: 'md', md: 'xl' }}>
|
||||
{/* === Berita Utama === */}
|
||||
{featuredState.loading ? (
|
||||
<Center><Skeleton h={400} /></Center>
|
||||
<Center>
|
||||
<Skeleton h={400} />
|
||||
</Center>
|
||||
) : featured ? (
|
||||
<Box mb={50}>
|
||||
<Text fz="h2" fw={700} mb="md">Berita Utama</Text>
|
||||
<Title order={2} mb="md">
|
||||
Berita Utama
|
||||
</Title>
|
||||
<Paper shadow="md" radius="md" withBorder>
|
||||
<Grid gutter={0}>
|
||||
<GridCol span={{ base: 12, md: 6 }}>
|
||||
@@ -74,13 +78,29 @@ export default function Content({ kategori }: { kategori: string }) {
|
||||
<Badge color="blue" variant="light" mb="md">
|
||||
{featured.kategoriBerita?.name || kategori}
|
||||
</Badge>
|
||||
<Title order={2} mb="md">{featured.judul}</Title>
|
||||
<Text c="dimmed" lineClamp={3} mb="md" dangerouslySetInnerHTML={{ __html: featured.deskripsi }} />
|
||||
<Title order={3} mb="md">
|
||||
{featured.judul}
|
||||
</Title>
|
||||
<Text
|
||||
c="dimmed"
|
||||
lineClamp={3}
|
||||
mb="md"
|
||||
style={{ lineHeight: 1.6 }}
|
||||
dangerouslySetInnerHTML={{ __html: featured.deskripsi }}
|
||||
/>
|
||||
</div>
|
||||
<Group justify="apart" mt="auto">
|
||||
<Group gap="xs">
|
||||
<IconCalendar size={18} />
|
||||
<Text size="sm">
|
||||
<Text
|
||||
fz={{ base: 'xs', md: 'sm' }}
|
||||
c="dimmed"
|
||||
lh={1.5}
|
||||
style={{
|
||||
fontSize: '0.875rem',
|
||||
lineHeight: '1.5rem',
|
||||
}}
|
||||
>
|
||||
{new Date(featured.createdAt).toLocaleDateString('id-ID', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
@@ -91,7 +111,9 @@ export default function Content({ kategori }: { kategori: string }) {
|
||||
<Button
|
||||
variant="light"
|
||||
rightSection={<IconArrowRight size={16} />}
|
||||
onClick={() => router.push(`/darmasaba/desa/berita/${kategori}/${featured.id}`)}
|
||||
onClick={() =>
|
||||
router.push(`/darmasaba/desa/berita/${kategori}/${featured.id}`)
|
||||
}
|
||||
>
|
||||
Baca Selengkapnya
|
||||
</Button>
|
||||
@@ -105,19 +127,29 @@ export default function Content({ kategori }: { kategori: string }) {
|
||||
|
||||
{/* === Daftar Berita === */}
|
||||
<Box mt={50}>
|
||||
<Title order={2} mb="md">Daftar Berita</Title>
|
||||
<Title order={2} mb="md">
|
||||
Daftar Berita
|
||||
</Title>
|
||||
<Divider mb="xl" />
|
||||
|
||||
{state.findMany.loading ? (
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl">
|
||||
{Array(3).fill(0).map((_, i) => (
|
||||
<Skeleton key={i} h={300} radius="md" />
|
||||
))}
|
||||
{Array(3)
|
||||
.fill(0)
|
||||
.map((_, i) => (
|
||||
<Skeleton key={i} h={300} radius="md" />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
) : paginatedNews.length === 0 ? (
|
||||
<Text c="dimmed" ta="center">Belum ada berita di kategori "{kategori}".</Text>
|
||||
<Text c="dimmed" ta="center" fz={{ base: 'sm', md: 'md' }} lh={1.5}>
|
||||
Belum ada berita di kategori "{kategori}".
|
||||
</Text>
|
||||
) : (
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl" verticalSpacing="xl">
|
||||
<SimpleGrid
|
||||
cols={{ base: 1, sm: 2, lg: 3 }}
|
||||
spacing="xl"
|
||||
verticalSpacing="xl"
|
||||
>
|
||||
{paginatedNews.map((item) => (
|
||||
<Card
|
||||
key={item.id}
|
||||
@@ -125,19 +157,51 @@ export default function Content({ kategori }: { kategori: string }) {
|
||||
p="lg"
|
||||
radius="md"
|
||||
withBorder
|
||||
onClick={() => router.push(`/darmasaba/desa/berita/${kategori}/${item.id}`)}
|
||||
onClick={() =>
|
||||
router.push(`/darmasaba/desa/berita/${kategori}/${item.id}`)
|
||||
}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<Card.Section>
|
||||
<Image src={item.image?.link} height={200} alt={item.judul} fit="cover" loading="lazy"/>
|
||||
<Image
|
||||
src={item.image?.link}
|
||||
height={200}
|
||||
alt={item.judul}
|
||||
fit="cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
</Card.Section>
|
||||
<Badge color="blue" variant="light" mt="md">
|
||||
{item.kategoriBerita?.name || kategori}
|
||||
</Badge>
|
||||
<Text fw={600} size="lg" mt="sm" lineClamp={2}>{item.judul}</Text>
|
||||
<Text size="sm" c="dimmed" lineClamp={3} style={{wordBreak: "break-word", whiteSpace: "normal"}} mt="xs" dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||
<Title
|
||||
order={4}
|
||||
mt="sm"
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
style={{ lineHeight: 1.4 }}
|
||||
lineClamp={2}
|
||||
>
|
||||
{item.judul}
|
||||
</Title>
|
||||
<Text
|
||||
fz={{ base: 'xs', md: 'sm' }}
|
||||
c="dimmed"
|
||||
lineClamp={3}
|
||||
style={{
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: 'normal',
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
mt="xs"
|
||||
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
|
||||
/>
|
||||
<Group justify="apart" mt="md" gap="xs">
|
||||
<Text size="xs" c="dimmed">
|
||||
<Text
|
||||
fz={{ base: 'xs', md: 'xs' }}
|
||||
c="dimmed"
|
||||
lh={1.4}
|
||||
style={{ fontSize: '0.75rem', lineHeight: '1.125rem' }}
|
||||
>
|
||||
{new Date(item.createdAt).toLocaleDateString('id-ID', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
|
||||
@@ -3,18 +3,16 @@
|
||||
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
|
||||
import NewsReader from '@/app/darmasaba/_com/NewsReader';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Center, Container, Group, Image, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { Box, Center, Container, Group, Image, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
|
||||
|
||||
function Page() {
|
||||
const params = useParams<{ id: string }>();
|
||||
const id = Array.isArray(params.id) ? params.id[0] : params.id;
|
||||
const state = useProxy(stateDashboardBerita.berita)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const state = useProxy(stateDashboardBerita.berita);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
@@ -27,9 +25,9 @@ function Page() {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
loadData()
|
||||
}, [id])
|
||||
};
|
||||
loadData();
|
||||
}, [id]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -47,41 +45,49 @@ function Page() {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"} px={{ base: "md", md: 0 }}>
|
||||
<Group px={{ base: "md", md: 100 }}>
|
||||
<Stack pos="relative" bg={colors.Bg} pb="xl" gap="xs" px={{ base: 'md', md: 0 }}>
|
||||
<Group px={{ base: 'md', md: 100 }}>
|
||||
<NewsReader />
|
||||
</Group>
|
||||
<Container w={{ base: "100%", md: "50%" }} >
|
||||
<Container w={{ base: '100%', md: '50%' }}>
|
||||
<Box pb={20}>
|
||||
<Text id='news-title' ta={"center"} fz={"2.4rem"} c={colors["blue-button"]} fw={"bold"}>
|
||||
{state.findUnique.data?.judul}
|
||||
</Text>
|
||||
<Text
|
||||
ta={"center"}
|
||||
fw={"bold"}
|
||||
fz={"1.5rem"}
|
||||
<Title
|
||||
id="news-title"
|
||||
order={1}
|
||||
ta="center"
|
||||
c={colors['blue-button']}
|
||||
fw="bold"
|
||||
lh={{ base: 1.2, md: 1.25 }}
|
||||
>
|
||||
{state.findUnique.data.judul}
|
||||
</Title>
|
||||
<Title
|
||||
order={2}
|
||||
ta="center"
|
||||
fw="bold"
|
||||
fz={{ base: 'md', md: 'lg' }}
|
||||
lh={{ base: 1.3, md: 1.35 }}
|
||||
>
|
||||
Informasi dan Pelayanan Administrasi Digital
|
||||
</Text>
|
||||
</Title>
|
||||
</Box>
|
||||
<Image src={state.findUnique.data?.image?.link || ''} alt='' w={"100%"} loading="lazy" />
|
||||
<Image src={state.findUnique.data.image?.link || ''} alt="" w="100%" loading="lazy" />
|
||||
</Container>
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Stack gap={"xs"}>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Stack gap="xs">
|
||||
<Text
|
||||
id='news-content'
|
||||
id="news-content"
|
||||
py={20}
|
||||
fz={{ base: "sm", md: "lg" }}
|
||||
lh={{ base: 1.6, md: 1.8 }} // ✅ line-height lebih rapat dan responsif
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={{ base: 1.6, md: 1.8 }}
|
||||
ta="justify"
|
||||
style={{
|
||||
wordBreak: "break-word",
|
||||
whiteSpace: "normal",
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: 'normal',
|
||||
}}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: state.findUnique.data?.content || "",
|
||||
__html: state.findUnique.data.content || '',
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
@@ -90,4 +96,4 @@ function Page() {
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
export default Page;
|
||||
@@ -1,12 +1,44 @@
|
||||
// app/desa/berita/BeritaLayoutClient.tsx
|
||||
'use client'
|
||||
import dynamic from 'next/dynamic';
|
||||
// app/darmasaba/(pages)/desa/berita/layout.tsx
|
||||
'use client';
|
||||
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { ReactNode } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { Box } from '@mantine/core';
|
||||
import BackButton from '../layanan/_com/BackButto';
|
||||
import colors from '@/con/colors';
|
||||
const LayoutTabsBerita = dynamic(
|
||||
() => import('./_lib/layoutTabs'),
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
export default function BeritaLayoutClient({ children }: { children: React.ReactNode }) {
|
||||
export default function BeritaLayoutClient({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
|
||||
// Contoh path:
|
||||
// - /darmasaba/desa/berita/semua → panjang 5 → list
|
||||
// - /darmasaba/desa/berita/Pemerintahan → panjang 5 → list
|
||||
// - /darmasaba/desa/berita/Pemerintahan/123 → panjang 6 → detail
|
||||
|
||||
const segments = pathname.split('/').filter(Boolean);
|
||||
const isDetailPage = segments.length === 5; // [darmasaba, desa, berita, kategori, id]
|
||||
|
||||
if (isDetailPage) {
|
||||
// Tampilkan tanpa tab menu
|
||||
return (
|
||||
<Box bg={colors.Bg}>
|
||||
<Box pt={33} px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Tampilkan dengan tab menu (untuk /semua atau /kategori)
|
||||
return <LayoutTabsBerita>{children}</LayoutTabsBerita>;
|
||||
}
|
||||
@@ -16,35 +16,30 @@ function Semua() {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useTransitionRouter();
|
||||
|
||||
// Ambil parameter langsung dari URL
|
||||
const search = searchParams.get('search') || '';
|
||||
const page = parseInt(searchParams.get('page') || '1');
|
||||
|
||||
// Gunakan proxy untuk state global
|
||||
const state = useProxy(stateDashboardBerita.berita);
|
||||
const featured = useProxy(stateDashboardBerita.berita.findFirst);
|
||||
const loadingGrid = state.findMany.loading;
|
||||
const loadingFeatured = featured.loading;
|
||||
|
||||
// Load berita utama sekali saja
|
||||
useEffect(() => {
|
||||
if (!featured.data && !loadingFeatured) {
|
||||
stateDashboardBerita.berita.findFirst.load();
|
||||
}
|
||||
}, [featured.data, loadingFeatured]);
|
||||
|
||||
// Load berita terbaru tiap page / search berubah
|
||||
useEffect(() => {
|
||||
const limit = 3;
|
||||
state.findMany.load(page, limit, search);
|
||||
}, [page, search]);
|
||||
|
||||
// Handler pagination → langsung update URL
|
||||
const handlePageChange = (newPage: number) => {
|
||||
const url = new URLSearchParams(searchParams.toString());
|
||||
if (search) url.set('search', search);
|
||||
if (newPage > 1) url.set('page', newPage.toString());
|
||||
else url.delete('page'); // biar page=1 ga muncul di URL
|
||||
else url.delete('page');
|
||||
|
||||
router.replace(`?${url.toString()}`);
|
||||
};
|
||||
@@ -61,7 +56,7 @@ function Semua() {
|
||||
<Center><Skeleton h={400} /></Center>
|
||||
) : featuredData ? (
|
||||
<Box mb={50}>
|
||||
<Text fz="h2" fw={700} mb="md">Berita Utama</Text>
|
||||
<Title order={2} mb="md">Berita Utama</Title>
|
||||
<Paper shadow="md" radius="md" withBorder>
|
||||
<Grid gutter={0}>
|
||||
<GridCol span={{ base: 12, md: 6 }}>
|
||||
@@ -81,13 +76,24 @@ function Semua() {
|
||||
<Badge color="blue" variant="light" mb="md">
|
||||
{featuredData.kategoriBerita?.name || 'Berita'}
|
||||
</Badge>
|
||||
<Title order={2} mb="md">{featuredData.judul}</Title>
|
||||
<Text c="dimmed" lineClamp={3} mb="md" dangerouslySetInnerHTML={{ __html: featuredData.deskripsi }} />
|
||||
<Title order={3} mb="md">{featuredData.judul}</Title>
|
||||
<Text
|
||||
c="dimmed"
|
||||
lineClamp={3}
|
||||
mb="md"
|
||||
dangerouslySetInnerHTML={{ __html: featuredData.deskripsi }}
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={{ base: 1.5, md: 1.6 }}
|
||||
/>
|
||||
</div>
|
||||
<Group justify="apart" mt="auto">
|
||||
<Group gap="xs">
|
||||
<IconCalendar size={18} />
|
||||
<Text size="sm">
|
||||
<Text
|
||||
c="dimmed"
|
||||
fz={{ base: 'xs', md: 'sm' }}
|
||||
lh={{ base: 1.4, md: 1.5 }}
|
||||
>
|
||||
{new Date(featuredData.createdAt).toLocaleDateString('id-ID', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
@@ -124,7 +130,9 @@ function Semua() {
|
||||
))}
|
||||
</SimpleGrid>
|
||||
) : paginatedNews.length === 0 ? (
|
||||
<Text c="dimmed" ta="center">Tidak ada berita ditemukan.</Text>
|
||||
<Text c="dimmed" ta="center" fz={{ base: 'sm', md: 'md' }} lh={{ base: 1.5, md: 1.6 }}>
|
||||
Tidak ada berita ditemukan.
|
||||
</Text>
|
||||
) : (
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl" verticalSpacing="xl">
|
||||
{paginatedNews.map((item) => (
|
||||
@@ -143,11 +151,24 @@ function Semua() {
|
||||
{item.kategoriBerita?.name || 'Berita'}
|
||||
</Badge>
|
||||
|
||||
<Text fw={600} size="lg" mt="sm" lineClamp={2}>{item.judul}</Text>
|
||||
<Text size="sm" c="dimmed" lineClamp={3} mt="xs" dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||
<Title order={4} mt="sm" lineClamp={2}>
|
||||
{item.judul}
|
||||
</Title>
|
||||
<Text
|
||||
c="dimmed"
|
||||
lineClamp={3}
|
||||
mt="xs"
|
||||
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
|
||||
fz={{ base: 'xs', md: 'sm' }}
|
||||
lh={{ base: 1.5, md: 1.6 }}
|
||||
/>
|
||||
|
||||
<Flex align="center" justify="apart" mt="md" gap="xs">
|
||||
<Text size="xs" c="dimmed">
|
||||
<Text
|
||||
c="dimmed"
|
||||
fz={{ base: 'xs', md: 'xs' }}
|
||||
lh={{ base: 1.4, md: 1.4 }}
|
||||
>
|
||||
{new Date(item.createdAt).toLocaleDateString('id-ID', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
@@ -187,4 +208,4 @@ function Semua() {
|
||||
);
|
||||
}
|
||||
|
||||
export default Semua;
|
||||
export default Semua;
|
||||
@@ -1,150 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Center, Image, Pagination, Paper, SimpleGrid, Stack, Text } from '@mantine/core';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import ApiFetch from '@/lib/api-fetch';
|
||||
|
||||
interface FileItem {
|
||||
id: string;
|
||||
name: string;
|
||||
link: string;
|
||||
realName: string;
|
||||
createdAt: string | Date;
|
||||
category: string;
|
||||
path: string;
|
||||
mimeType: string;
|
||||
}
|
||||
|
||||
export default function FotoContent() {
|
||||
const [files, setFiles] = useState<FileItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [search, setSearch] = useState('');
|
||||
const [page, setPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const limit = 9; // ✅ ambil 12 data per page
|
||||
|
||||
const loadData = useCallback(async (pageNum: number, searchTerm: string) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const query: Record<string, string> = {
|
||||
category: 'image',
|
||||
page: pageNum.toString(),
|
||||
limit: limit.toString(),
|
||||
};
|
||||
if (searchTerm) query.search = searchTerm;
|
||||
|
||||
const response = await ApiFetch.api.fileStorage.findMany.get({ query });
|
||||
|
||||
if (response.status === 200 && response.data) {
|
||||
setFiles(response.data.data || []);
|
||||
setTotalPages(response.data.meta?.totalPages || 1);
|
||||
} else {
|
||||
setFiles([]);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Load error:', err);
|
||||
setFiles([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// ✅ Initial load + update when URL/search changes
|
||||
useEffect(() => {
|
||||
const handleRouteChange = () => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const urlSearch = urlParams.get('search') || '';
|
||||
const urlPage = parseInt(urlParams.get('page') || '1');
|
||||
setSearch(urlSearch);
|
||||
setPage(urlPage);
|
||||
loadData(urlPage, urlSearch);
|
||||
};
|
||||
|
||||
const handleSearchUpdate = (e: Event) => {
|
||||
const { search } = (e as CustomEvent).detail;
|
||||
setSearch(search);
|
||||
setPage(1);
|
||||
loadData(1, search);
|
||||
};
|
||||
|
||||
handleRouteChange();
|
||||
window.addEventListener('popstate', handleRouteChange);
|
||||
window.addEventListener('searchUpdate', handleSearchUpdate as EventListener);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('popstate', handleRouteChange);
|
||||
window.removeEventListener('searchUpdate', handleSearchUpdate as EventListener);
|
||||
};
|
||||
}, [loadData]);
|
||||
|
||||
// ✅ Update when page/search changes
|
||||
useEffect(() => {
|
||||
loadData(page, search);
|
||||
}, [page, search, loadData]);
|
||||
|
||||
const updateURL = (newSearch: string, newPage: number) => {
|
||||
const url = new URL(window.location.href);
|
||||
if (newSearch) url.searchParams.set('search', newSearch);
|
||||
else url.searchParams.delete('search');
|
||||
if (newPage > 1) url.searchParams.set('page', newPage.toString());
|
||||
else url.searchParams.delete('page');
|
||||
window.history.pushState({}, '', url);
|
||||
};
|
||||
|
||||
const handlePageChange = (newPage: number) => {
|
||||
setPage(newPage);
|
||||
updateURL(search, newPage);
|
||||
};
|
||||
|
||||
if (loading && files.length === 0) {
|
||||
return <Center>Memuat data...</Center>;
|
||||
}
|
||||
|
||||
if (files.length === 0) {
|
||||
return <Center>Tidak ada foto ditemukan</Center>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box pt={20} px={{ base: 'md', md: 100 }}>
|
||||
<SimpleGrid cols={{ base: 1, md: 3 }}>
|
||||
{files.map((file) => (
|
||||
<Paper
|
||||
key={file.id}
|
||||
mb={50}
|
||||
p="md"
|
||||
radius={26}
|
||||
bg={colors['white-trans-1']}
|
||||
style={{ height: '100%' }}
|
||||
>
|
||||
<Box style={{ height: '250px', overflow: 'hidden', borderRadius: '12px' }}>
|
||||
<Image
|
||||
src={file.link}
|
||||
alt={file.realName || file.name}
|
||||
height={250}
|
||||
width="100%"
|
||||
style={{ objectFit: 'cover', height: '100%', width: '100%' }}
|
||||
loading="lazy"
|
||||
/>
|
||||
</Box>
|
||||
<Stack gap="sm" py={10}>
|
||||
<Text fw="bold" fz={{ base: 'h4', md: 'h3' }}>
|
||||
{file.realName || file.name}
|
||||
</Text>
|
||||
<Text fz="sm" c="dimmed">
|
||||
{new Date(file.createdAt).toLocaleDateString('id-ID', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
<Center mt="xl">
|
||||
<Pagination total={totalPages} value={page} onChange={handlePageChange} />
|
||||
</Center>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,25 +1,173 @@
|
||||
'use client'
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
import { Suspense } from 'react';
|
||||
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
Box,
|
||||
Center,
|
||||
Grid,
|
||||
Image,
|
||||
Pagination,
|
||||
Paper,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconPhoto } from '@tabler/icons-react';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
// ✅ Load komponen tanpa SSR
|
||||
const FotoContent = dynamic(
|
||||
() => import('./Content'),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => <div>Memuat konten...</div>
|
||||
}
|
||||
);
|
||||
// Komponen kartu foto
|
||||
function FotoCard({ item }: { item: any }) {
|
||||
|
||||
function PageContent() {
|
||||
return (
|
||||
<Suspense fallback={<div>Memuat...</div>}>
|
||||
<FotoContent />
|
||||
</Suspense>
|
||||
<Grid.Col span={{ base: 12, xs: 6, md: 4 }}>
|
||||
<Paper
|
||||
shadow="sm"
|
||||
radius="md"
|
||||
p={0}
|
||||
style={{ transition: 'transform 0.2s' }}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.02)')}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')}
|
||||
|
||||
>
|
||||
{item.imageGalleryFoto?.link ? (
|
||||
<Box
|
||||
pos="relative"
|
||||
style={{
|
||||
paddingBottom: '100%',
|
||||
overflow: 'hidden',
|
||||
borderRadius: '4px 4px 0 0',
|
||||
backgroundColor: '#f9f9f9',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
radius="lg"
|
||||
src={item.imageGalleryFoto.link}
|
||||
alt={item.name || 'Foto Galeri'}
|
||||
p={10}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'contain',
|
||||
objectPosition: 'center',
|
||||
}}
|
||||
loading="lazy"
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<Center h={180} bg="gray.1">
|
||||
<IconPhoto size={40} color="gray" />
|
||||
</Center>
|
||||
)}
|
||||
|
||||
<Stack p="md" gap={4}>
|
||||
<Text fw={600} lineClamp={1} fz={{ base: 'sm', md: 'md' }} lh={{ base: '1.4', md: '1.5' }}>
|
||||
{item.name || 'Tanpa Judul'}
|
||||
</Text>
|
||||
{item.deskripsi && (
|
||||
<Text
|
||||
fz={{ base: 'xs', md: 'sm' }}
|
||||
c="dimmed"
|
||||
lineClamp={2}
|
||||
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
|
||||
lh={{ base: '1.4', md: '1.5' }}
|
||||
/>
|
||||
)}
|
||||
<Text
|
||||
fz={{ base: 11, md: 'xs' }}
|
||||
c="dimmed"
|
||||
lh={{ base: '1.3', md: '1.4' }}
|
||||
>
|
||||
{new Date(item.createdAt).toLocaleDateString('id-ID', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Grid.Col>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
return <PageContent />;
|
||||
// Komponen utama
|
||||
export default function GaleriFotoUser() {
|
||||
const [search] = useState('');
|
||||
return (
|
||||
<Box py="xl" px={{ base: 'md', md: 'lg' }}>
|
||||
{/* Header */}
|
||||
<Title order={2} c={colors['blue-button']} mb="lg" ta="center">
|
||||
Galeri Foto Desa Darmasaba
|
||||
</Title>
|
||||
|
||||
{/* Daftar Foto */}
|
||||
<FotoList search={search} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function FotoList({ search }: { search: string }) {
|
||||
const FotoState = useProxy(stateGallery.foto);
|
||||
|
||||
const { data, page, totalPages, loading, load } = FotoState.findMany;
|
||||
|
||||
useShallowEffect(() => {
|
||||
load(page, 3, search);
|
||||
}, [page, search]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Grid mt="md">
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<Grid.Col key={i} span={{ base: 12, xs: 6, md: 4 }}>
|
||||
<Skeleton height={280} radius="md" />
|
||||
</Grid.Col>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
return (
|
||||
<Center py="xl">
|
||||
<Stack align="center" c="dimmed">
|
||||
<IconPhoto size={48} />
|
||||
<Text fz={{ base: 'sm', md: 'md' }} lh={{ base: '1.4', md: '1.5' }}>
|
||||
Tidak ada foto ditemukan
|
||||
</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack mt="md" gap="xl">
|
||||
<Grid>
|
||||
{data.map((item) => (
|
||||
<FotoCard key={item.id} item={item} />
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
{/* Pagination */}
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 3, search);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
total={totalPages}
|
||||
color="blue"
|
||||
radius="md"
|
||||
/>
|
||||
</Center>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,36 @@
|
||||
'use client'
|
||||
import { usePathname } from "next/navigation";
|
||||
import { ReactNode } from "react";
|
||||
import LayoutTabsGalery from "./_lib/layoutTabs";
|
||||
|
||||
export default function LayoutGalery({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<LayoutTabsGalery>
|
||||
{children}
|
||||
</LayoutTabsGalery>
|
||||
)
|
||||
// export default function LayoutGalery({ children }: { children: React.ReactNode }) {
|
||||
// return (
|
||||
// <LayoutTabsGalery>
|
||||
// {children}
|
||||
// </LayoutTabsGalery>
|
||||
// )
|
||||
// }
|
||||
|
||||
export default function BeritaLayoutClient({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
|
||||
// Contoh path:
|
||||
// - /darmasaba/desa/berita/semua → panjang 5 → list
|
||||
// - /darmasaba/desa/berita/Pemerintahan → panjang 5 → list
|
||||
// - /darmasaba/desa/berita/Pemerintahan/123 → panjang 6 → detail
|
||||
|
||||
const segments = pathname.split('/').filter(Boolean);
|
||||
const isDetailPage = segments.length === 5; // [darmasaba, desa, berita, kategori, id]
|
||||
|
||||
if (isDetailPage) {
|
||||
// Tampilkan tanpa tab menu
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
// Tampilkan dengan tab menu (untuk /semua atau /kategori)
|
||||
return <LayoutTabsGalery>{children}</LayoutTabsGalery>;
|
||||
}
|
||||
@@ -4,29 +4,29 @@ import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Group,
|
||||
Pagination,
|
||||
Paper,
|
||||
SimpleGrid,
|
||||
Spoiler,
|
||||
Stack,
|
||||
Text,
|
||||
Title
|
||||
} from '@mantine/core';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTransitionRouter } from 'next-view-transitions';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useSnapshot } from 'valtio';
|
||||
|
||||
export default function VideoContent() {
|
||||
// ✅ expanded state per index
|
||||
const [expandedMap, setExpandedMap] = useState<Record<number, boolean>>({});
|
||||
const videoState = useSnapshot(stateGallery.video);
|
||||
const router = useTransitionRouter();
|
||||
const { data, page, totalPages, loading } = videoState.findMany;
|
||||
|
||||
// Handle search and pagination changes
|
||||
const loadData = useCallback((pageNum: number, searchTerm: string) => {
|
||||
stateGallery.video.findMany.load(pageNum, 10, searchTerm.trim());
|
||||
stateGallery.video.findMany.load(pageNum, 3, searchTerm.trim());
|
||||
}, []);
|
||||
|
||||
// Initial load and URL change handler
|
||||
useEffect(() => {
|
||||
const handleRouteChange = () => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
@@ -56,19 +56,14 @@ export default function VideoContent() {
|
||||
loadData(newPage, search);
|
||||
};
|
||||
|
||||
const toggleExpanded = (index: number, value: boolean) => {
|
||||
setExpandedMap((prev) => ({
|
||||
...prev,
|
||||
[index]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const dataVideo = data || [];
|
||||
|
||||
if (loading && !data) {
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Text>Memuat Video...</Text>
|
||||
<Text fz={{ base: 'sm', md: 'md' }} c="dimmed" ta="center">
|
||||
Memuat Video...
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -83,60 +78,71 @@ export default function VideoContent() {
|
||||
p="md"
|
||||
radius={26}
|
||||
bg={colors['white-trans-1']}
|
||||
w={{ base: '100%', md: '100%' }}
|
||||
w="100%"
|
||||
>
|
||||
<Box>
|
||||
<Center>
|
||||
<Box
|
||||
component="iframe"
|
||||
src={convertToEmbedUrl(v.linkVideo)}
|
||||
width="100%"
|
||||
height={300}
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
style={{ borderRadius: 8 }}
|
||||
/>
|
||||
</Center>
|
||||
</Box>
|
||||
<Box>
|
||||
<Stack gap="sm" py={10}>
|
||||
<Text fz="sm" c="dimmed">
|
||||
{new Date(v.createdAt).toLocaleDateString('id-ID', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</Text>
|
||||
<Text fw="bold" fz="sm" lineClamp={1}>
|
||||
{v.name}
|
||||
</Text>
|
||||
<Spoiler
|
||||
showLabel={
|
||||
<Text fw="bold" fz="sm" c={colors['blue-button']}>
|
||||
Show more
|
||||
</Text>
|
||||
}
|
||||
hideLabel={
|
||||
<Text fw="bold" fz="sm" c={colors['blue-button']}>
|
||||
Hide details
|
||||
</Text>
|
||||
}
|
||||
expanded={expandedMap[k] || false}
|
||||
onExpandedChange={(val) => toggleExpanded(k, val)}
|
||||
<Center>
|
||||
<Box
|
||||
component="iframe"
|
||||
src={convertToEmbedUrl(v.linkVideo)}
|
||||
width="100%"
|
||||
height={300}
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
style={{ borderRadius: 8 }}
|
||||
/>
|
||||
</Center>
|
||||
|
||||
<Stack gap="sm" py={10}>
|
||||
{/* Tanggal: Caption */}
|
||||
<Text
|
||||
fz={{ base: 12, md: 14 }}
|
||||
c="dimmed"
|
||||
ta="left"
|
||||
>
|
||||
{new Date(v.createdAt).toLocaleDateString('id-ID', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</Text>
|
||||
|
||||
{/* Judul Video: Subsection (H3) */}
|
||||
<Title
|
||||
order={3}
|
||||
c="dark"
|
||||
ta="left"
|
||||
lh={1.3}
|
||||
style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}
|
||||
>
|
||||
{v.name}
|
||||
</Title>
|
||||
|
||||
{/* Deskripsi: Body kecil */}
|
||||
<Text
|
||||
ta="justify"
|
||||
fz={{ base: 13, md: 14 }}
|
||||
c="dimmed"
|
||||
style={{ wordBreak: 'break-word' }}
|
||||
lineClamp={3}
|
||||
>
|
||||
<span dangerouslySetInnerHTML={{ __html: v.deskripsi }} />
|
||||
</Text>
|
||||
|
||||
<Group justify="right">
|
||||
<Button
|
||||
onClick={() => router.push(`/darmasaba/desa/galery/video/${v.id}`)}
|
||||
bg={colors['blue-button']}
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
>
|
||||
<Text
|
||||
ta="justify"
|
||||
fz="sm"
|
||||
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
|
||||
style={{wordBreak: "break-word", whiteSpace: "normal"}}
|
||||
/>
|
||||
</Spoiler>
|
||||
</Stack>
|
||||
</Box>
|
||||
Detail
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
@@ -150,7 +156,6 @@ export default function VideoContent() {
|
||||
);
|
||||
}
|
||||
|
||||
// ✅ Fix: convert YouTube URL ke embed
|
||||
function convertToEmbedUrl(youtubeUrl: string): string {
|
||||
try {
|
||||
const url = new URL(youtubeUrl);
|
||||
@@ -161,4 +166,4 @@ function convertToEmbedUrl(youtubeUrl: string): string {
|
||||
console.error('Error converting YouTube URL to embed:', err);
|
||||
return youtubeUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
209
src/app/darmasaba/(pages)/desa/galery/video/[id]/page.tsx
Normal file
209
src/app/darmasaba/(pages)/desa/galery/video/[id]/page.tsx
Normal file
@@ -0,0 +1,209 @@
|
||||
'use client';
|
||||
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Group,
|
||||
Paper,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Text,
|
||||
ThemeIcon,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconArrowBack, IconInfoCircle, IconVideo } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
|
||||
import BackButton from '../../../layanan/_com/BackButto';
|
||||
|
||||
|
||||
function convertToEmbedUrl(youtubeUrl: string): string {
|
||||
try {
|
||||
const url = new URL(youtubeUrl);
|
||||
let videoId = '';
|
||||
|
||||
if (url.hostname === 'youtu.be') {
|
||||
videoId = url.pathname.slice(1);
|
||||
} else if (url.hostname.includes('youtube.com')) {
|
||||
videoId = url.searchParams.get('v') || '';
|
||||
}
|
||||
|
||||
return videoId ? `https://www.youtube.com/embed/${videoId}` : youtubeUrl;
|
||||
} catch {
|
||||
return youtubeUrl;
|
||||
}
|
||||
}
|
||||
|
||||
export default function DetailVideoUser() {
|
||||
const params = useParams<{ id: string }>();
|
||||
const router = useRouter();
|
||||
const videoState = useProxy(stateGallery.video);
|
||||
const [videoError, setVideoError] = useState(false);
|
||||
|
||||
const id = Array.isArray(params.id) ? params.id[0] : params.id;
|
||||
|
||||
useShallowEffect(() => {
|
||||
if (id) {
|
||||
videoState.findUnique.load(id);
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
const data = videoState.findUnique.data;
|
||||
|
||||
if (!videoState.findUnique && !id) {
|
||||
return (
|
||||
<Box py="xl" px={{ base: 'md', md: 'lg' }}>
|
||||
<Skeleton height={400} radius="md" />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<Box py="xl" px={{ base: 'md', md: 'lg' }}>
|
||||
<Alert
|
||||
icon={<IconInfoCircle size={20} />}
|
||||
title="Video tidak ditemukan"
|
||||
color="red"
|
||||
radius="md"
|
||||
>
|
||||
<Text fz={{ base: 'sm', md: 'md' }} c="red.9">
|
||||
Video yang Anda cari tidak tersedia.
|
||||
</Text>
|
||||
</Alert>
|
||||
<Button
|
||||
leftSection={<IconArrowBack size={16} />}
|
||||
mt="md"
|
||||
onClick={() => router.push('/darmasaba/galeri/video')}
|
||||
variant="outline"
|
||||
>
|
||||
Kembali ke Galeri
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const embedUrl = data.linkVideo ? convertToEmbedUrl(data.linkVideo) : null;
|
||||
|
||||
return (
|
||||
<Box py="xl" px={{ base: 'md', md: 100 }}>
|
||||
{/* Tombol Kembali */}
|
||||
<Box>
|
||||
<BackButton />
|
||||
</Box>
|
||||
|
||||
{/* Header - Dijadikan Title */}
|
||||
<Title
|
||||
order={1}
|
||||
ta="center"
|
||||
c={colors['blue-button']}
|
||||
mb="lg"
|
||||
lh={{ base: 1.2, md: 1.25 }}
|
||||
>
|
||||
{data.name || 'Video Galeri Desa'}
|
||||
</Title>
|
||||
|
||||
{/* Konten Utama */}
|
||||
<Card
|
||||
shadow="sm"
|
||||
radius="md"
|
||||
p={{ base: 'md', md: 'xl' }}
|
||||
bg={colors['white-1']}
|
||||
>
|
||||
<Stack gap="lg">
|
||||
{/* Video */}
|
||||
{embedUrl ? (
|
||||
<Box
|
||||
pos="relative"
|
||||
style={{ paddingBottom: '56.25%', height: 0, overflow: 'hidden' }}
|
||||
>
|
||||
<iframe
|
||||
src={embedUrl}
|
||||
title={data.name}
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: 8,
|
||||
border: 'none',
|
||||
}}
|
||||
onError={() => setVideoError(true)}
|
||||
/>
|
||||
</Box>
|
||||
) : videoError ? (
|
||||
<Alert
|
||||
color="orange"
|
||||
icon={<IconVideo size={20} />}
|
||||
title="Gagal memuat video"
|
||||
radius="md"
|
||||
>
|
||||
<Text fz={{ base: 'xs', md: 'sm' }} c="orange.9">
|
||||
Mohon maaf, video tidak dapat diputar.
|
||||
</Text>
|
||||
</Alert>
|
||||
) : (
|
||||
<Alert
|
||||
color="gray"
|
||||
icon={<IconInfoCircle size={20} />}
|
||||
title="Tidak ada video"
|
||||
radius="md"
|
||||
>
|
||||
<Text fz={{ base: 'xs', md: 'sm' }} c="dimmed">
|
||||
Konten video belum tersedia.
|
||||
</Text>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Informasi Tambahan */}
|
||||
{data.createdAt && (
|
||||
<Group gap="xs" justify="center" wrap="nowrap">
|
||||
<ThemeIcon variant="light" size="sm" radius="xl">
|
||||
<IconInfoCircle size={14} />
|
||||
</ThemeIcon>
|
||||
<Text
|
||||
fz={{ base: 'xs', md: 'sm' }}
|
||||
c="dimmed"
|
||||
lh={{ base: 1.4, md: 1.5 }}
|
||||
>
|
||||
Diunggah pada{' '}
|
||||
{new Date(data.createdAt).toLocaleDateString('id-ID', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})}
|
||||
</Text>
|
||||
</Group>
|
||||
)}
|
||||
|
||||
{/* Deskripsi */}
|
||||
{data.deskripsi && (
|
||||
<Paper p="md" bg="gray.0" radius="md">
|
||||
<Text
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
c="dark"
|
||||
ta={"justify"}
|
||||
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
|
||||
style={{
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: 'normal',
|
||||
lineHeight: 1.6,
|
||||
}}
|
||||
/>
|
||||
</Paper>
|
||||
)}
|
||||
</Stack>
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -100,7 +100,7 @@ function Page() {
|
||||
{data.name}
|
||||
</Text>
|
||||
</Container>
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Box px={{ base: "35", md: 100 }}>
|
||||
<Stack gap="md">
|
||||
<Text
|
||||
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
|
||||
import colors from '@/con/colors';
|
||||
import { ActionIcon, Box, Divider, Flex, Group, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
|
||||
import { ActionIcon, Box, Divider, Flex, Group, Skeleton, Stack, Text, Title, Tooltip } from '@mantine/core';
|
||||
import { IconBrandFacebook, IconBrandInstagram, IconBrandTwitter, IconBrandWhatsapp } from '@tabler/icons-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
@@ -39,30 +39,38 @@ function PelayananPendudukNonPermanent() {
|
||||
) : (
|
||||
<Stack gap="xl">
|
||||
<Box>
|
||||
<Text fz={{ base: "xl", md: "2xl" }} fw={700} lh={1.3} c="dark">
|
||||
<Title
|
||||
order={1}
|
||||
fz={{ base: 'lg', md: 'xl' }}
|
||||
fw={700}
|
||||
lh={{ base: 1.3, md: 1.3 }}
|
||||
c="dark"
|
||||
>
|
||||
{data?.name || "Judul belum tersedia"}
|
||||
</Text>
|
||||
</Title>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
{data?.deskripsi ? (
|
||||
<Text
|
||||
fz={{ base: "sm", md: "md" }}
|
||||
lh={1.7}
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={{ base: 1.6, md: 1.7 }}
|
||||
ta="justify"
|
||||
c="dimmed"
|
||||
c="black"
|
||||
dangerouslySetInnerHTML={{ __html: data?.deskripsi }}
|
||||
style={{wordBreak: "break-word", whiteSpace: "normal"}}
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
/>
|
||||
) : (
|
||||
<Text fz="sm" c="gray">Deskripsi belum tersedia.</Text>
|
||||
<Text fz="xs" c="gray">
|
||||
Deskripsi belum tersedia.
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Divider color={colors["blue-button"]} size="sm" />
|
||||
|
||||
<Flex justify="space-between" align="center" wrap="wrap" gap="md">
|
||||
<Text fz={{ base: "xs", md: "sm" }} c="dimmed">
|
||||
<Text fz={{ base: 'xs', md: 'sm' }} lh={{ base: 1.4, md: 1.5 }} c="black">
|
||||
25 Mei 2021 • Darmasaba
|
||||
</Text>
|
||||
<Group gap="md">
|
||||
@@ -96,4 +104,4 @@ function PelayananPendudukNonPermanent() {
|
||||
);
|
||||
}
|
||||
|
||||
export default PelayananPendudukNonPermanent;
|
||||
export default PelayananPendudukNonPermanent;
|
||||
@@ -47,7 +47,7 @@ function PelayananPerizinanBerusaha() {
|
||||
return (
|
||||
<Center mih={300}>
|
||||
<Stack align="center" gap="sm">
|
||||
<Text fz="lg" fw={500} c="dimmed">
|
||||
<Text fz={{ base: 'md', md: 'lg' }} fw={500} c="dimmed" lh="sm">
|
||||
Belum ada informasi layanan yang tersedia
|
||||
</Text>
|
||||
<Button component="a" href="https://oss.go.id" target="_blank" radius="xl">
|
||||
@@ -67,10 +67,10 @@ function PelayananPerizinanBerusaha() {
|
||||
) : (
|
||||
<Stack gap="lg">
|
||||
<Box>
|
||||
<Title order={2} fw={700} fz={{ base: 22, md: 32 }} mb="sm">
|
||||
<Title order={2} fw={700} mb="sm">
|
||||
Perizinan Berusaha Berbasis Risiko melalui OSS
|
||||
</Title>
|
||||
<Text fz={{ base: 'sm', md: 'md' }} c="dimmed">
|
||||
<Text fz={{ base: 'sm', md: 'md' }} c="black" lh="sm">
|
||||
Sistem Online Single Submission (OSS) untuk pendaftaran NIB
|
||||
</Text>
|
||||
</Box>
|
||||
@@ -83,13 +83,13 @@ function PelayananPerizinanBerusaha() {
|
||||
/>
|
||||
|
||||
<Box>
|
||||
<Text fw={600} mb="sm" fz={{ base: 'sm', md: 'lg' }}>
|
||||
<Title order={3} fw={600} mb="sm">
|
||||
Alur pendaftaran NIB:
|
||||
</Text>
|
||||
</Title>
|
||||
<Stepper
|
||||
active={active}
|
||||
onStepClick={(step) => {
|
||||
if (step <= active) { // Only allow clicking on previous or current steps
|
||||
if (step <= active) {
|
||||
setActive(step);
|
||||
}
|
||||
}}
|
||||
@@ -102,28 +102,42 @@ function PelayananPerizinanBerusaha() {
|
||||
}}
|
||||
>
|
||||
<StepperStep label="Langkah 1" description="Daftar Akun">
|
||||
<Text fz="sm">Membuat akun di portal OSS</Text>
|
||||
<Text fz={{ base: 'xs', md: 'sm' }} lh="sm">
|
||||
Membuat akun di portal OSS
|
||||
</Text>
|
||||
</StepperStep>
|
||||
<StepperStep label="Langkah 2" description="Isi Data Perusahaan">
|
||||
<Text fz="sm">Lengkapi informasi perusahaan, data pemegang saham, dan alamat</Text>
|
||||
<Text fz={{ base: 'xs', md: 'sm' }} lh="sm">
|
||||
Lengkapi informasi perusahaan, data pemegang saham, dan alamat
|
||||
</Text>
|
||||
</StepperStep>
|
||||
<StepperStep label="Langkah 3" description="Pilih KBLI">
|
||||
<Text fz="sm">Menentukan kode KBLI sesuai jenis usaha</Text>
|
||||
<Text fz={{ base: 'xs', md: 'sm' }} lh="sm">
|
||||
Menentukan kode KBLI sesuai jenis usaha
|
||||
</Text>
|
||||
</StepperStep>
|
||||
<StepperStep label="Langkah 4" description="Unggah Dokumen">
|
||||
<Text fz="sm">Unggah akta pendirian, surat izin, dan dokumen wajib lainnya</Text>
|
||||
<Text fz={{ base: 'xs', md: 'sm' }} lh="sm">
|
||||
Unggah akta pendirian, surat izin, dan dokumen wajib lainnya
|
||||
</Text>
|
||||
</StepperStep>
|
||||
<StepperStep label="Langkah 5" description="Verifikasi Instansi">
|
||||
<Text fz="sm">Menunggu verifikasi dan persetujuan dari pihak berwenang</Text>
|
||||
<Text fz={{ base: 'xs', md: 'sm' }} lh="sm">
|
||||
Menunggu verifikasi dan persetujuan dari pihak berwenang
|
||||
</Text>
|
||||
</StepperStep>
|
||||
<StepperStep label="Langkah 6" description="Terbit NIB">
|
||||
<Text fz="sm">Menerima NIB sebagai identitas resmi usaha</Text>
|
||||
<Text fz={{ base: 'xs', md: 'sm' }} lh="sm">
|
||||
Menerima NIB sebagai identitas resmi usaha
|
||||
</Text>
|
||||
</StepperStep>
|
||||
<StepperCompleted>
|
||||
<Center>
|
||||
<Stack align="center" gap="xs">
|
||||
<IconCheck size={40} color="green" />
|
||||
<Text fz="sm" fw={500}>Proses pendaftaran selesai</Text>
|
||||
<Text fz={{ base: 'xs', md: 'sm' }} fw={500} lh="sm">
|
||||
Proses pendaftaran selesai
|
||||
</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
</StepperCompleted>
|
||||
@@ -159,7 +173,7 @@ function PelayananPerizinanBerusaha() {
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Text fz="sm" ta="justify" c="dimmed" mt="md">
|
||||
<Text fz={{ base: 'xs', md: 'sm' }} ta="justify" c="black" lh="sm" mt="md">
|
||||
Catatan: Persyaratan dan prosedur dapat berubah sewaktu-waktu. Untuk informasi resmi terbaru, silakan kunjungi situs{' '}
|
||||
<a href="https://oss.go.id/" target="_blank" rel="noopener noreferrer">
|
||||
oss.go.id
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
|
||||
import colors from '@/con/colors';
|
||||
import { BackgroundImage, Box, Button, Center, Group, Pagination, SimpleGrid, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
|
||||
import { BackgroundImage, Box, Button, Center, Group, Pagination, SimpleGrid, Skeleton, Stack, Text, Title, Tooltip } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconFileDescription, IconInfoCircle } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
@@ -35,7 +35,7 @@ function PelayananSuratKeterangan({ search }: { search: string }) {
|
||||
<Center py="xl">
|
||||
<Stack align="center" gap="xs">
|
||||
<IconFileDescription size={40} stroke={1.5} color={colors["blue-button"]} />
|
||||
<Text c="dimmed" ta="center">
|
||||
<Text c="dimmed" ta="center" fz={{ base: 'sm', md: 'md' }} lh="sm">
|
||||
Tidak ada layanan surat keterangan yang ditemukan
|
||||
</Text>
|
||||
</Stack>
|
||||
@@ -48,9 +48,9 @@ function PelayananSuratKeterangan({ search }: { search: string }) {
|
||||
<Group justify="space-between" align="center" mb="md">
|
||||
<Group gap="xs">
|
||||
<IconFileDescription size={28} stroke={1.8} />
|
||||
<Text fz={{ base: "h4", md: "h2" }} fw={700}>
|
||||
<Title order={2} c="black">
|
||||
Layanan Surat Keterangan
|
||||
</Text>
|
||||
</Title>
|
||||
</Group>
|
||||
<Tooltip label="Pilih layanan surat keterangan sesuai kebutuhan Anda" withArrow>
|
||||
<IconInfoCircle size={22} stroke={1.8} />
|
||||
@@ -82,15 +82,15 @@ function PelayananSuratKeterangan({ search }: { search: string }) {
|
||||
style={{ borderRadius: 16 }}
|
||||
/>
|
||||
<Stack justify="space-between" h="100%" gap="md" p="lg" pos="relative">
|
||||
<Text
|
||||
<Title
|
||||
order={3}
|
||||
c="white"
|
||||
fw={600}
|
||||
fz="lg"
|
||||
ta="center"
|
||||
lineClamp={2}
|
||||
lh="sm"
|
||||
>
|
||||
{v.name}
|
||||
</Text>
|
||||
</Title>
|
||||
<Group justify="center">
|
||||
<Button
|
||||
size="md"
|
||||
@@ -128,4 +128,4 @@ function PelayananSuratKeterangan({ search }: { search: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default PelayananSuratKeterangan;
|
||||
export default PelayananSuratKeterangan;
|
||||
@@ -42,9 +42,10 @@ function PelayananTelunjukSaktiDesa() {
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Title order={2} mb="lg" fz={{ base: 22, md: 28 }} fw={700} style={{ lineHeight: 1.4 }}>
|
||||
Layanan Telunjuk Sakti Desa <br />
|
||||
<Text span c="dimmed" fz="lg" fw={400}>
|
||||
<Title order={2} mb="lg" fw={700} style={{ lineHeight: 1.3 }} ta="left">
|
||||
Layanan Telunjuk Sakti Desa
|
||||
<Text span c="black" fz={{ base: 'sm', md: 'md' }} fw={400} style={{ lineHeight: 1.5 }}>
|
||||
{' '}
|
||||
Terwujudnya sistem administrasi kependudukan terintegrasi berbasis elektronik, cerdas, dan aman
|
||||
</Text>
|
||||
</Title>
|
||||
@@ -53,7 +54,7 @@ function PelayananTelunjukSaktiDesa() {
|
||||
<Skeleton h={400} radius="lg" />
|
||||
) : data.length === 0 ? (
|
||||
<Card shadow="sm" radius="lg" withBorder>
|
||||
<Text c="dimmed" ta="center" py="xl">
|
||||
<Text c="black" ta="center" py="xl" fz={{ base: 'sm', md: 'md' }} lh={1.5}>
|
||||
Belum ada layanan tersedia untuk saat ini
|
||||
</Text>
|
||||
</Card>
|
||||
@@ -72,9 +73,9 @@ function PelayananTelunjukSaktiDesa() {
|
||||
}}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
<Text fw={700} fz="lg" lh={1.4}>
|
||||
<Title order={3} fw={700} lh={1.3}>
|
||||
{v.name}
|
||||
</Text>
|
||||
</Title>
|
||||
<Flex gap="xs" align="center">
|
||||
<IconExternalLink size={18} stroke={1.5} />
|
||||
<Text
|
||||
@@ -82,7 +83,7 @@ function PelayananTelunjukSaktiDesa() {
|
||||
href={v.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
fz="sm"
|
||||
fz={{ base: 'xs', md: 'sm' }}
|
||||
c="blue"
|
||||
td="underline"
|
||||
style={{ cursor: 'pointer' }}
|
||||
@@ -100,4 +101,4 @@ function PelayananTelunjukSaktiDesa() {
|
||||
);
|
||||
}
|
||||
|
||||
export default PelayananTelunjukSaktiDesa;
|
||||
export default PelayananTelunjukSaktiDesa;
|
||||
@@ -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;
|
||||
@@ -1,58 +1,94 @@
|
||||
'use client'
|
||||
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Container, Group, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { Box, Container, Group, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import BackButton from '../../../layanan/_com/BackButto';
|
||||
|
||||
import NewsReader from '@/app/darmasaba/_com/NewsReader';
|
||||
import BackButton from '../../../layanan/_com/BackButto';
|
||||
|
||||
function Page() {
|
||||
const detail = useProxy(stateDesaPengumuman.pengumuman.findUnique)
|
||||
|
||||
const params = useParams()
|
||||
|
||||
const detail = useProxy(stateDesaPengumuman.pengumuman.findUnique);
|
||||
const params = useParams();
|
||||
|
||||
useShallowEffect(() => {
|
||||
stateDesaPengumuman.pengumuman.findUnique.load(params?.id as string)
|
||||
}, [])
|
||||
|
||||
stateDesaPengumuman.pengumuman.findUnique.load(params?.id as string);
|
||||
}, []);
|
||||
|
||||
if (!detail.data) {
|
||||
return (
|
||||
<Box>
|
||||
<Skeleton h={400} />
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap="md">
|
||||
{/* Header */}
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
|
||||
<Container size="lg" px="md">
|
||||
<Group>
|
||||
<NewsReader />
|
||||
</Group>
|
||||
<Stack gap="xs" >
|
||||
<Group justify={"space-between"} align={"center"}>
|
||||
<Text fz={{ base: "2rem", md: "2rem" }} c={colors["blue-button"]} fw="bold" >
|
||||
{detail.data?.judul}
|
||||
|
||||
<Stack gap="xs">
|
||||
<Group justify="space-between" align="flex-start" wrap="wrap">
|
||||
<Title
|
||||
order={1}
|
||||
c={colors['blue-button']}
|
||||
fz={{ base: 28, md: 36 }}
|
||||
style={{
|
||||
wordBreak: 'break-word',
|
||||
flex: '1 1 auto',
|
||||
minWidth: 0
|
||||
}}
|
||||
>
|
||||
{detail.data?.judul}
|
||||
</Title>
|
||||
<Paper bg={colors['blue-button']} p={8} style={{ flexShrink: 0 }}>
|
||||
<Text c={colors['white-1']} fz={{ base: 'xs', md: 'sm' }} lh={1.2}>
|
||||
{detail.data?.CategoryPengumuman?.name}
|
||||
</Text>
|
||||
<Group justify='end'>
|
||||
<Paper bg={colors['blue-button']} p={5}>
|
||||
<Text c={colors['white-1']}>{detail.data?.CategoryPengumuman?.name}</Text>
|
||||
</Paper>
|
||||
</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" >
|
||||
</Paper>
|
||||
</Group>
|
||||
|
||||
<Paper
|
||||
bg={colors['white-1']}
|
||||
p="md"
|
||||
w="100%"
|
||||
mih={{ base: 200, md: 300 }}
|
||||
>
|
||||
<Text
|
||||
px="lg"
|
||||
id="news-content"
|
||||
fz={{ base: 14, md: 16 }}
|
||||
lh={{ base: 1.6, md: 1.6 }}
|
||||
style={{
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: 'normal',
|
||||
width: '100%'
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: detail.data?.content }}
|
||||
/>
|
||||
<Text
|
||||
px="lg"
|
||||
fz={{ base: 12, md: 14 }}
|
||||
c={colors['blue-button']}
|
||||
fw="bold"
|
||||
lh={{ base: 1.4, md: 1.4 }}
|
||||
mt="md"
|
||||
>
|
||||
{new Date(detail.data?.createdAt).toLocaleDateString('id-ID', {
|
||||
weekday: 'long',
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
year: 'numeric',
|
||||
})}
|
||||
</Text>
|
||||
</Paper>
|
||||
@@ -62,4 +98,4 @@ function Page() {
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
export default Page;
|
||||
@@ -2,14 +2,13 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Container, Group, Paper, Stack, Text } from '@mantine/core';
|
||||
import { Box, Container, Group, Paper, Stack, Text, Title } from '@mantine/core';
|
||||
import { IconCalendar } from '@tabler/icons-react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import BackButton from '../../layanan/_com/BackButto';
|
||||
import { useEffect } from 'react';
|
||||
import { useParams } from 'next/navigation';
|
||||
|
||||
|
||||
function Page() {
|
||||
const unwrappedParams = useParams();
|
||||
const kategoriState = useProxy(stateDesaPengumuman);
|
||||
@@ -26,48 +25,85 @@ function Page() {
|
||||
<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">
|
||||
{categoryName.split('-').map(word =>
|
||||
|
||||
<Container size="lg" px="md">
|
||||
<Stack align="center" gap="xs">
|
||||
<Title
|
||||
order={1}
|
||||
c={colors["blue-button"]}
|
||||
ta="center"
|
||||
style={{ fontWeight: 'bold' }}
|
||||
>
|
||||
{categoryName.split('-').map(word =>
|
||||
word.charAt(0).toUpperCase() + word.slice(1)
|
||||
).join(' ')}
|
||||
</Text>
|
||||
<Text ta="center" px="md" pb={10}>
|
||||
</Title>
|
||||
<Text
|
||||
ta="center"
|
||||
px="md"
|
||||
pb="sm"
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={{ base: 1.5, md: 1.6 }}
|
||||
c="dimmed"
|
||||
>
|
||||
Informasi dan pengumuman resmi terkait {categoryName.split('-').join(' ')}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Container>
|
||||
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
{!kategoriState.pengumuman.findMany.data?.length ? (
|
||||
<Paper p="lg" radius="md" shadow="md" bg={colors["white-1"]}>
|
||||
Tidak ada pengumuman yang ditemukan
|
||||
<Text
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
ta="center"
|
||||
c="dimmed"
|
||||
>
|
||||
Tidak ada pengumuman yang ditemukan
|
||||
</Text>
|
||||
</Paper>
|
||||
) : kategoriState.pengumuman.findMany.data?.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}>
|
||||
) : (
|
||||
kategoriState.pengumuman.findMany.data?.map((v, k) => (
|
||||
<Paper
|
||||
mb="md"
|
||||
key={k}
|
||||
withBorder
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="md"
|
||||
bg={colors["white-1"]}
|
||||
>
|
||||
<Title order={3}>{v.judul}</Title>
|
||||
<Group style={{ color: 'black' }} pb="sm">
|
||||
<Group gap="xs">
|
||||
<IconCalendar size={18} />
|
||||
<Text size="sm">
|
||||
{v.createdAt ? new Date(v.createdAt).toLocaleDateString('id-ID', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
}) : 'No date available'}
|
||||
<Text
|
||||
fz={{ base: 'xs', md: 'sm' }}
|
||||
lh={{ base: 1.4, md: 1.5 }}
|
||||
>
|
||||
{v.createdAt
|
||||
? new Date(v.createdAt).toLocaleDateString('id-ID', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})
|
||||
: 'No date available'}
|
||||
</Text>
|
||||
</Group>
|
||||
</Group>
|
||||
<Text ta={'justify'}>
|
||||
<Text
|
||||
ta="justify"
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={{ base: 1.6, md: 1.7 }}
|
||||
>
|
||||
{v.deskripsi}
|
||||
</Text>
|
||||
</Paper>
|
||||
)
|
||||
})}
|
||||
))
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
export default Page;
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
Center,
|
||||
Container,
|
||||
Divider,
|
||||
Flex,
|
||||
Grid,
|
||||
GridCol,
|
||||
Group,
|
||||
@@ -22,7 +21,7 @@ import {
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
UnstyledButton,
|
||||
UnstyledButton
|
||||
} from '@mantine/core';
|
||||
import { IconCalendar, IconClock, IconSearch } from '@tabler/icons-react';
|
||||
import { useTransitionRouter } from 'next-view-transitions';
|
||||
@@ -98,10 +97,14 @@ function Page() {
|
||||
|
||||
<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">
|
||||
<Title
|
||||
order={1}
|
||||
c={colors['blue-button']}
|
||||
ta="center"
|
||||
>
|
||||
Pengumuman Desa Darmasaba
|
||||
</Text>
|
||||
<Text ta="center" px="md" pb={10}>
|
||||
</Title>
|
||||
<Text ta="center" px="md" pb={10} fz={{ base: 'sm', md: 'md' }} lh="sm">
|
||||
Informasi dan pengumuman resmi terkait kegiatan dan kebijakan Desa Darmasaba
|
||||
</Text>
|
||||
</Stack>
|
||||
@@ -126,17 +129,17 @@ function Page() {
|
||||
withCloseButton={false}
|
||||
title={item.CategoryPengumuman?.name || 'Pengumuman'}
|
||||
>
|
||||
<Stack gap={"xs"}>
|
||||
<Text fz="sm" fw="bold" c="black" style={{ textTransform: 'uppercase' }}>
|
||||
<Stack gap="xs">
|
||||
<Text fz={{ base: 'sm', md: 'sm' }} fw="bold" c="black" style={{ textTransform: 'uppercase' }}>
|
||||
{item.judul}
|
||||
</Text>
|
||||
<Text ta="justify" fz="sm" c="black" lineClamp={3} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||
<Text ta="justify" fz={{ base: 'xs', md: 'sm' }} c="black" lineClamp={3} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||
</Stack>
|
||||
<Flex pt={20} gap="md" justify="space-between">
|
||||
<Group pt={20} gap="md" justify="space-between">
|
||||
<Group style={{ color: 'black' }}>
|
||||
<Group gap="xs">
|
||||
<IconCalendar size={18} />
|
||||
<Text size="sm">
|
||||
<Text fz={{ base: 'xs', md: 'sm' }}>
|
||||
{new Date(item.createdAt).toLocaleDateString('id-ID', {
|
||||
weekday: 'long',
|
||||
day: 'numeric',
|
||||
@@ -147,7 +150,7 @@ function Page() {
|
||||
</Group>
|
||||
<Group gap="xs">
|
||||
<IconClock size={18} />
|
||||
<Text size="sm">
|
||||
<Text fz={{ base: 'xs', md: 'sm' }}>
|
||||
{new Date(item.createdAt).toLocaleTimeString('id-ID', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
@@ -157,11 +160,11 @@ function Page() {
|
||||
</Group>
|
||||
</Group>
|
||||
<Anchor variant="transparent" href={`/darmasaba/desa/pengumuman/${item.CategoryPengumuman?.name}/${item.id}`}>
|
||||
<Text fs="unset" c={colors['blue-button']} fz="sm">
|
||||
<Text fs="unset" c={colors['blue-button']} fz={{ base: 'xs', md: 'sm' }}>
|
||||
Baca Selengkapnya
|
||||
</Text>
|
||||
</Anchor>
|
||||
</Flex>
|
||||
</Group>
|
||||
</Notification>
|
||||
))
|
||||
)}
|
||||
@@ -169,19 +172,19 @@ function Page() {
|
||||
|
||||
<Paper p="md">
|
||||
<Stack gap="xs">
|
||||
<Text fw="bold" fz="lg" c={colors['blue-button']}>
|
||||
<Title order={3} c={colors['blue-button']}>
|
||||
Kategori
|
||||
</Text>
|
||||
</Title>
|
||||
{stateDesaPengumuman.category.findMany.data?.map((v: any, k) => {
|
||||
const count = v._count?.pengumumans || 0;
|
||||
return (
|
||||
<UnstyledButton component={Link} href={`/darmasaba/desa/pengumuman/${v.name}`} key={k}>
|
||||
<Paper bg={colors['BG-trans']} p={5}>
|
||||
<Group px={3} justify="space-between">
|
||||
<Text fz="md" c="black">
|
||||
<Text fz={{ base: 'sm', md: 'md' }} c="black">
|
||||
{v.name}
|
||||
</Text>
|
||||
<Text fz="md" c="black">
|
||||
<Text fz={{ base: 'sm', md: 'md' }} c="black">
|
||||
{count}
|
||||
</Text>
|
||||
</Group>
|
||||
@@ -200,7 +203,7 @@ function Page() {
|
||||
<Divider mb={10} color={colors['blue-button']} />
|
||||
<Grid>
|
||||
<GridCol span={{ base: 12, md: 8 }}>
|
||||
<Title order={3}>Daftar Pengumuman</Title>
|
||||
<Title order={2}>Daftar Pengumuman</Title>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 4 }}>
|
||||
<TextInput
|
||||
@@ -210,6 +213,7 @@ function Page() {
|
||||
w="100%"
|
||||
value={searchInput}
|
||||
onChange={(e) => setSearchInput(e.target.value)}
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
/>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
@@ -223,7 +227,9 @@ function Page() {
|
||||
</SimpleGrid>
|
||||
) : !state.findMany.data?.length ? (
|
||||
<Notification withCloseButton={false} h={100}>
|
||||
Tidak ada pengumuman yang ditemukan
|
||||
<Text fz={{ base: 'sm', md: 'md' }} ta="center">
|
||||
Tidak ada pengumuman yang ditemukan
|
||||
</Text>
|
||||
</Notification>
|
||||
) : (
|
||||
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg" verticalSpacing="lg">
|
||||
@@ -231,26 +237,26 @@ function Page() {
|
||||
<Paper key={item.id} p="md" withBorder radius="md" h="100%">
|
||||
<Stack h="100%" justify="space-between">
|
||||
<div>
|
||||
<Text fw={600} c={colors['blue-button']} mb={5}>
|
||||
<Text fw={600} c={colors['blue-button']} mb={5} fz={{ base: 'sm', md: 'md' }}>
|
||||
{item.CategoryPengumuman?.name || 'Pengumuman'}
|
||||
</Text>
|
||||
<Text fz="lg" fw={700} mb="sm" lineClamp={2} style={{ textTransform: 'uppercase' }}>
|
||||
<Text fw={700} mb="sm" lineClamp={2} style={{ textTransform: 'uppercase' }} fz={{ base: 'sm', md: 'lg' }}>
|
||||
{item.judul}
|
||||
</Text>
|
||||
<Text
|
||||
fz="sm"
|
||||
c="dimmed"
|
||||
lineClamp={4}
|
||||
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
|
||||
mb="md"
|
||||
style={{wordBreak: "break-word", whiteSpace: "normal"}}
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
fz={{ base: 'xs', md: 'sm' }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Group mb="sm" c="dimmed">
|
||||
<Group gap={5}>
|
||||
<IconCalendar size={16} />
|
||||
<Text size="xs">
|
||||
<Text fz={{ base: 'xs', md: 'xs' }}>
|
||||
{new Date(item.createdAt).toLocaleDateString('id-ID', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
@@ -260,19 +266,19 @@ function Page() {
|
||||
</Group>
|
||||
<Group gap={5}>
|
||||
<IconClock size={16} />
|
||||
<Text size="xs">
|
||||
<Text fz={{ base: 'xs', md: 'xs' }}>
|
||||
{new Date(item.createdAt).toLocaleTimeString('id-ID', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})}
|
||||
</Text>
|
||||
</Group>
|
||||
<Anchor variant="transparent" href={`/darmasaba/desa/pengumuman/${item.CategoryPengumuman?.name}/${item.id}`}>
|
||||
<Text fw={600} c={colors['blue-button']} fz={{ base: 'sm', md: 'sm' }}>
|
||||
Baca Selengkapnya →
|
||||
</Text>
|
||||
</Anchor>
|
||||
</Group>
|
||||
<Anchor variant="transparent" href={`/darmasaba/desa/pengumuman/${item.CategoryPengumuman?.name}/${item.id}`}>
|
||||
<Text fw={600} c={colors['blue-button']} size="sm">
|
||||
Baca Selengkapnya →
|
||||
</Text>
|
||||
</Anchor>
|
||||
</div>
|
||||
</Stack>
|
||||
</Paper>
|
||||
@@ -289,6 +295,7 @@ function Page() {
|
||||
siblings={1}
|
||||
boundaries={1}
|
||||
withEdges
|
||||
fz={{ base: 'xs', md: 'sm' }}
|
||||
/>
|
||||
</Center>
|
||||
</Stack>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useEffect, useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import BackButton from '../../layanan/_com/BackButto';
|
||||
|
||||
|
||||
function Page() {
|
||||
const params = useParams<{ id: string }>();
|
||||
const id = Array.isArray(params.id) ? params.id[0] : params.id;
|
||||
@@ -35,7 +36,9 @@ function Page() {
|
||||
<Center h="80vh">
|
||||
<Stack align="center" gap="md">
|
||||
<Loader size="lg" color="blue" />
|
||||
<Text c="dimmed" fz="sm">Sedang memuat informasi...</Text>
|
||||
<Text c="dimmed" fz={{ base: 'xs', md: 'sm' }} ta="center">
|
||||
Sedang memuat informasi...
|
||||
</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
);
|
||||
@@ -46,28 +49,31 @@ function Page() {
|
||||
<Center h="80vh">
|
||||
<Stack align="center" gap="sm">
|
||||
<IconMoodSad size={64} stroke={1.5} color="var(--mantine-color-blue-6)" />
|
||||
<Title order={3}>Data Tidak Ditemukan</Title>
|
||||
<Text c="dimmed" fz="sm">Mohon periksa kembali atau coba beberapa saat lagi</Text>
|
||||
<Title order={3} ta="center">
|
||||
Data Tidak Ditemukan
|
||||
</Title>
|
||||
<Text c="dimmed" fz={{ base: 'xs', md: 'sm' }} ta="center">
|
||||
Mohon periksa kembali atau coba beberapa saat lagi
|
||||
</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap="xl" px={{ base: "md", md: 0 }}>
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap="xl" px={{ base: 'md', md: 0 }}>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
<Container w={{ base: "100%", md: "60%" }}>
|
||||
<Container w={{ base: '100%', md: '60%' }}>
|
||||
<Paper radius="2xl" shadow="lg" p="xl" withBorder>
|
||||
<Stack gap="lg" align="center">
|
||||
<Title ta="center" fz={{ base: "2rem", md: "3rem" }} c={colors["blue-button"]} fw={800}>
|
||||
<Title order={1} ta="center" c={colors['blue-button']} fw={800}>
|
||||
{state.findUnique.data?.name}
|
||||
</Title>
|
||||
<Text ta="center" fw={600} fz={{ base: "md", md: "lg" }} c="dimmed">
|
||||
<Text ta="center" fw={600} fz={{ base: 'md', md: 'lg' }} c="dimmed">
|
||||
Informasi & Pelayanan Potensi Desa Digital
|
||||
</Text>
|
||||
{/* ✅ Bagian gambar dibuat konsisten tanpa CSS manual */}
|
||||
<Box
|
||||
w="100%"
|
||||
h={{ base: 220, md: 400 }}
|
||||
@@ -87,7 +93,15 @@ function Page() {
|
||||
radius="lg"
|
||||
/>
|
||||
</Box>
|
||||
<Text py="md" fz={{ base: "sm", md: "md" }} ta="justify" lh={1.8} dangerouslySetInnerHTML={{ __html: state.findUnique.data?.deskripsi || 'Belum ada deskripsi untuk potensi desa ini.' }} />
|
||||
<Text
|
||||
py="md"
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
ta="justify"
|
||||
lh={{ base: 1.6, md: 1.8 }}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: state.findUnique.data?.content || 'Belum ada deskripsi untuk potensi desa ini.',
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Container>
|
||||
@@ -95,4 +109,4 @@ function Page() {
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
export default Page;
|
||||
@@ -2,7 +2,7 @@
|
||||
'use client'
|
||||
import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi';
|
||||
import colors from '@/con/colors';
|
||||
import { BackgroundImage, Box, Button, Center, Flex, Group, Paper, SimpleGrid, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { BackgroundImage, Box, Button, Flex, Group, Paper, SimpleGrid, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||
import { IconEye } from '@tabler/icons-react';
|
||||
import { useTransitionRouter } from 'next-view-transitions';
|
||||
import { useEffect, useState } from 'react';
|
||||
@@ -41,10 +41,10 @@ function Page() {
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Flex justify="space-between" align="center" direction={{ base: "column", md: "row" }} gap="lg">
|
||||
<Stack gap="sm" maw={600}>
|
||||
<Text fz={{ base: "2rem", md: "3rem" }} fw={900} c={colors["blue-button"]} lh={1.2}>
|
||||
<Title order={1} fz={{ base: 28, md: 36 }} lh={1.2} c={colors["blue-button"]}>
|
||||
Potensi Desa Darmasaba
|
||||
</Text>
|
||||
<Text fz="lg" ta="justify">
|
||||
</Title>
|
||||
<Text fz={{ base: 14, md: 16 }} lh={1.6} ta="justify">
|
||||
Temukan berbagai potensi unggulan, peluang, dan daya tarik yang menjadikan Desa Darmasaba istimewa.
|
||||
</Text>
|
||||
</Stack>
|
||||
@@ -58,18 +58,18 @@ function Page() {
|
||||
>
|
||||
<Flex justify="center" align="center" gap="xl">
|
||||
<Box>
|
||||
<Text ta="center" fz="2rem" fw={800} c="white">
|
||||
<Text ta="center" fz={{ base: 20, md: 32 }} fw={800} c="white" lh={1.2}>
|
||||
{data?.filter(item => item.kategori?.nama.toLowerCase() !== 'wisata').length || 0}
|
||||
</Text>
|
||||
<Text ta="center" fz="sm" c="white" fw={500}>
|
||||
<Text ta="center" fz={{ base: 12, md: 14 }} c="white" fw={500}>
|
||||
Potensi
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text ta="center" fz="2rem" fw={800} c="white">
|
||||
<Text ta="center" fz={{ base: 20, md: 32 }} fw={800} c="white" lh={1.2}>
|
||||
{data?.filter(item => item.kategori?.nama.toLowerCase() === 'wisata').length || 0}
|
||||
</Text>
|
||||
<Text ta="center" fz="sm" c="white" fw={500}>
|
||||
<Text ta="center" fz={{ base: 12, md: 14 }} c="white" fw={500}>
|
||||
Wisata
|
||||
</Text>
|
||||
</Box>
|
||||
@@ -91,45 +91,40 @@ function Page() {
|
||||
radius="xl"
|
||||
onMouseEnter={() => setHoveredId(v.id)}
|
||||
onMouseLeave={() => setHoveredId(null)}
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
cursor: 'pointer',
|
||||
transition: 'transform 0.3s ease'
|
||||
}}
|
||||
>
|
||||
{/* Overlay with smooth transition */}
|
||||
<Box
|
||||
pos="absolute"
|
||||
inset={0}
|
||||
bg={hoveredId === v.id
|
||||
? "linear-gradient(180deg, rgba(0,0,0,0.4) 0%, rgba(0,0,0,0.75) 100%)"
|
||||
bg={hoveredId === v.id
|
||||
? "linear-gradient(180deg, rgba(0,0,0,0.4) 0%, rgba(0,0,0,0.75) 100%)"
|
||||
: "linear-gradient(180deg, rgba(0,0,0,0.1) 0%, rgba(0,0,0,0.15) 100%)"
|
||||
}
|
||||
style={{
|
||||
transition: 'background 0.3s ease'
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
<Stack justify="space-between" h="100%" gap="md" p="lg" pos="relative">
|
||||
{/* Kategori badge - always visible */}
|
||||
<Group>
|
||||
<Paper
|
||||
radius="lg"
|
||||
py={6}
|
||||
px={12}
|
||||
shadow="md"
|
||||
withBorder
|
||||
<Paper
|
||||
radius="lg"
|
||||
py={6}
|
||||
px={12}
|
||||
shadow="md"
|
||||
withBorder
|
||||
bg="rgba(255,255,255,0.9)"
|
||||
style={{
|
||||
transition: 'all 0.3s ease'
|
||||
}}
|
||||
style={{ transition: 'all 0.3s ease' }}
|
||||
>
|
||||
<Text fz="sm" fw={600}>{v.kategori?.nama}</Text>
|
||||
<Text fz={{ base: 11, md: 14 }} fw={600}>{v.kategori?.nama}</Text>
|
||||
</Paper>
|
||||
</Group>
|
||||
|
||||
{/* Nama potensi - visible on hover */}
|
||||
<Box
|
||||
style={{
|
||||
opacity: hoveredId === v.id ? 1 : 0,
|
||||
@@ -138,20 +133,20 @@ function Page() {
|
||||
pointerEvents: hoveredId === v.id ? 'auto' : 'none'
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
<Title
|
||||
order={3}
|
||||
fw={800}
|
||||
c="white"
|
||||
fz="xl"
|
||||
fz={{ base: 18, md: 20 }}
|
||||
ta="center"
|
||||
lineClamp={2}
|
||||
lh={1.3}
|
||||
>
|
||||
{v.name}
|
||||
</Text>
|
||||
</Title>
|
||||
</Box>
|
||||
|
||||
{/* Button - visible on hover */}
|
||||
<Group
|
||||
<Group
|
||||
justify="center"
|
||||
style={{
|
||||
opacity: hoveredId === v.id ? 1 : 0,
|
||||
@@ -169,23 +164,21 @@ function Page() {
|
||||
gradient={{ from: colors["blue-button"], to: "#4dabf7", deg: 45 }}
|
||||
onClick={() => router.push(`/darmasaba/desa/potensi/${v.id}`)}
|
||||
>
|
||||
Lihat Detail
|
||||
<Text c={'white'} fz={{ base: 12, md: 14 }} fw={500}>Lihat Detail</Text>
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</BackgroundImage>
|
||||
))
|
||||
) : (
|
||||
<Center h={240}>
|
||||
<Stack align="center" gap="xs">
|
||||
<Text fz="lg" fw={600} c="dimmed">
|
||||
Belum ada potensi desa
|
||||
</Text>
|
||||
<Text fz="sm" c="dimmed">
|
||||
Data potensi akan tampil di sini setelah tersedia.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
<Stack align="center" gap="xs">
|
||||
<Text fz={{ base: 14, md: 16 }} fw={600} c="dimmed">
|
||||
Belum ada potensi desa
|
||||
</Text>
|
||||
<Text fz={{ base: 12, md: 14 }} c="dimmed">
|
||||
Data potensi akan tampil di sini setelah tersedia.
|
||||
</Text>
|
||||
</Stack>
|
||||
)}
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
@@ -193,4 +186,4 @@ function Page() {
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
export default Page;
|
||||
|
||||
@@ -26,7 +26,6 @@ function DetailPegawaiUser() {
|
||||
statePegawai.findUnique.load(params?.id as string);
|
||||
}, []);
|
||||
|
||||
|
||||
if (!statePegawai.findUnique.data) {
|
||||
return (
|
||||
<Stack py="lg">
|
||||
@@ -41,7 +40,7 @@ function DetailPegawaiUser() {
|
||||
<Box px={{ base: 'md', md: 100 }} py="xl">
|
||||
{/* Back button */}
|
||||
<Group mb="lg" px={{ base: 'md', md: 100 }}>
|
||||
<BackButton/>
|
||||
<BackButton />
|
||||
</Group>
|
||||
|
||||
<Paper
|
||||
@@ -69,11 +68,17 @@ function DetailPegawaiUser() {
|
||||
/>
|
||||
|
||||
{/* Nama & Jabatan */}
|
||||
<Stack align="center" gap={2}>
|
||||
<Title order={3} fw={700} c={colors['blue-button']}>
|
||||
<Stack align="center" gap={4}>
|
||||
{/* Title utama → H2 karena ini judul profil */}
|
||||
<Title order={2} c={colors['blue-button']} lh={1.2}>
|
||||
{data.namaLengkap || '-'} {data.gelarAkademik || ''}
|
||||
</Title>
|
||||
<Text fz="sm" c="dimmed">
|
||||
|
||||
<Text
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={1.4}
|
||||
c="dimmed"
|
||||
>
|
||||
{data.posisi?.nama || 'Posisi tidak tersedia'}
|
||||
</Text>
|
||||
</Stack>
|
||||
@@ -82,7 +87,11 @@ function DetailPegawaiUser() {
|
||||
<Divider my="lg" />
|
||||
|
||||
{/* Informasi Detail */}
|
||||
<Stack gap="md">
|
||||
<Stack gap="lg">
|
||||
<Title order={3} lh={1.3}>
|
||||
Informasi Pegawai
|
||||
</Title>
|
||||
|
||||
<InfoRow label="Email" value={data.email} />
|
||||
<InfoRow label="Telepon" value={data.telepon} />
|
||||
<InfoRow label="Alamat" value={data.alamat} multiline />
|
||||
@@ -91,10 +100,10 @@ function DetailPegawaiUser() {
|
||||
value={
|
||||
data.tanggalMasuk
|
||||
? new Date(data.tanggalMasuk).toLocaleDateString('id-ID', {
|
||||
day: '2-digit',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})
|
||||
day: '2-digit',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})
|
||||
: '-'
|
||||
}
|
||||
/>
|
||||
@@ -123,11 +132,18 @@ function InfoRow({
|
||||
}) {
|
||||
return (
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} c="dark">
|
||||
<Text
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
fw={600}
|
||||
lh={1.3}
|
||||
c="dark"
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
fz="sm"
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={1.5}
|
||||
c={valueColor || 'dimmed'}
|
||||
style={{
|
||||
whiteSpace: multiline ? 'normal' : 'nowrap',
|
||||
@@ -14,6 +14,9 @@ import {
|
||||
Loader,
|
||||
Paper,
|
||||
Stack,
|
||||
Tabs,
|
||||
TabsList,
|
||||
TabsTab,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
@@ -33,10 +36,12 @@ import { useTransitionRouter } from 'next-view-transitions'
|
||||
import { OrganizationChart } from 'primereact/organizationchart'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useProxy } from 'valtio/utils'
|
||||
|
||||
import './struktur.css'
|
||||
import { useMediaQuery } from '@mantine/hooks'
|
||||
import BackButton from '../_com/BackButto'
|
||||
|
||||
export default function StrukturPerangkatDesa() {
|
||||
export default function Page() {
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
@@ -55,10 +60,11 @@ export default function StrukturPerangkatDesa() {
|
||||
ta="center"
|
||||
c={colors['blue-button']}
|
||||
fz={{ base: 28, md: 36, lg: 44 }}
|
||||
lh={{ base: 1.05, md: 1.03 }}
|
||||
>
|
||||
Struktur Perangkat Desa
|
||||
</Title>
|
||||
<Text ta="center" c="black" maw={800}>
|
||||
<Text ta="center" c="black" maw={800} fz={{ base: 13, md: 15 }} lh={1.45}>
|
||||
Gambaran visual peran dan pegawai yang ditugaskan. Arahkan kursor
|
||||
untuk melihat detail atau klik node untuk fokus tampilan.
|
||||
</Text>
|
||||
@@ -101,8 +107,8 @@ function StrukturPerangkatDesaNode() {
|
||||
<Center py={48}>
|
||||
<Stack align="center" gap="sm">
|
||||
<Loader size="lg" />
|
||||
<Text fw={600}>Memuat struktur organisasi…</Text>
|
||||
<Text c="dimmed" size="sm">
|
||||
<Text fw={600} fz={{ base: 15, md: 16 }} lh={1.2}>Memuat struktur organisasi…</Text>
|
||||
<Text c="dimmed" fz={{ base: 12, md: 13 }} lh={1.4}>
|
||||
Mengambil data pegawai dan posisi. Mohon tunggu sebentar.
|
||||
</Text>
|
||||
</Stack>
|
||||
@@ -128,10 +134,10 @@ function StrukturPerangkatDesaNode() {
|
||||
<Center>
|
||||
<IconUsers size={56} />
|
||||
</Center>
|
||||
<Title order={3} mt="md">
|
||||
<Title order={3} mt="md" fz={{ base: 16, md: 18 }} lh={1.15}>
|
||||
Data pegawai belum tersedia
|
||||
</Title>
|
||||
<Text c="dimmed" mt="xs">
|
||||
<Text c="dimmed" mt="xs" fz={{ base: 13, md: 14 }} lh={1.4}>
|
||||
Belum ada data pegawai yang tercatat untuk PPID.
|
||||
</Text>
|
||||
<Group justify="center" mt="lg">
|
||||
@@ -228,90 +234,137 @@ function StrukturPerangkatDesaNode() {
|
||||
{/* 🔍 Controls */}
|
||||
<Paper
|
||||
shadow="xs"
|
||||
w={{
|
||||
base: '100%', // Mobile: 100%
|
||||
sm: '40%', // Tablet: 95%
|
||||
md: '39%', // Desktop: 70%
|
||||
lg: '38%', // Desktop L: 60%
|
||||
xl: '37%', // 4K: 50%
|
||||
'2xl': '36%', // Ultra-wide: 45%
|
||||
}}
|
||||
p="md"
|
||||
radius="md"
|
||||
style={{
|
||||
background: colors['blue-button']
|
||||
background: colors['blue-button'], // ⬅️ 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,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Group gap="xs">
|
||||
<Button
|
||||
variant="light"
|
||||
bg={colors['blue-button-2']}
|
||||
size="sm"
|
||||
onClick={handleZoomOut}
|
||||
leftSection={<IconZoomOut size={16} />}
|
||||
c={colors['blue-button']}
|
||||
>
|
||||
Zoom Out
|
||||
</Button>
|
||||
|
||||
<Box
|
||||
bg={colors['blue-button-2']}
|
||||
c={colors['blue-button']}
|
||||
px={16}
|
||||
py={8}
|
||||
style={{ width: '100%' }} // 👈 penting
|
||||
>
|
||||
<TabsList
|
||||
style={{
|
||||
fontSize: 14,
|
||||
fontWeight: 700,
|
||||
borderRadius: '8px',
|
||||
minWidth: 70,
|
||||
textAlign: 'center',
|
||||
display: 'flex',
|
||||
overflowX: 'auto',
|
||||
overflowY: 'hidden',
|
||||
gap: '4px',
|
||||
paddingBottom: '4px',
|
||||
flexWrap: 'nowrap',
|
||||
WebkitOverflowScrolling: 'touch',
|
||||
scrollbarWidth: 'thin',
|
||||
msOverflowStyle: '-ms-autohiding-scrollbar',
|
||||
maxWidth: '100%',
|
||||
scrollBehavior: 'smooth', // 👈 smooth scroll
|
||||
}}
|
||||
>
|
||||
{Math.round(scale * 100)}%
|
||||
</Box>
|
||||
<TabsTab
|
||||
value="zoom-out"
|
||||
onClick={handleZoomOut}
|
||||
leftSection={<IconZoomOut size={16} />}
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
<Text fz={{ base: 12, sm: 13 }} lh={1} ta="center">Zoom Out</Text>
|
||||
</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={{
|
||||
fontWeight: 700,
|
||||
borderRadius: '6px',
|
||||
minWidth: 60,
|
||||
textAlign: 'center',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
<Text fz={{ base: 12, sm: 13 }} lh={1} c={colors['blue-button']}>
|
||||
{Math.round(scale * 100)}%
|
||||
</Text>
|
||||
</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 }}
|
||||
>
|
||||
<Text fz={{ base: 12, sm: 13 }} lh={1} ta="center">Zoom In</Text>
|
||||
</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 }}
|
||||
>
|
||||
<Text fz={{ base: 12, sm: 13 }} lh={1} ta="center">Reset</Text>
|
||||
</TabsTab>
|
||||
|
||||
<TabsTab
|
||||
value="fullscreen"
|
||||
onClick={toggleFullscreen}
|
||||
leftSection={
|
||||
isFullscreen ? (
|
||||
<IconArrowsMinimize size={16} />
|
||||
) : (
|
||||
<IconArrowsMaximize size={16} />
|
||||
)
|
||||
}
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
<Text fz={{ base: 12, sm: 13 }} lh={1} ta="center">
|
||||
{isFullscreen ? 'Exit' : 'Fullscreen'}
|
||||
</Text>
|
||||
</TabsTab>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* 🧩 Chart Container */}
|
||||
@@ -325,15 +378,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 +403,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 +414,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,
|
||||
@@ -406,17 +466,17 @@ function NodeCard({ node, router }: any) {
|
||||
{/* Name */}
|
||||
<Text
|
||||
fw={700}
|
||||
size="sm"
|
||||
ta="center"
|
||||
c={colors['blue-button']}
|
||||
lineClamp={2}
|
||||
fz={{ base: 13, md: 15 }}
|
||||
lh={1.2}
|
||||
style={{
|
||||
minHeight: 40,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
wordBreak: 'break-word',
|
||||
lineHeight: 1.3,
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
@@ -424,18 +484,18 @@ function NodeCard({ node, router }: any) {
|
||||
|
||||
{/* Title/Position */}
|
||||
<Text
|
||||
size="xs"
|
||||
c="dimmed"
|
||||
ta="center"
|
||||
fw={500}
|
||||
lineClamp={2}
|
||||
fz={{ base: 12, md: 13 }}
|
||||
lh={1.3}
|
||||
style={{
|
||||
minHeight: 32,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
wordBreak: 'break-word',
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
@@ -451,14 +511,14 @@ function NodeCard({ node, router }: any) {
|
||||
mt={8}
|
||||
radius="md"
|
||||
onClick={() =>
|
||||
router.push(`/darmasaba/desa/profile/struktur-perangkat-desa/${node.data.id}`)
|
||||
router.push(`/darmasaba/desa/profil/struktur-perangkat-desa/${node.data.id}`)
|
||||
}
|
||||
style={{
|
||||
height: 32,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
Lihat Detail
|
||||
<Text fz={{ base: 12, md: 13 }} lh={1} ta="center">Lihat Detail</Text>
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
@@ -2,7 +2,7 @@
|
||||
'use client'
|
||||
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'
|
||||
import colors from '@/con/colors'
|
||||
import { Box, Center, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'
|
||||
import { Box, Center, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core'
|
||||
import { useEffect } from 'react'
|
||||
import { useProxy } from 'valtio/utils'
|
||||
|
||||
@@ -26,6 +26,8 @@ function LambangDesa() {
|
||||
return (
|
||||
<Box>
|
||||
<Stack align="center" gap="lg">
|
||||
|
||||
{/* HEADER */}
|
||||
<Box pb="lg">
|
||||
<Center>
|
||||
<Image
|
||||
@@ -36,17 +38,20 @@ function LambangDesa() {
|
||||
loading="lazy"
|
||||
/>
|
||||
</Center>
|
||||
<Text
|
||||
|
||||
{/* TITLE - H1 */}
|
||||
<Title
|
||||
order={1}
|
||||
c={colors['blue-button']}
|
||||
ta="center"
|
||||
fw={800}
|
||||
fz={{ base: 28, md: 40 }}
|
||||
mt="sm"
|
||||
style={{ letterSpacing: '-0.5px' }}
|
||||
>
|
||||
Lambang Desa
|
||||
</Text>
|
||||
</Title>
|
||||
</Box>
|
||||
|
||||
{/* DESKRIPSI */}
|
||||
<Paper
|
||||
p="xl"
|
||||
radius="xl"
|
||||
@@ -58,15 +63,20 @@ function LambangDesa() {
|
||||
borderColor: '#e0e9ff',
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
fz={{ base: '1.125rem', md: '1.375rem' }}
|
||||
lh={1.8}
|
||||
c="dark"
|
||||
ta="justify"
|
||||
style={{ fontWeight: 400, wordBreak: "break-word", whiteSpace: "normal", }}
|
||||
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
|
||||
/>
|
||||
<Text
|
||||
fz={{ base: 'sm', md: 'md' }} // Body text mobile & desktop
|
||||
lh={1.7}
|
||||
c="dark"
|
||||
ta="justify"
|
||||
style={{
|
||||
fontWeight: 400,
|
||||
wordBreak: "break-word",
|
||||
whiteSpace: "normal",
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
|
||||
/>
|
||||
</Paper>
|
||||
|
||||
</Stack>
|
||||
</Box>
|
||||
)
|
||||
@@ -2,7 +2,7 @@
|
||||
'use client'
|
||||
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Card, Center, Group, Image, Loader, Paper, Stack, Text } from '@mantine/core';
|
||||
import { Box, Card, Center, Group, Image, Loader, Paper, Stack, Text, Title } from '@mantine/core';
|
||||
import { IconPhoto } from '@tabler/icons-react';
|
||||
import { useEffect } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
@@ -21,7 +21,9 @@ function MaskotDesa() {
|
||||
<Center mih={500}>
|
||||
<Stack align="center" gap="sm">
|
||||
<Loader size="lg" color="blue" />
|
||||
<Text c="dimmed" fz="sm">Sedang memuat data maskot desa...</Text>
|
||||
<Text c="dimmed" fz={{ base: 'xs', md: 'sm' }}>
|
||||
Sedang memuat data maskot desa...
|
||||
</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
);
|
||||
@@ -31,8 +33,21 @@ function MaskotDesa() {
|
||||
<Box>
|
||||
<Stack align="center" gap="xl">
|
||||
<Stack align="center" gap={10}>
|
||||
<Image src="/pudak-icon.png" alt="Ikon Desa" w={{ base: 160, md: 240 }} loading="lazy"/>
|
||||
<Text c={colors['blue-button']} ta="center" fw={700} fz={{ base: 28, md: 36 }}>Maskot Desa</Text>
|
||||
<Image
|
||||
src="/pudak-icon.png"
|
||||
alt="Ikon Desa"
|
||||
w={{ base: 160, md: 240 }}
|
||||
loading="lazy"
|
||||
/>
|
||||
|
||||
{/* Page Title */}
|
||||
<Title
|
||||
order={1}
|
||||
ta="center"
|
||||
c={colors['blue-button']}
|
||||
>
|
||||
Maskot Desa
|
||||
</Title>
|
||||
</Stack>
|
||||
|
||||
<Paper
|
||||
@@ -42,48 +57,60 @@ function MaskotDesa() {
|
||||
withBorder
|
||||
style={{ background: 'linear-gradient(145deg, #ffffff, #f8f9fa)' }}
|
||||
>
|
||||
{/* Body Description */}
|
||||
<Text
|
||||
fz={{ base: 'sm', md: 'lg' }}
|
||||
lh={1.7}
|
||||
ta="justify"
|
||||
c="dark"
|
||||
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
|
||||
style={{wordBreak: "break-word", whiteSpace: "normal"}}
|
||||
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
|
||||
/>
|
||||
|
||||
<Group justify="center" gap="lg" mt="lg">
|
||||
{data.images.length > 0 ? (
|
||||
data.images.map((img, index) => (
|
||||
<Card
|
||||
<Card
|
||||
key={index}
|
||||
radius="lg"
|
||||
shadow="md"
|
||||
withBorder
|
||||
w={220}
|
||||
p="sm"
|
||||
style={{
|
||||
transition: 'transform 200ms ease, box-shadow 200ms ease',
|
||||
}}
|
||||
className="hover:scale-105 hover:shadow-lg"
|
||||
radius="lg"
|
||||
shadow="md"
|
||||
withBorder
|
||||
w={220}
|
||||
p="sm"
|
||||
style={{
|
||||
transition: 'transform 200ms ease, box-shadow 200ms ease',
|
||||
}}
|
||||
className="hover:scale-105 hover:shadow-lg"
|
||||
>
|
||||
<Image
|
||||
src={img.image.link}
|
||||
alt={img.label}
|
||||
w="100%"
|
||||
h={200}
|
||||
fit="cover"
|
||||
radius="md"
|
||||
loading="lazy"
|
||||
/>
|
||||
|
||||
{/* Image Label */}
|
||||
<Text
|
||||
ta="center"
|
||||
mt="sm"
|
||||
fw={600}
|
||||
fz={{ base: 'xs', md: 'sm' }}
|
||||
c="dark"
|
||||
>
|
||||
<Image
|
||||
src={img.image.link}
|
||||
alt={img.label}
|
||||
w="100%"
|
||||
h={200}
|
||||
fit="cover"
|
||||
radius="md"
|
||||
loading="lazy"
|
||||
/>
|
||||
<Text ta="center" mt="sm" fw={600} fz="sm" c="dark">
|
||||
{img.label}
|
||||
</Text>
|
||||
</Card>
|
||||
{img.label}
|
||||
</Text>
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
<Stack align="center" gap="xs" mt="lg">
|
||||
<IconPhoto size={48} stroke={1.5} color="gray" />
|
||||
<Text c="dimmed" fz="sm">Belum ada gambar maskot yang ditambahkan</Text>
|
||||
|
||||
<Text c="dimmed" fz={{ base: 'xs', md: 'sm' }}>
|
||||
Belum ada gambar maskot yang ditambahkan
|
||||
</Text>
|
||||
</Stack>
|
||||
)}
|
||||
</Group>
|
||||
@@ -1,35 +1,15 @@
|
||||
'use client'
|
||||
import { ActionIcon, Box, Flex, Paper, SimpleGrid, Stack, Text } from '@mantine/core';
|
||||
import { ActionIcon, Box, Flex, Paper, SimpleGrid, Stack, Text, Title } from '@mantine/core';
|
||||
import { motion } from 'framer-motion';
|
||||
import { IconSparkles } from '@tabler/icons-react';
|
||||
import colors from '@/con/colors';
|
||||
|
||||
const dataText = [
|
||||
{
|
||||
id: 1,
|
||||
title: "Santun",
|
||||
description: "Pelayanan ramah, penuh empati, sopan, dan beretika."
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Adaptif",
|
||||
description: "Cepat menyesuaikan diri terhadap perubahan dan selalu proaktif."
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Inovatif",
|
||||
description: "Berani menciptakan pembaruan dan ide-ide kreatif."
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "Profesional",
|
||||
description: "Berpengetahuan luas, terampil, dan bertanggung jawab."
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: "Gesit",
|
||||
description: "Cekatan, sigap, dan penuh inisiatif dalam bekerja."
|
||||
},
|
||||
{ id: 1, title: "Santun", description: "Pelayanan ramah, penuh empati, sopan, dan beretika." },
|
||||
{ id: 2, title: "Adaptif", description: "Cepat menyesuaikan diri terhadap perubahan dan selalu proaktif." },
|
||||
{ id: 3, title: "Inovatif", description: "Berani menciptakan pembaruan dan ide-ide kreatif." },
|
||||
{ id: 4, title: "Profesional", description: "Berpengetahuan luas, terampil, dan bertanggung jawab." },
|
||||
{ id: 5, title: "Gesit", description: "Cekatan, sigap, dan penuh inisiatif dalam bekerja." },
|
||||
];
|
||||
|
||||
const letters = ["S", "I", "G", "A", "P"];
|
||||
@@ -38,11 +18,14 @@ function MotoDesa() {
|
||||
return (
|
||||
<Box px={{ base: "md", md: "xl" }}>
|
||||
<Stack align="center" gap="lg">
|
||||
{/* Page Title */}
|
||||
<Box>
|
||||
<Text
|
||||
<Title
|
||||
order={1}
|
||||
ta="center"
|
||||
fw={800}
|
||||
fz={{ base: "2rem", md: "2.8rem" }}
|
||||
fz={{ base: 28, md: 36 }}
|
||||
lh={{ base: 1.2, md: 1.3 }}
|
||||
style={{
|
||||
background: "linear-gradient(90deg, #0D5594FF, #094678FF)",
|
||||
WebkitBackgroundClip: "text",
|
||||
@@ -50,9 +33,10 @@ function MotoDesa() {
|
||||
}}
|
||||
>
|
||||
Moto Desa Darmasaba
|
||||
</Text>
|
||||
</Title>
|
||||
</Box>
|
||||
|
||||
{/* Letter Icons */}
|
||||
<Flex gap={30} pb={40} pt={10} wrap="wrap" justify="center">
|
||||
{letters.map((letter, i) => (
|
||||
<motion.div
|
||||
@@ -71,7 +55,7 @@ function MotoDesa() {
|
||||
backdropFilter: "blur(6px)",
|
||||
}}
|
||||
>
|
||||
<Text c="white" fw={800} fz="xl">
|
||||
<Text c="white" fw={800} fz={{ base: 20, md: 24 }}>
|
||||
{letter}
|
||||
</Text>
|
||||
</ActionIcon>
|
||||
@@ -79,6 +63,7 @@ function MotoDesa() {
|
||||
))}
|
||||
</Flex>
|
||||
|
||||
{/* Values Card */}
|
||||
<Paper
|
||||
radius="lg"
|
||||
p="xl"
|
||||
@@ -90,19 +75,22 @@ function MotoDesa() {
|
||||
>
|
||||
<SimpleGrid cols={{ base: 1, md: 2 }} spacing="xl">
|
||||
{dataText.map((v) => (
|
||||
<motion.div
|
||||
key={v.id}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<motion.div key={v.id} whileHover={{ scale: 1.02 }} transition={{ duration: 0.2 }}>
|
||||
<Stack gap={4}>
|
||||
{/* Section Title */}
|
||||
<Flex align="center" gap="sm">
|
||||
<IconSparkles size={20} color={colors['blue-button']} />
|
||||
<Text fw={700} fz={{ base: "lg", md: "xl" }} c={colors['blue-button']}>
|
||||
<Title
|
||||
order={3}
|
||||
fw={700}
|
||||
fz={{ base: 20, md: 24 }}
|
||||
c={colors['blue-button']}
|
||||
>
|
||||
{v.title}
|
||||
</Text>
|
||||
</Title>
|
||||
</Flex>
|
||||
<Text fz={{ base: "sm", md: "md" }} c="gray.7">
|
||||
{/* Body Text */}
|
||||
<Text fz={{ base: 14, md: 16 }} lh={{ base: 1.5, md: 1.6 }} c="gray.7">
|
||||
{v.description}
|
||||
</Text>
|
||||
</Stack>
|
||||
@@ -111,16 +99,15 @@ function MotoDesa() {
|
||||
</SimpleGrid>
|
||||
</Paper>
|
||||
|
||||
{/* Motto Description */}
|
||||
<Text
|
||||
ta="center"
|
||||
fw={700}
|
||||
fz={{ base: "md", md: "xl" }}
|
||||
fz={{ base: 15, md: 20 }}
|
||||
lh={{ base: 1.6, md: 1.8 }}
|
||||
c="blue.8"
|
||||
mt="md"
|
||||
style={{
|
||||
maxWidth: 720,
|
||||
lineHeight: 1.6,
|
||||
}}
|
||||
style={{ maxWidth: 720 }}
|
||||
>
|
||||
"Berkomitmen menghadirkan pelayanan terbaik dengan semangat{" "}
|
||||
<Text span fw={800} c="cyan.6">
|
||||
@@ -2,44 +2,45 @@
|
||||
'use client'
|
||||
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Divider, Image, Paper, SimpleGrid, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { Box, Divider, Image, Paper, SimpleGrid, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||
import { IconBriefcase, IconTargetArrow, IconUser, IconUsers } from '@tabler/icons-react';
|
||||
import { useEffect } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
function ProfilPerbekel() {
|
||||
const state = useProxy(stateProfileDesa.profilPerbekel)
|
||||
const state = useProxy(stateProfileDesa.profilPerbekel);
|
||||
|
||||
useEffect(() => {
|
||||
state.findUnique.load("edit")
|
||||
}, [])
|
||||
state.findUnique.load("edit");
|
||||
}, []);
|
||||
|
||||
const { data, loading } = state.findUnique
|
||||
const { data, loading } = state.findUnique;
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Box py={20} px="md">
|
||||
<Skeleton h={500} radius="lg" />
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box px="md">
|
||||
{/* ===== PAGE TITLE ===== */}
|
||||
<Stack align="center" gap={0} mb={40}>
|
||||
<Text
|
||||
<Title
|
||||
order={1}
|
||||
c={colors['blue-button']}
|
||||
ta="center"
|
||||
fw="bold"
|
||||
fz={{ base: "2rem", md: "2.8rem" }}
|
||||
style={{ letterSpacing: "0.5px" }}
|
||||
>
|
||||
Profil Perbekel
|
||||
</Text>
|
||||
</Title>
|
||||
<Divider w={120} size="sm" color={colors['blue-button']} mt={10} />
|
||||
</Stack>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, md: 2 }} spacing="xl" pb={50}>
|
||||
{/* ========== FOTO PERBEKEL ========== */}
|
||||
<Box>
|
||||
<Paper
|
||||
bg={colors['white-trans-1']}
|
||||
@@ -60,6 +61,8 @@ function ProfilPerbekel() {
|
||||
}}
|
||||
loading="lazy"
|
||||
/>
|
||||
|
||||
{/* ===== NAMA DAN JABATAN ===== */}
|
||||
<Paper
|
||||
bg={colors['blue-button']}
|
||||
px="lg"
|
||||
@@ -67,22 +70,23 @@ function ProfilPerbekel() {
|
||||
className="glass3"
|
||||
py={{ base: 20, md: 50 }}
|
||||
>
|
||||
<Text c={colors['white-1']} fz={{ base: "lg", md: "h3" }}>
|
||||
<Title order={3} c={colors['white-1']}>
|
||||
Perbekel Desa Darmasaba
|
||||
</Text>
|
||||
<Text
|
||||
</Title>
|
||||
|
||||
<Title
|
||||
order={2}
|
||||
c={colors['white-1']}
|
||||
fw="bolder"
|
||||
fz={{ base: "xl", md: "h2" }}
|
||||
mt={8}
|
||||
>
|
||||
{"I.B. Surya Prabhawa Manuaba, S.H.,M.H.,NL.P."}
|
||||
</Text>
|
||||
</Title>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
{/* ========== BIODATA & PENGALAMAN ========== */}
|
||||
<Paper
|
||||
p="xl"
|
||||
bg={colors['white-trans-1']}
|
||||
@@ -92,34 +96,39 @@ function ProfilPerbekel() {
|
||||
withBorder
|
||||
>
|
||||
<Stack gap="xl">
|
||||
|
||||
{/* ===== BIODATA ===== */}
|
||||
<Box>
|
||||
<Stack gap={6}>
|
||||
<Stack align="center" gap={6}>
|
||||
<IconUser size={22} />
|
||||
<Text fz={{ base: "1.2rem", md: "1.5rem" }} fw="bold">Biodata</Text>
|
||||
<Title order={3}>Biodata</Title>
|
||||
</Stack>
|
||||
|
||||
<Text
|
||||
fz={{ base: "1rem", md: "1.2rem" }}
|
||||
fz={{ base: "sm", md: "md" }}
|
||||
ta="justify"
|
||||
lh={1.6}
|
||||
lh={1.7}
|
||||
dangerouslySetInnerHTML={{ __html: data.biodata }}
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
style={{ wordBreak: "break-word" }}
|
||||
/>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{/* ===== PENGALAMAN ===== */}
|
||||
<Box>
|
||||
<Stack gap={6}>
|
||||
<Stack align="center" gap={6}>
|
||||
<IconBriefcase size={22} />
|
||||
<Text fz={{ base: "1.2rem", md: "1.5rem" }} fw="bold">Pengalaman</Text>
|
||||
<Title order={3}>Pengalaman</Title>
|
||||
</Stack>
|
||||
|
||||
<Text
|
||||
fz={{ base: "1rem", md: "1.2rem" }}
|
||||
fz={{ base: "sm", md: "md" }}
|
||||
ta="left"
|
||||
lh={1.6}
|
||||
lh={1.7}
|
||||
dangerouslySetInnerHTML={{ __html: data.pengalaman }}
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
style={{ wordBreak: "break-word" }}
|
||||
/>
|
||||
</Stack>
|
||||
</Box>
|
||||
@@ -127,6 +136,7 @@ function ProfilPerbekel() {
|
||||
</Paper>
|
||||
</SimpleGrid>
|
||||
|
||||
{/* ========== ORGANISASI & PROGRAM UNGGULAN ========== */}
|
||||
<Paper
|
||||
p="xl"
|
||||
bg={colors['white-trans-1']}
|
||||
@@ -136,35 +146,41 @@ function ProfilPerbekel() {
|
||||
withBorder
|
||||
>
|
||||
<Stack gap="xl">
|
||||
|
||||
{/* ===== PENGALAMAN ORGANISASI ===== */}
|
||||
<Box>
|
||||
<Stack align="center" gap={6} >
|
||||
<Stack align="center" gap={6}>
|
||||
<IconUsers size={22} />
|
||||
<Text fz={{ base: "1.2rem", md: "1.5rem" }} fw="bold">Pengalaman Organisasi</Text>
|
||||
<Title order={3}>Pengalaman Organisasi</Title>
|
||||
</Stack>
|
||||
|
||||
<Text
|
||||
fz={{ base: "1rem", md: "1.2rem" }}
|
||||
fz={{ base: "sm", md: "md" }}
|
||||
ta="justify"
|
||||
lh={1.6}
|
||||
lh={1.7}
|
||||
dangerouslySetInnerHTML={{ __html: data.pengalamanOrganisasi }}
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
style={{ wordBreak: "break-word" }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* ===== PROGRAM UNGGULAN ===== */}
|
||||
<Box>
|
||||
<Stack align="center" gap={6} mb={6}>
|
||||
<IconTargetArrow size={22} />
|
||||
<Text fz={{ base: "1.2rem", md: "1.5rem" }} fw="bold">Program Kerja Unggulan</Text>
|
||||
<Title order={3}>Program Kerja Unggulan</Title>
|
||||
</Stack>
|
||||
|
||||
<Box px={10}>
|
||||
<Text
|
||||
fz={{ base: "1rem", md: "1.2rem" }}
|
||||
fz={{ base: "sm", md: "md" }}
|
||||
ta="justify"
|
||||
lh={1.6}
|
||||
lh={1.7}
|
||||
dangerouslySetInnerHTML={{ __html: data.programUnggulan }}
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
style={{ wordBreak: "break-word" }}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
@@ -2,7 +2,7 @@
|
||||
'use client'
|
||||
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Center, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { Box, Center, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||
import { useEffect } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
@@ -26,29 +26,32 @@ function SejarahDesa() {
|
||||
return (
|
||||
<Box>
|
||||
<Stack align="center" gap="xl">
|
||||
{/* HEADER ICON + TITLE */}
|
||||
<Stack align="center" gap="sm">
|
||||
<Center>
|
||||
<Image
|
||||
src="/darmasaba-icon.png"
|
||||
alt="Ikon Desa Darmasaba"
|
||||
w={{ base: 180, md: 260 }}
|
||||
w={{ base: 160, md: 240 }}
|
||||
radius="md"
|
||||
style={{ filter: 'drop-shadow(0 4px 12px rgba(0,0,0,0.15))' }}
|
||||
loading="lazy"
|
||||
/>
|
||||
</Center>
|
||||
|
||||
<Center>
|
||||
<Text
|
||||
<Title
|
||||
order={1}
|
||||
c={colors['blue-button']}
|
||||
ta="center"
|
||||
fw={700}
|
||||
fz={{ base: '2rem', md: '2.8rem' }}
|
||||
style={{ letterSpacing: '-0.5px' }}
|
||||
>
|
||||
Sejarah Desa
|
||||
</Text>
|
||||
</Title>
|
||||
</Center>
|
||||
</Stack>
|
||||
|
||||
{/* CONTENT */}
|
||||
<Paper
|
||||
p="xl"
|
||||
radius="lg"
|
||||
@@ -61,10 +64,14 @@ function SejarahDesa() {
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Text
|
||||
fz={{ base: 'md', md: 'lg' }}
|
||||
lh={1.8}
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={1.75}
|
||||
ta="justify"
|
||||
style={{ color: '#2a2a2a', wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
c="dark.7"
|
||||
style={{
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: 'normal',
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
|
||||
/>
|
||||
</Stack>
|
||||
@@ -28,8 +28,10 @@ function SemuaPerbekel() {
|
||||
<Center py="xl">
|
||||
<Stack align="center" gap="sm">
|
||||
<IconUser size={48} stroke={1.5} />
|
||||
<Title fw="bold" order={2}>Belum ada data Perbekel</Title>
|
||||
<Text c="dimmed" fz="sm" ta="center">Data mantan Perbekel akan muncul di sini ketika sudah tersedia</Text>
|
||||
<Title order={2} ta="center">Belum ada data Perbekel</Title>
|
||||
<Text c="dimmed" fz={{ base: 'xs', md: 'sm' }} lh={{ base: 1.4, md: 1.6 }} ta="center">
|
||||
Data mantan Perbekel akan muncul di sini ketika sudah tersedia
|
||||
</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
);
|
||||
@@ -38,17 +40,20 @@ function SemuaPerbekel() {
|
||||
return (
|
||||
<Box>
|
||||
<Stack align="center" gap="lg">
|
||||
<Box>
|
||||
<Text
|
||||
ta="center"
|
||||
fw={900}
|
||||
fz={{ base: "2rem", md: "2.5rem" }}
|
||||
variant="gradient"
|
||||
gradient={{ from: "blue", to: "cyan", deg: 45 }}
|
||||
>
|
||||
Perbekel Dari Masa ke Masa
|
||||
</Text>
|
||||
</Box>
|
||||
<Title
|
||||
order={1}
|
||||
ta="center"
|
||||
style={{
|
||||
background: 'linear-gradient(45deg, blue, cyan)',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
}}
|
||||
fz={{ base: 28, md: 36 }}
|
||||
lh={{ base: 1.2, md: 1.3 }}
|
||||
fw={900}
|
||||
>
|
||||
Perbekel Dari Masa ke Masa
|
||||
</Title>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="xl" w="100%">
|
||||
{data.map((v: any, k: number) => (
|
||||
@@ -59,9 +64,7 @@ function SemuaPerbekel() {
|
||||
withBorder
|
||||
p="lg"
|
||||
bg="white"
|
||||
style={{
|
||||
transition: "all 250ms ease",
|
||||
}}
|
||||
style={{ transition: "all 250ms ease" }}
|
||||
className="hover:shadow-xl hover:scale-[1.02]"
|
||||
>
|
||||
<Stack gap="md" align="center">
|
||||
@@ -77,17 +80,17 @@ function SemuaPerbekel() {
|
||||
</Box>
|
||||
|
||||
<Stack gap={4} align="center">
|
||||
<Text fw={700} fz="lg" ta="center">
|
||||
{v.nama}
|
||||
</Text>
|
||||
<Title order={3} fz={{ base: 18, md: 20 }} ta="center" fw={700}>
|
||||
{v.nama}
|
||||
</Title>
|
||||
|
||||
<Text c="dimmed" fz="sm" ta="center">
|
||||
{v.daerah}
|
||||
</Text>
|
||||
<Text c="dimmed" fz={{ base: 12, md: 14 }} lh={{ base: 1.4, md: 1.6 }} ta="center">
|
||||
{v.daerah}
|
||||
</Text>
|
||||
|
||||
<Text c="blue" fw={600} fz="sm" ta="center">
|
||||
{v.periode}
|
||||
</Text>
|
||||
<Text c="blue" fw={600} fz={{ base: 12, md: 14 }} lh={{ base: 1.4, md: 1.6 }} ta="center">
|
||||
{v.periode}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Paper>
|
||||
@@ -2,7 +2,7 @@
|
||||
'use client'
|
||||
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { Box, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||
import { useEffect } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
@@ -34,60 +34,57 @@ function VisiMisiDesa() {
|
||||
loading="lazy"
|
||||
/>
|
||||
|
||||
{/* VISI */}
|
||||
<Paper
|
||||
p="xl"
|
||||
radius="lg"
|
||||
shadow="md"
|
||||
withBorder
|
||||
w="100%"
|
||||
style={{
|
||||
background: 'linear-gradient(145deg, #ffffff, #f5f7fa)',
|
||||
}}
|
||||
style={{ background: 'linear-gradient(145deg, #ffffff, #f5f7fa)' }}
|
||||
>
|
||||
<Text
|
||||
<Title
|
||||
order={1}
|
||||
c={colors['blue-button']}
|
||||
ta="center"
|
||||
fw={700}
|
||||
fz={{ base: '2rem', md: '2.5rem' }}
|
||||
mb="md"
|
||||
>
|
||||
Visi Desa
|
||||
</Text>
|
||||
</Title>
|
||||
|
||||
<Text
|
||||
fz={{ base: '1.125rem', md: '1.375rem' }}
|
||||
fz={{ base: 'sm', md: 'md' }} // body text responsive
|
||||
lh={1.7}
|
||||
ta="center"
|
||||
fw={500}
|
||||
lh={1.6}
|
||||
dangerouslySetInnerHTML={{ __html: data.visi }}
|
||||
style={{wordBreak: "break-word", whiteSpace: "normal"}}
|
||||
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
|
||||
/>
|
||||
</Paper>
|
||||
|
||||
{/* MISI */}
|
||||
<Paper
|
||||
p="xl"
|
||||
radius="lg"
|
||||
shadow="md"
|
||||
withBorder
|
||||
w="100%"
|
||||
style={{
|
||||
background: 'linear-gradient(145deg, #ffffff, #f5f7fa)',
|
||||
}}
|
||||
style={{ background: 'linear-gradient(145deg, #ffffff, #f5f7fa)' }}
|
||||
>
|
||||
<Text
|
||||
<Title
|
||||
order={1}
|
||||
c={colors['blue-button']}
|
||||
ta="center"
|
||||
fw={700}
|
||||
fz={{ base: '2rem', md: '2.5rem' }}
|
||||
mb="md"
|
||||
>
|
||||
Misi Desa
|
||||
</Text>
|
||||
</Title>
|
||||
|
||||
<Text
|
||||
fz={{ base: '1.125rem', md: '1.375rem' }}
|
||||
fw={500}
|
||||
lh={1.6}
|
||||
fz={{ base: 'sm', md: 'md' }} // body text responsive
|
||||
lh={1.7}
|
||||
ta="left"
|
||||
dangerouslySetInnerHTML={{ __html: data.misi }}
|
||||
style={{wordBreak: "break-word", whiteSpace: "normal"}}
|
||||
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
|
||||
/>
|
||||
</Paper>
|
||||
</Stack>
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Grid, GridCol, Paper, SimpleGrid, Stack, Table, Text, Title } from '@mantine/core';
|
||||
import { Box, Flex, Group, Paper, SimpleGrid, Stack, Table, Text, Title } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||
@@ -30,196 +30,265 @@ function Page() {
|
||||
// Hasil akhir
|
||||
const sisaAnggaran = totalPendapatan - totalBelanja - totalPembiayaan;
|
||||
|
||||
const formatCurrency = (value: number) => {
|
||||
return new Intl.NumberFormat('id-ID', {
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
minimumFractionDigits: 0
|
||||
}).format(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap="lg">
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
<Text ta="center" fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw="bold">
|
||||
|
||||
{/* Page Title */}
|
||||
<Title
|
||||
ta="center"
|
||||
c={colors["blue-button"]}
|
||||
fw="bold"
|
||||
order={1}
|
||||
fz={{ base: 28, md: 36 }}
|
||||
>
|
||||
Pendapatan Asli Desa
|
||||
</Text>
|
||||
</Title>
|
||||
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Stack gap="lg" justify="center">
|
||||
<Paper bg={colors['white-1']} p="xl">
|
||||
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="md">
|
||||
<Paper bg={colors['white-1']} p={{ base: 'md', md: 'xl' }}>
|
||||
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg">
|
||||
{/* Pendapatan Card */}
|
||||
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
|
||||
<Stack gap={"xs"}>
|
||||
<Title order={3}>Pendapatan</Title>
|
||||
{latestApb?.pendapatan?.map((item) => (
|
||||
<Box key={item.id}>
|
||||
<Grid>
|
||||
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
<Text fz="md" fw={500}>{item.name}</Text>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
<Box
|
||||
p="md"
|
||||
style={{
|
||||
border: '1px solid #e9ecef',
|
||||
borderRadius: '8px',
|
||||
height: '100%'
|
||||
}}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Title order={3} fz={{ base: 18, md: 20 }} c={colors['blue-button']}>
|
||||
Pendapatan
|
||||
</Title>
|
||||
|
||||
<Stack gap="sm">
|
||||
{latestApb?.pendapatan?.map((item) => (
|
||||
<Box key={item.id}>
|
||||
<Flex gap={1}>
|
||||
<Text
|
||||
fz="md"
|
||||
fz={{ base: 13, md: 14 }}
|
||||
fw={500}
|
||||
style={{
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: 'normal',
|
||||
textAlign: 'right',
|
||||
}}
|
||||
lh={1.4}
|
||||
c="black"
|
||||
style={{ wordBreak: 'break-word' }}
|
||||
>
|
||||
{new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(item.value)}
|
||||
{item.name} {formatCurrency(item.value)}
|
||||
</Text>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
</Box>
|
||||
))}
|
||||
<Grid>
|
||||
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
<Text fz="lg" fw={600} mb="xs">Total Pendapatan</Text>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
<Text style={{
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: 'normal'
|
||||
}} fz="xl" fw={700} c={colors['blue-button']}>
|
||||
{new Intl.NumberFormat('id-ID', {
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
minimumFractionDigits: 0
|
||||
}).format(totalPendapatan)}
|
||||
</Flex>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
<Box
|
||||
pt="sm"
|
||||
mt="auto"
|
||||
style={{
|
||||
borderTop: `2px solid ${colors['blue-button']}`
|
||||
}}
|
||||
>
|
||||
<Flex direction="column" gap={4}>
|
||||
<Text fz={{ base: 14, md: 16 }} fw={600} lh={1.4}>
|
||||
Total Pendapatan
|
||||
</Text>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
<Text
|
||||
fz={{ base: 18, md: 22 }}
|
||||
fw={700}
|
||||
c={colors['blue-button']}
|
||||
lh={1.4}
|
||||
>
|
||||
{formatCurrency(totalPendapatan)}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{/* Belanja Card */}
|
||||
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
|
||||
<Stack gap={"xs"}>
|
||||
<Title order={3}>Belanja</Title>
|
||||
{latestApb?.belanja?.map((item) => (
|
||||
<Box key={item.id}>
|
||||
<Grid>
|
||||
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
<Text fz="md" fw={500}>{item.name}</Text>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
<Text
|
||||
fz="md"
|
||||
<Box
|
||||
p="md"
|
||||
style={{
|
||||
border: '1px solid #e9ecef',
|
||||
borderRadius: '8px',
|
||||
height: '100%'
|
||||
}}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Title order={3} fz={{ base: 18, md: 20 }} c="orange">
|
||||
Belanja
|
||||
</Title>
|
||||
|
||||
<Stack gap="sm">
|
||||
{latestApb?.belanja?.map((item) => (
|
||||
<Box key={item.id}>
|
||||
<Group gap={1}>
|
||||
<Text
|
||||
fz={{ base: 13, md: 14 }}
|
||||
fw={500}
|
||||
style={{
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: 'normal',
|
||||
textAlign: 'right',
|
||||
}}
|
||||
lh={1.4}
|
||||
c="black"
|
||||
style={{ wordBreak: 'break-word' }}
|
||||
>
|
||||
{new Intl.NumberFormat('id-ID', {
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
minimumFractionDigits: 0
|
||||
}).format(item.value)}
|
||||
{item.name} {formatCurrency(item.value)}
|
||||
</Text>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
</Box>
|
||||
))}
|
||||
<Grid>
|
||||
<GridCol span={{ base: 12, md: 6 }}>
|
||||
<Text fz="lg" fw={600} mb="xs">Total Belanja</Text>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 6 }}>
|
||||
<Text fz="xl" fw={700} c="orange">
|
||||
{new Intl.NumberFormat('id-ID', {
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
minimumFractionDigits: 0
|
||||
}).format(totalBelanja)}
|
||||
</Group>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
<Box
|
||||
pt="sm"
|
||||
mt="auto"
|
||||
style={{
|
||||
borderTop: '2px solid orange'
|
||||
}}
|
||||
>
|
||||
<Flex direction="column" gap={4}>
|
||||
<Text fz={{ base: 14, md: 16 }} fw={600} lh={1.4}>
|
||||
Total Belanja
|
||||
</Text>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
<Text
|
||||
fz={{ base: 18, md: 22 }}
|
||||
fw={700}
|
||||
c="orange"
|
||||
lh={1.4}
|
||||
>
|
||||
{formatCurrency(totalBelanja)}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{/* Pembiayaan Card */}
|
||||
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
|
||||
<Stack gap={"xs"}>
|
||||
<Title order={3}>Pembiayaan</Title>
|
||||
{latestApb?.pembiayaan?.map((item) => (
|
||||
<Box key={item.id}>
|
||||
<Grid>
|
||||
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
<Text fz="md" fw={500}>{item.name}</Text>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
<Text
|
||||
fz="md"
|
||||
<Box
|
||||
p="md"
|
||||
style={{
|
||||
border: '1px solid #e9ecef',
|
||||
borderRadius: '8px',
|
||||
height: '100%'
|
||||
}}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Title order={3} fz={{ base: 18, md: 20 }} c="green">
|
||||
Pembiayaan
|
||||
</Title>
|
||||
|
||||
<Stack gap="sm">
|
||||
{latestApb?.pembiayaan?.map((item) => (
|
||||
<Box key={item.id}>
|
||||
<Group gap={1}>
|
||||
<Text
|
||||
fz={{ base: 13, md: 14 }}
|
||||
fw={500}
|
||||
style={{
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: 'normal',
|
||||
textAlign: 'right',
|
||||
}}
|
||||
lh={1.4}
|
||||
c="black"
|
||||
style={{ wordBreak: 'break-word' }}
|
||||
>
|
||||
{new Intl.NumberFormat('id-ID', {
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
minimumFractionDigits: 0
|
||||
}).format(item.value)}
|
||||
{item.name} {formatCurrency(item.value)}
|
||||
</Text>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
</Box>
|
||||
))}
|
||||
<Grid>
|
||||
<GridCol span={{ base: 12, md: 6 }}>
|
||||
<Text fz="lg" fw={600} mb="xs">Total Pembiayaan</Text>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 6 }}>
|
||||
<Text fz="xl" fw={700} c="green">
|
||||
{new Intl.NumberFormat('id-ID', {
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
minimumFractionDigits: 0
|
||||
}).format(totalPembiayaan)}
|
||||
</Group>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
<Box
|
||||
pt="sm"
|
||||
mt="auto"
|
||||
style={{
|
||||
borderTop: '2px solid green'
|
||||
}}
|
||||
>
|
||||
<Flex direction="column" gap={4}>
|
||||
<Text fz={{ base: 14, md: 16 }} fw={600} lh={1.4}>
|
||||
Total Pembiayaan
|
||||
</Text>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
<Text
|
||||
fz={{ base: 18, md: 22 }}
|
||||
fw={700}
|
||||
c="green"
|
||||
lh={1.4}
|
||||
>
|
||||
{formatCurrency(totalPembiayaan)}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
</SimpleGrid>
|
||||
</Paper>
|
||||
|
||||
{/* 🔽 Tambahan Ringkasan Anggaran */}
|
||||
<Paper bg={colors['white-1']} p="xl" shadow="sm" withBorder>
|
||||
<Title order={3} mb="md">Ringkasan Anggaran</Title>
|
||||
{/* Ringkasan Anggaran */}
|
||||
<Paper bg={colors['white-1']} p={{ base: 'md', md: 'xl' }} shadow="sm" withBorder>
|
||||
<Title order={3} mb="md" fz={{ base: 18, md: 20 }}>
|
||||
Ringkasan Anggaran
|
||||
</Title>
|
||||
<Table striped highlightOnHover withTableBorder>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Keterangan</Table.Th>
|
||||
<Table.Th ta={"right"}>Jumlah</Table.Th>
|
||||
<Table.Th>
|
||||
<Text fz={{ base: 13, md: 14 }} fw={600}>Keterangan</Text>
|
||||
</Table.Th>
|
||||
<Table.Th ta="right">
|
||||
<Text fz={{ base: 13, md: 14 }} fw={600}>Jumlah</Text>
|
||||
</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
<Table.Tr>
|
||||
<Table.Td>Total Pendapatan</Table.Td>
|
||||
<Table.Td align="right">
|
||||
{new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(totalPendapatan)}
|
||||
<Table.Td>
|
||||
<Text fz={{ base: 13, md: 14 }} lh={1.4}>Total Pendapatan</Text>
|
||||
</Table.Td>
|
||||
<Table.Td ta="right">
|
||||
<Text fz={{ base: 13, md: 14 }} fw={600} lh={1.4}>
|
||||
{formatCurrency(totalPendapatan)}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
<Table.Tr>
|
||||
<Table.Td>Total Belanja</Table.Td>
|
||||
<Table.Td align="right" c="orange">
|
||||
{new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(totalBelanja)}
|
||||
<Table.Td>
|
||||
<Text fz={{ base: 13, md: 14 }} lh={1.4} c="orange">Total Belanja</Text>
|
||||
</Table.Td>
|
||||
<Table.Td ta="right">
|
||||
<Text fz={{ base: 13, md: 14 }} fw={600} lh={1.4} c="orange">
|
||||
{formatCurrency(totalBelanja)}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
<Table.Tr>
|
||||
<Table.Td>Total Pembiayaan</Table.Td>
|
||||
<Table.Td align="right" c="green">
|
||||
{new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(totalPembiayaan)}
|
||||
<Table.Td>
|
||||
<Text fz={{ base: 13, md: 14 }} lh={1.4} c="green">Total Pembiayaan</Text>
|
||||
</Table.Td>
|
||||
<Table.Td ta="right">
|
||||
<Text fz={{ base: 13, md: 14 }} fw={600} lh={1.4} c="green">
|
||||
{formatCurrency(totalPembiayaan)}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
<Table.Tr>
|
||||
<Table.Td><b>Sisa Anggaran</b></Table.Td>
|
||||
<Table.Td align="right" c={sisaAnggaran >= 0 ? "blue" : "red"}>
|
||||
<b>
|
||||
{new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(sisaAnggaran)}
|
||||
</b>
|
||||
<Table.Tr style={{ backgroundColor: '#f8f9fa' }}>
|
||||
<Table.Td>
|
||||
<Text fz={{ base: 14, md: 15 }} fw={700} lh={1.4}>Sisa Anggaran</Text>
|
||||
</Table.Td>
|
||||
<Table.Td ta="right">
|
||||
<Text
|
||||
fz={{ base: 14, md: 15 }}
|
||||
fw={700}
|
||||
c={sisaAnggaran >= 0 ? colors['blue-button'] : "red"}
|
||||
lh={1.4}
|
||||
>
|
||||
{formatCurrency(sisaAnggaran)}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
</Table.Tbody>
|
||||
|
||||
@@ -76,13 +76,13 @@ function Page() {
|
||||
</Box>
|
||||
<Flex pb={30} justify={'center'} gap={'xl'} align={'center'}>
|
||||
<Box>
|
||||
<Flex gap={{ base: 0, md: 5 }} align={'center'}>
|
||||
<Flex gap={{base: 7, md: 5}} align={'center'}>
|
||||
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Laki-Laki</Text>
|
||||
<ColorSwatch color="#5082EE" size={30} />
|
||||
</Flex>
|
||||
</Box>
|
||||
<Box>
|
||||
<Flex gap={{ base: 0, md: 5 }} align={'center'}>
|
||||
<Flex gap={{base: 7, md: 5}} align={'center'}>
|
||||
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Perempuan</Text>
|
||||
<ColorSwatch color="#6EDF9C" size={30} />
|
||||
</Flex>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
import pasarDesaState from '@/app/admin/(dashboard)/_state/ekonomi/pasar-desa/pasar-desa';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Center, Flex, Grid, GridCol, Image, Pagination, Paper, Select, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
|
||||
import { Box, Center, Flex, Grid, GridCol, Image, Pagination, Paper, Select, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||
import { IconBrandWhatsapp, IconMapPinFilled, IconSearch, IconStarFilled } from '@tabler/icons-react';
|
||||
import { motion } from 'motion/react';
|
||||
@@ -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);
|
||||
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
||||
const {
|
||||
data,
|
||||
@@ -28,7 +28,6 @@ function Page() {
|
||||
pasarDesaState.kategoriProduk.findManyAll.load()
|
||||
}, [])
|
||||
|
||||
// Filter data based on selected category
|
||||
const filteredData = selectedCategory
|
||||
? data?.filter(item =>
|
||||
item.KategoriToPasar?.some(kategori => kategori.kategoriId === selectedCategory)
|
||||
@@ -39,7 +38,6 @@ function Page() {
|
||||
load(page, 4, debouncedSearch, selectedCategory || undefined)
|
||||
}, [page, debouncedSearch, selectedCategory])
|
||||
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
@@ -49,44 +47,38 @@ function Page() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Grid align='center' px={{ base: 'md', md: 100 }}>
|
||||
<Grid align="center" px={{ base: 'md', md: 100 }}>
|
||||
<GridCol span={{ base: 12, md: 9 }}>
|
||||
<Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
|
||||
<Title order={1} c={colors["blue-button"]} fw="bold">
|
||||
Pasar Desa
|
||||
</Text>
|
||||
</Title>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 3 }}>
|
||||
<TextInput
|
||||
radius={"lg"}
|
||||
placeholder='Cari Produk'
|
||||
radius="lg"
|
||||
placeholder="Cari Produk"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftSection={<IconSearch size={20} />}
|
||||
w={{ base: "50%", md: "100%" }}
|
||||
w={"100%"}
|
||||
/>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
<Text px={{ base: 'md', md: 100 }} ta={"justify"} fz="md" >
|
||||
Pasar Desa Online adalah media promosi untuk membantu warga memasarkan
|
||||
</Text>
|
||||
<Text px={{ base: 'md', md: 100 }} ta={"justify"} fz="md" >
|
||||
dan memperkenalkan produk mereka.
|
||||
|
||||
<Text px={{ base: 'md', md: 100 }} pt={20} ta="justify" fz={{ base: 'sm', md: 'md' }}>
|
||||
Pasar Desa Online adalah media promosi untuk membantu warga memasarkan dan memperkenalkan produk mereka.
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Stack gap={'lg'}>
|
||||
<SimpleGrid
|
||||
pb={30}
|
||||
cols={{
|
||||
base: 1,
|
||||
md: 2
|
||||
}}
|
||||
>
|
||||
<Stack gap="lg">
|
||||
<SimpleGrid pb={30} cols={{ base: 1, md: 2 }}>
|
||||
<Box>
|
||||
<Select
|
||||
placeholder="Pilih Kategori"
|
||||
@@ -103,50 +95,58 @@ function Page() {
|
||||
/>
|
||||
</Box>
|
||||
</SimpleGrid>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, md: 4 }}>
|
||||
{filteredData?.map((v, k) => {
|
||||
return (
|
||||
<Stack key={k}>
|
||||
<motion.div
|
||||
onClick={() => router.push(`/darmasaba/ekonomi/pasar-desa/${v.id}`)}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.8 }}
|
||||
>
|
||||
<Paper p={'lg'}>
|
||||
<Image
|
||||
radius={'lg'}
|
||||
src={v.image?.link || '/placeholder-product.jpg'}
|
||||
alt={v.nama}
|
||||
h={200}
|
||||
w='100%'
|
||||
style={{ objectFit: 'cover' }}
|
||||
loading="lazy"
|
||||
/>
|
||||
<Text py={10} fw={'bold'} fz={'lg'}>{v.nama}</Text>
|
||||
<Text fz={'md'}>Rp {v.harga.toLocaleString('id-ID')}</Text>
|
||||
<Flex py={10} gap={'md'}>
|
||||
<IconStarFilled size={20} color='#EBCB09' />
|
||||
<Text fz={'sm'} ml={2}>{v.rating}</Text>
|
||||
</Flex>
|
||||
<Flex justify={'space-between'} align={'center'}>
|
||||
<Box>
|
||||
<Flex gap={'md'} align={'center'}>
|
||||
<IconMapPinFilled size={20} color='red' />
|
||||
<Text fz={'sm'} ml={2}>{v.alamatUsaha}</Text>
|
||||
</Flex>
|
||||
</Box>
|
||||
<IconBrandWhatsapp size={20} color={colors['blue-button']} />
|
||||
</Flex>
|
||||
</Paper>
|
||||
</motion.div>
|
||||
</Stack>
|
||||
)
|
||||
})}
|
||||
{filteredData?.map((v, k) => (
|
||||
<Stack key={k}>
|
||||
<motion.div
|
||||
onClick={() => router.push(`/darmasaba/ekonomi/pasar-desa/${v.id}`)}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.8 }}
|
||||
>
|
||||
<Paper p="lg">
|
||||
<Image
|
||||
radius="lg"
|
||||
src={v.image?.link || '/placeholder-product.jpg'}
|
||||
alt={v.nama}
|
||||
h={200}
|
||||
w="100%"
|
||||
style={{ objectFit: 'cover' }}
|
||||
loading="lazy"
|
||||
/>
|
||||
<Text py="sm" fw="bold" fz={{ base: 'md', md: 'lg' }}>
|
||||
{v.nama}
|
||||
</Text>
|
||||
<Text fz={{ base: 'sm', md: 'md' }}>
|
||||
Rp {v.harga.toLocaleString('id-ID')}
|
||||
</Text>
|
||||
<Flex py="sm" gap="md">
|
||||
<IconStarFilled size={20} color="#EBCB09" />
|
||||
<Text fz={{ base: 'xs', md: 'sm' }} ml={2}>
|
||||
{v.rating}
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex justify="space-between" align="center">
|
||||
<Box>
|
||||
<Flex gap="md" align="center">
|
||||
<IconMapPinFilled size={20} color="red" />
|
||||
<Text fz={{ base: 'xs', md: 'sm' }} ml={2}>
|
||||
{v.alamatUsaha}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Box>
|
||||
<IconBrandWhatsapp size={20} color={colors['blue-button']} />
|
||||
</Flex>
|
||||
</Paper>
|
||||
</motion.div>
|
||||
</Stack>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => load(newPage)} // ini penting!
|
||||
onChange={(newPage) => load(newPage)}
|
||||
total={totalPages}
|
||||
my="md"
|
||||
/>
|
||||
@@ -157,4 +157,4 @@ function Page() {
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
export default Page;
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
import programKemiskinanState from '@/app/admin/(dashboard)/_state/ekonomi/program-kemiskinan';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Center, Grid, GridCol, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
|
||||
import { Box, Center, Grid, GridCol, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||
import { IconSearch } from '@tabler/icons-react';
|
||||
import { useState } from 'react';
|
||||
@@ -32,10 +32,9 @@ interface ProgramKemiskinanData {
|
||||
|
||||
function Page() {
|
||||
const [search, setSearch] = useState('')
|
||||
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||
const state = useProxy(programKemiskinanState)
|
||||
|
||||
// 🔧 Get valid statistics data with proper type checking
|
||||
const statistikData = state.findMany.data
|
||||
.filter((item): item is ProgramKemiskinanData & { statistik: StatistikData } => {
|
||||
return !!item?.statistik &&
|
||||
@@ -43,11 +42,11 @@ function Page() {
|
||||
item.statistik.jumlah !== undefined;
|
||||
})
|
||||
.map(item => ({
|
||||
tahun: Number(item.statistik.tahun) || 0, // Ensure tahun is a number
|
||||
jumlah: Number(item.statistik.jumlah) || 0, // Ensure jumlah is a number
|
||||
tahun: Number(item.statistik.tahun) || 0,
|
||||
jumlah: Number(item.statistik.jumlah) || 0,
|
||||
}))
|
||||
.sort((a, b) => a.tahun - b.tahun)
|
||||
.filter(item => !isNaN(item.tahun) && !isNaN(item.jumlah)); // Remove any invalid entries
|
||||
.filter(item => !isNaN(item.tahun) && !isNaN(item.jumlah));
|
||||
|
||||
const {
|
||||
data,
|
||||
@@ -74,12 +73,18 @@ function Page() {
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
<Box px={{ base: 'md', md: 100 }} >
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Grid align='center'>
|
||||
<GridCol span={{ base: 12, md: 9 }}>
|
||||
<Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
|
||||
<Title
|
||||
order={1}
|
||||
c={colors["blue-button"]}
|
||||
fw={"bold"}
|
||||
fz={{ base: '28px', md: '32px' }}
|
||||
lh={{ base: '1.2', md: '1.25' }}
|
||||
>
|
||||
Program Kemiskinan
|
||||
</Text>
|
||||
</Title>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 3 }}>
|
||||
<TextInput
|
||||
@@ -92,7 +97,15 @@ function Page() {
|
||||
/>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
<Text fz={'h4'}>Berbagai program bantuan untuk mengurangi kemiskinan dan meningkatkan kesejahteraan masyarakat</Text>
|
||||
<Text
|
||||
fz={{ base: '14px', md: '16px' }}
|
||||
lh={{ base: '1.5', md: '1.6' }}
|
||||
c="black"
|
||||
ta={{ base: 'left', md: 'left' }}
|
||||
pt={20}
|
||||
>
|
||||
Berbagai program bantuan untuk mengurangi kemiskinan dan meningkatkan kesejahteraan masyarakat
|
||||
</Text>
|
||||
</Box>
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Stack gap={'lg'} justify='center'>
|
||||
@@ -106,8 +119,22 @@ function Page() {
|
||||
{state.findMany.data.map(v => {
|
||||
return (
|
||||
<Paper p={'xl'} key={v.id}>
|
||||
<Text fz={'h3'} fw={'bold'} c={colors['blue-button']}>{v.nama}</Text>
|
||||
<Text fz={'lg'} c={'black'} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: v.deskripsi }}></Text>
|
||||
<Title
|
||||
order={3}
|
||||
fw={'bold'}
|
||||
c={colors['blue-button']}
|
||||
fz={{ base: '18px', md: '20px' }}
|
||||
lh={{ base: '1.3', md: '1.35' }}
|
||||
>
|
||||
{v.nama}
|
||||
</Title>
|
||||
<Text
|
||||
fz={{ base: '14px', md: '16px' }}
|
||||
lh={{ base: '1.5', md: '1.6' }}
|
||||
c={'black'}
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
|
||||
/>
|
||||
</Paper>
|
||||
)
|
||||
})}
|
||||
@@ -124,7 +151,16 @@ function Page() {
|
||||
/>
|
||||
</Center>
|
||||
<Paper p={'xl'}>
|
||||
<Text fz={'h3'} fw={'bold'} c={colors['blue-button']} mb="md">Statistik Kemiskinan Masyarakat</Text>
|
||||
<Title
|
||||
order={3}
|
||||
fw={'bold'}
|
||||
c={colors['blue-button']}
|
||||
fz={{ base: '18px', md: '20px' }}
|
||||
lh={{ base: '1.3', md: '1.35' }}
|
||||
mb="md"
|
||||
>
|
||||
Statistik Kemiskinan Masyarakat
|
||||
</Title>
|
||||
<Box style={{ width: '100%', height: 'auto' }}>
|
||||
{statistikData.length > 0 ? (
|
||||
<Box w="100%" style={{ overflowX: 'auto' }}>
|
||||
@@ -162,7 +198,11 @@ function Page() {
|
||||
</Box>
|
||||
) : (
|
||||
<Box p="md" ta="center" bg="gray.0" style={{ borderRadius: '8px' }}>
|
||||
<Text c="dimmed">
|
||||
<Text
|
||||
fz={{ base: '12px', md: '14px' }}
|
||||
c="dimmed"
|
||||
lh={{ base: '1.4', md: '1.5' }}
|
||||
>
|
||||
{state.findMany.loading
|
||||
? 'Memuat data statistik...'
|
||||
: 'Belum ada data statistik yang tersedia atau data tidak valid'}
|
||||
@@ -177,4 +217,4 @@ function Page() {
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
export default Page;
|
||||
@@ -53,6 +53,7 @@ function Page() {
|
||||
Ton: item.value,
|
||||
}));
|
||||
|
||||
const chartWidth = Math.max(600, chartData.length * 150); // contoh: 150px per bar
|
||||
|
||||
return (
|
||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
||||
@@ -78,7 +79,7 @@ function Page() {
|
||||
<Box style={{ width: '100%', overflowX: 'auto' }}>
|
||||
<Paper p="xl">
|
||||
<Text pb={10} fw="bold" fz="h4">Statistik Sektor Unggulan Darmasaba</Text>
|
||||
<Box style={{ width: '100%', minWidth: '600px' }}>
|
||||
<Box style={{ width: '100%', overflowX: 'auto', maxWidth: `${chartWidth}px` }}>
|
||||
<BarChart
|
||||
p={10}
|
||||
h={300}
|
||||
@@ -90,11 +91,14 @@ function Page() {
|
||||
tickLine="y"
|
||||
tooltipAnimationDuration={200}
|
||||
withTooltip
|
||||
style={{
|
||||
fontFamily: 'inherit',
|
||||
}}
|
||||
withXAxis
|
||||
withYAxis
|
||||
xAxisLabel="Sektor"
|
||||
yAxisLabel="Ton"
|
||||
style={{
|
||||
fontFamily: 'inherit',
|
||||
fontSize: '12px', // ukuran font lebih kecil di mobile
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
@@ -14,6 +14,9 @@ import {
|
||||
Loader,
|
||||
Paper,
|
||||
Stack,
|
||||
Tabs,
|
||||
TabsList,
|
||||
TabsTab,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
@@ -33,6 +36,8 @@ import { OrganizationChart } from 'primereact/organizationchart'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useProxy } from 'valtio/utils'
|
||||
import BackButton from '../../desa/layanan/_com/BackButto'
|
||||
import { useMediaQuery } from '@mantine/hooks'
|
||||
import { useTransitionRouter } from 'next-view-transitions'
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
@@ -51,11 +56,11 @@ export default function Page() {
|
||||
order={1}
|
||||
ta="center"
|
||||
c={colors['blue-button']}
|
||||
fz={{ base: 28, md: 36, lg: 44 }}
|
||||
fz={{ base: 28, md: 36 }}
|
||||
>
|
||||
Struktur Organisasi & SK Pengurus BumDes
|
||||
</Title>
|
||||
<Text ta="center" c="black" maw={800}>
|
||||
<Text ta="center" c="black" maw={800} fz={{ base: 14, md: 16 }} lh={1.6}>
|
||||
Gambaran visual peran dan pengurus yang ditugaskan. Gunakan kontrol
|
||||
di bawah untuk mencari, memperbesar, atau melihat lebih jelas.
|
||||
</Text>
|
||||
@@ -70,13 +75,14 @@ export default function Page() {
|
||||
}
|
||||
|
||||
function StrukturOrganisasiBumDes() {
|
||||
const router = useTransitionRouter()
|
||||
const stateOrganisasi: any = useProxy(stateStrukturBumDes.pegawai)
|
||||
const chartContainerRef = useRef<HTMLDivElement>(null)
|
||||
const [scale, setScale] = useState(1)
|
||||
const [isFullscreen, setFullscreen] = useState(false)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const debouncedSearch = useRef(
|
||||
debounce((value: string) => setSearchQuery(value), 400)
|
||||
debounce((value: string) => setSearchQuery(value), 1000)
|
||||
).current
|
||||
|
||||
useEffect(() => {
|
||||
@@ -92,8 +98,10 @@ function StrukturOrganisasiBumDes() {
|
||||
<Center py={48}>
|
||||
<Stack align="center" gap="sm">
|
||||
<Loader size="lg" />
|
||||
<Text fw={600}>Memuat struktur organisasi…</Text>
|
||||
<Text c="dimmed" size="sm">
|
||||
<Text fw={600} fz={{ base: 15, md: 16 }} lh={1.4}>
|
||||
Memuat struktur organisasi…
|
||||
</Text>
|
||||
<Text c="dimmed" fz={{ base: 12, md: 14 }} lh={1.4}>
|
||||
Mengambil data pengurus dan posisi. Mohon tunggu sebentar.
|
||||
</Text>
|
||||
</Stack>
|
||||
@@ -119,10 +127,10 @@ function StrukturOrganisasiBumDes() {
|
||||
<Center>
|
||||
<IconUsers size={56} />
|
||||
</Center>
|
||||
<Title order={3} mt="md">
|
||||
<Title order={3} mt="md" ta="center">
|
||||
Data pengurus belum tersedia
|
||||
</Title>
|
||||
<Text c="dimmed" mt="xs">
|
||||
<Text c="dimmed" mt="xs" fz={{ base: 12, md: 14 }} lh={1.4}>
|
||||
Belum ada data pengurus yang tercatat untuk BumDes.
|
||||
</Text>
|
||||
<Group justify="center" mt="lg">
|
||||
@@ -218,155 +226,299 @@ function StrukturOrganisasiBumDes() {
|
||||
return (
|
||||
<Stack align="center" mt="xl">
|
||||
{/* 🧭 Kontrol atas */}
|
||||
<Paper shadow="xs" p="md" radius="md" bg={colors['blue-button']}>
|
||||
<Group gap="sm" wrap="wrap" justify="center">
|
||||
<TextInput
|
||||
placeholder="Cari nama atau jabatan..."
|
||||
leftSection={<IconSearch size={16} />}
|
||||
onChange={(e) => debouncedSearch(e.target.value)}
|
||||
<Paper
|
||||
shadow="xs"
|
||||
w={{
|
||||
base: '100%', // Mobile: 100%
|
||||
sm: '40%', // Tablet: 95%
|
||||
md: '39%', // Desktop: 70%
|
||||
lg: '38%', // Desktop L: 60%
|
||||
xl: '37%', // 4K: 50%
|
||||
'2xl': '36%', // Ultra-wide: 45%
|
||||
}}
|
||||
p="md"
|
||||
radius="md"
|
||||
style={{
|
||||
background: colors['blue-button'], // ⬅️ penting
|
||||
maxWidth: '100%', // ⬅️ penting
|
||||
overflowX: 'auto' // ⬅️ untuk mencegah overflow
|
||||
}}
|
||||
>
|
||||
<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,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Group gap="xs">
|
||||
<Button
|
||||
variant="light"
|
||||
bg={colors['blue-button-2']}
|
||||
c={colors['blue-button']}
|
||||
size="sm"
|
||||
onClick={handleZoomOut}
|
||||
leftSection={<IconZoomOut size={16} />}
|
||||
>
|
||||
Zoom Out
|
||||
</Button>
|
||||
<Box
|
||||
bg={colors['blue-button-2']}
|
||||
c={colors['blue-button']}
|
||||
px={16}
|
||||
py={8}
|
||||
style={{ width: '100%' }} // 👈 penting
|
||||
>
|
||||
<TabsList
|
||||
style={{
|
||||
fontSize: 14,
|
||||
fontWeight: 700,
|
||||
borderRadius: '8px',
|
||||
minWidth: 70,
|
||||
textAlign: 'center',
|
||||
display: 'flex',
|
||||
overflowX: 'auto',
|
||||
overflowY: 'hidden',
|
||||
gap: '4px',
|
||||
paddingBottom: '4px',
|
||||
flexWrap: 'nowrap',
|
||||
WebkitOverflowScrolling: 'touch',
|
||||
scrollbarWidth: 'thin',
|
||||
msOverflowStyle: '-ms-autohiding-scrollbar',
|
||||
maxWidth: '100%',
|
||||
scrollBehavior: 'smooth', // 👈 smooth scroll
|
||||
}}
|
||||
>
|
||||
{Math.round(scale * 100)}%
|
||||
</Box>
|
||||
<Button
|
||||
variant="light"
|
||||
bg={colors['blue-button-2']}
|
||||
c={colors['blue-button']}
|
||||
size="sm"
|
||||
onClick={handleZoomIn}
|
||||
leftSection={<IconZoomIn size={16} />}
|
||||
>
|
||||
Zoom In
|
||||
</Button>
|
||||
<Button
|
||||
variant="light"
|
||||
bg={colors['blue-button-2']}
|
||||
c={colors['blue-button']}
|
||||
size="sm"
|
||||
onClick={resetZoom}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
<Button
|
||||
variant="light"
|
||||
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="zoom-out"
|
||||
onClick={handleZoomOut}
|
||||
leftSection={<IconZoomOut size={16} />}
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
<Text fz={{ base: 12, sm: 13 }} lh={1} ta="center">Zoom Out</Text>
|
||||
</TabsTab>
|
||||
|
||||
<Box
|
||||
bg={colors['blue-button-2']}
|
||||
c={colors['blue-button']}
|
||||
px={12}
|
||||
py={6}
|
||||
style={{
|
||||
fontWeight: 700,
|
||||
borderRadius: '6px',
|
||||
minWidth: 60,
|
||||
textAlign: 'center',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
<Text fz={{ base: 12, sm: 13 }} lh={1} c={colors['blue-button']}>
|
||||
{Math.round(scale * 100)}%
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<TabsTab
|
||||
value="zoom-in"
|
||||
onClick={handleZoomIn}
|
||||
leftSection={<IconZoomIn size={16} />}
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
<Text fz={{ base: 12, sm: 13 }} lh={1} ta="center">Zoom In</Text>
|
||||
</TabsTab>
|
||||
|
||||
<TabsTab
|
||||
value="reset"
|
||||
onClick={resetZoom}
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
<Text fz={{ base: 12, sm: 13 }} lh={1} ta="center">Reset</Text>
|
||||
</TabsTab>
|
||||
|
||||
<TabsTab
|
||||
value="fullscreen"
|
||||
onClick={toggleFullscreen}
|
||||
leftSection={
|
||||
isFullscreen ? (
|
||||
<IconArrowsMinimize size={16} />
|
||||
) : (
|
||||
<IconArrowsMaximize size={16} />
|
||||
)
|
||||
}
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
<Text fz={{ base: 12, sm: 13 }} lh={1} ta="center">
|
||||
{isFullscreen ? 'Exit' : 'Fullscreen'}
|
||||
</Text>
|
||||
</TabsTab>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* 📊 Chart Container */}
|
||||
<Center style={{ width: '100%' }}>
|
||||
<Box
|
||||
ref={chartContainerRef}
|
||||
style={{
|
||||
overflowX: 'auto',
|
||||
overflowY: 'auto',
|
||||
width: '100%',
|
||||
padding: '32px 16px',
|
||||
transition: 'transform 0.2s ease',
|
||||
transform: `scale(${scale})`,
|
||||
transformOrigin: 'center top',
|
||||
}}
|
||||
>
|
||||
<OrganizationChart
|
||||
value={chartData}
|
||||
nodeTemplate={(node) => <NodeCard node={node} />}
|
||||
className="p-organizationchart p-organizationchart-horizontal"
|
||||
/>
|
||||
</Box>
|
||||
</Center>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
function NodeCard({ node }: any) {
|
||||
const imageSrc = node?.data?.image || '/img/default.png'
|
||||
const name = node?.data?.name || 'Tanpa Nama'
|
||||
const title = node?.data?.title || 'Tanpa Jabatan'
|
||||
const description = node?.data?.description || ''
|
||||
|
||||
return (
|
||||
<Transition mounted transition="pop" duration={300}>
|
||||
{(styles) => (
|
||||
<Card
|
||||
shadow="md"
|
||||
radius="xl"
|
||||
withBorder
|
||||
style={{
|
||||
...styles,
|
||||
width: 240,
|
||||
padding: 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)',
|
||||
transition: 'all 0.3s ease',
|
||||
}}
|
||||
>
|
||||
<Stack align="center" gap={10}>
|
||||
<Box
|
||||
style={{
|
||||
width: 90,
|
||||
height: 90,
|
||||
borderRadius: '50%',
|
||||
overflow: 'hidden',
|
||||
border: '3px solid rgba(28, 110, 164, 0.4)',
|
||||
}}
|
||||
>
|
||||
<Image src={imageSrc} alt={name} fit="cover" loading="lazy" />
|
||||
</Box>
|
||||
<Text fw={700} size="sm" ta="center" c={colors['blue-button']}>
|
||||
{name}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" ta="center">
|
||||
{title}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" ta="center" lineClamp={3}>
|
||||
{description || 'Belum ada deskripsi.'}
|
||||
</Text>
|
||||
{/* 🧩 Chart Container */}
|
||||
<Center style={{ width: '100%' }}>
|
||||
<Box
|
||||
ref={chartContainerRef}
|
||||
style={{
|
||||
overflowX: 'auto',
|
||||
overflowY: 'auto',
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
padding: '32px 16px',
|
||||
transition: 'transform 0.2s ease',
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
</Card>
|
||||
)}
|
||||
</Transition>
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function NodeCard({ node, router }: any) {
|
||||
const imageSrc = node?.data?.image || '/img/default.png'
|
||||
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}>
|
||||
{(styles) => (
|
||||
<Card
|
||||
shadow="md"
|
||||
radius="xl"
|
||||
withBorder
|
||||
|
||||
style={{
|
||||
...styles,
|
||||
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,
|
||||
transition: 'all 0.3s ease',
|
||||
cursor: hasId ? 'pointer' : 'default',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (hasId) {
|
||||
e.currentTarget.style.transform = 'translateY(-4px)'
|
||||
e.currentTarget.style.boxShadow = '0 8px 24px rgba(28, 110, 164, 0.25)'
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (hasId) {
|
||||
e.currentTarget.style.transform = 'translateY(0)'
|
||||
e.currentTarget.style.boxShadow = ''
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Stack align="center" gap={12}>
|
||||
{/* Photo */}
|
||||
<Box
|
||||
style={{
|
||||
width: 96,
|
||||
height: 96,
|
||||
borderRadius: '50%',
|
||||
overflow: 'hidden',
|
||||
border: '3px solid rgba(28, 110, 164, 0.4)',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
|
||||
background: 'white',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={imageSrc}
|
||||
alt={name}
|
||||
width={96}
|
||||
height={96}
|
||||
fit="cover"
|
||||
loading="lazy"
|
||||
style={{
|
||||
objectFit: 'cover',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Name */}
|
||||
<Text
|
||||
fw={700}
|
||||
ta="center"
|
||||
c={colors['blue-button']}
|
||||
lineClamp={2}
|
||||
fz={{ base: 13, md: 15 }}
|
||||
lh={1.2}
|
||||
style={{
|
||||
minHeight: 40,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</Text>
|
||||
|
||||
{/* Title/Position */}
|
||||
<Text
|
||||
c="dimmed"
|
||||
ta="center"
|
||||
fw={500}
|
||||
lineClamp={2}
|
||||
fz={{ base: 12, md: 13 }}
|
||||
lh={1.3}
|
||||
style={{
|
||||
minHeight: 32,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
|
||||
{/* Detail Button */}
|
||||
{hasId && (
|
||||
<Button
|
||||
variant="gradient"
|
||||
gradient={{ from: 'blue', to: 'cyan' }}
|
||||
size="xs"
|
||||
fullWidth
|
||||
mt={8}
|
||||
radius="md"
|
||||
onClick={() =>
|
||||
router.push(`/darmasaba/ppid/struktur-ppid/${node.data.id}`)
|
||||
}
|
||||
style={{
|
||||
height: 32,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
<Text fz={{ base: 12, md: 13 }} lh={1} ta="center">Lihat Detail</Text>
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</Card>
|
||||
)}
|
||||
</Transition>
|
||||
)
|
||||
}
|
||||
@@ -1,20 +1,18 @@
|
||||
'use client'
|
||||
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
||||
import ajukanIdeInovatifState from '@/app/admin/(dashboard)/_state/inovasi/ajukan-ide-inovatif';
|
||||
import colors from '@/con/colors';
|
||||
import { ActionIcon, Box, Button, Flex, List, ListItem, Modal, Paper, SimpleGrid, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import { IconArrowRight, IconBulbFilled } from '@tabler/icons-react';
|
||||
import { IconBulbFilled } from '@tabler/icons-react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
||||
|
||||
function Page() {
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const ideInovatif = useProxy(ajukanIdeInovatifState)
|
||||
|
||||
const ideInovatif = useProxy(ajukanIdeInovatifState);
|
||||
|
||||
const resetForm = () => {
|
||||
// Reset state di valtio
|
||||
ideInovatif.create.form = {
|
||||
name: "",
|
||||
deskripsi: "",
|
||||
@@ -23,53 +21,66 @@ function Page() {
|
||||
masalah: "",
|
||||
benefit: "",
|
||||
};
|
||||
|
||||
// Reset state lokal
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
// Submit data berita
|
||||
await ideInovatif.create.create();
|
||||
|
||||
// Reset form setelah submit
|
||||
resetForm();
|
||||
close();
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
<Box px={{ base: 'md', md: 100 }} >
|
||||
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
|
||||
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Title
|
||||
order={1}
|
||||
ta="center"
|
||||
c={colors["blue-button"]}
|
||||
fw="bold"
|
||||
style={{ fontSize: 'clamp(1.75rem, 4vw, 2.25rem)' }}
|
||||
>
|
||||
Ajukan Ide Inovatif
|
||||
</Title>
|
||||
<Text ta="center" fz={{ base: 'sm', md: 'md' }} c="black" lh="1.6">
|
||||
Desa Darmasaba percaya bahwa setiap warga memiliki potensi luar biasa untuk menciptakan perubahan positif. Platform "Ajukan Ide Inovatif" hadir sebagai ruang inklusif bagi seluruh masyarakat untuk mengembangkan dan mengusulkan gagasan transformatif.
|
||||
</Text>
|
||||
<Text ta={'center'} fz={'h4'}>Desa Darmasaba percaya bahwa setiap warga memiliki potensi luar biasa untuk menciptakan perubahan positif. Platform "Ajukan Ide Inovatif" hadir sebagai ruang inklusif bagi seluruh masyarakat untuk mengembangkan dan mengusulkan gagasan transformatif.</Text>
|
||||
</Box>
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Stack gap={'lg'} p={'lg'}>
|
||||
<SimpleGrid
|
||||
cols={{
|
||||
base: 1,
|
||||
md: 2,
|
||||
}}
|
||||
>
|
||||
<Paper p={'xl'} >
|
||||
<Stack gap={"xs"}>
|
||||
<Text fz={'h3'} fw={'bold'} c={colors['blue-button']}>Tujuan Ide Inovatif Ini</Text>
|
||||
<List>
|
||||
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Mendorong partisipasi aktif masyarakat</ListItem>
|
||||
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Memfasilitasi inovasi berbasis lokal</ListItem>
|
||||
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Memecahkan tantangan komunal</ListItem>
|
||||
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Mengembangkan potensi kreativitas warga</ListItem>
|
||||
</List>
|
||||
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Stack gap="lg" p="lg">
|
||||
<SimpleGrid cols={{ base: 1, md: 2 }}>
|
||||
<Paper p="xl">
|
||||
<Stack gap="xs">
|
||||
<Title order={2} c={colors['blue-button']} fw="bold">
|
||||
Tujuan Ide Inovatif Ini
|
||||
</Title>
|
||||
<List>
|
||||
<ListItem ta="justify" fz={{ base: 'sm', md: 'md' }} lh="1.5">
|
||||
Mendorong partisipasi aktif masyarakat
|
||||
</ListItem>
|
||||
<ListItem ta="justify" fz={{ base: 'sm', md: 'md' }} lh="1.5">
|
||||
Memfasilitasi inovasi berbasis lokal
|
||||
</ListItem>
|
||||
<ListItem ta="justify" fz={{ base: 'sm', md: 'md' }} lh="1.5">
|
||||
Memecahkan tantangan komunal
|
||||
</ListItem>
|
||||
<ListItem ta="justify" fz={{ base: 'sm', md: 'md' }} lh="1.5">
|
||||
Mengembangkan potensi kreativitas warga
|
||||
</ListItem>
|
||||
</List>
|
||||
</Stack>
|
||||
</Paper>
|
||||
<Paper p={'xl'} >
|
||||
<Flex align={'center'} justify={'space-between'}>
|
||||
|
||||
<Paper p="xl">
|
||||
<Flex align="center" justify="space-between" direction={{ base: 'column', md: 'row' }} gap="md">
|
||||
<Box>
|
||||
<Text fz={'h4'} fw={'bold'} c={colors['blue-button']}>Apabila Anda Ingin Mengajukan Ide Inovatif Bisa Klik Pada Gambar Di Samping</Text>
|
||||
<IconArrowRight size={30} color={colors['blue-button']} />
|
||||
<Title order={3} c={colors['blue-button']} fw="bold" ta={{ base: 'center', md: 'start' }}>
|
||||
Apabila Anda Ingin Mengajukan Ide Inovatif Bisa Klik Pada Gambar
|
||||
</Title>
|
||||
</Box>
|
||||
<Box px={{ base: 5, md: 10 }} py={5}>
|
||||
<ActionIcon variant="transparent" size={150} onClick={open}>
|
||||
@@ -88,32 +99,46 @@ function Page() {
|
||||
radius={0}
|
||||
transitionProps={{ transition: 'fade', duration: 200 }}
|
||||
>
|
||||
<Paper p={"md"} withBorder>
|
||||
<Stack gap={"xs"}>
|
||||
<Paper p="md" withBorder>
|
||||
<Stack gap="xs">
|
||||
<Title order={3}>Ajukan Ide Inovatif</Title>
|
||||
<TextInput
|
||||
label={<Text fz={"sm"} fw={"bold"}>Nama</Text>}
|
||||
label={
|
||||
<Text fz="sm" fw="bold">
|
||||
Nama
|
||||
</Text>
|
||||
}
|
||||
placeholder="masukkan nama"
|
||||
onChange={(val) => {
|
||||
ideInovatif.create.form.name = val.target.value
|
||||
ideInovatif.create.form.name = val.target.value;
|
||||
}}
|
||||
/>
|
||||
<TextInput
|
||||
label={<Text fz={"sm"} fw={"bold"}>Alamat</Text>}
|
||||
label={
|
||||
<Text fz="sm" fw="bold">
|
||||
Alamat
|
||||
</Text>
|
||||
}
|
||||
placeholder="masukkan alamat"
|
||||
onChange={(val) => {
|
||||
ideInovatif.create.form.alamat = val.target.value
|
||||
ideInovatif.create.form.alamat = val.target.value;
|
||||
}}
|
||||
/>
|
||||
<TextInput
|
||||
label={<Text fz={"sm"} fw={"bold"}>Nama Ide</Text>}
|
||||
label={
|
||||
<Text fz="sm" fw="bold">
|
||||
Nama Ide
|
||||
</Text>
|
||||
}
|
||||
placeholder="masukkan nama ide"
|
||||
onChange={(val) => {
|
||||
ideInovatif.create.form.namaIde = val.target.value
|
||||
ideInovatif.create.form.namaIde = val.target.value;
|
||||
}}
|
||||
/>
|
||||
<Box>
|
||||
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
|
||||
<Text fz="sm" fw="bold">
|
||||
Deskripsi
|
||||
</Text>
|
||||
<CreateEditor
|
||||
value={ideInovatif.create.form.deskripsi}
|
||||
onChange={(htmlContent) => {
|
||||
@@ -122,26 +147,35 @@ function Page() {
|
||||
/>
|
||||
</Box>
|
||||
<TextInput
|
||||
label={<Text fz={"sm"} fw={"bold"}>Masalah</Text>}
|
||||
label={
|
||||
<Text fz="sm" fw="bold">
|
||||
Masalah
|
||||
</Text>
|
||||
}
|
||||
placeholder="masukkan masalah"
|
||||
onChange={(val) => {
|
||||
ideInovatif.create.form.masalah = val.target.value
|
||||
ideInovatif.create.form.masalah = val.target.value;
|
||||
}}
|
||||
/>
|
||||
<TextInput
|
||||
label={<Text fz={"sm"} fw={"bold"}>Benefit</Text>}
|
||||
label={
|
||||
<Text fz="sm" fw="bold">
|
||||
Benefit
|
||||
</Text>
|
||||
}
|
||||
placeholder="masukkan benefit"
|
||||
onChange={(val) => {
|
||||
ideInovatif.create.form.benefit = val.target.value
|
||||
ideInovatif.create.form.benefit = val.target.value;
|
||||
}}
|
||||
/>
|
||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
|
||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>
|
||||
Simpan
|
||||
</Button>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Modal>
|
||||
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
export default Page;
|
||||
@@ -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 {
|
||||
@@ -58,7 +58,7 @@ function Page() {
|
||||
/>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
<Text fz={'md'}>Menjadikan Desa Darmasaba pusat inovasi digital untuk pemberdayaan masyarakat</Text>
|
||||
<Text pt={20} fz={'md'}>Menjadikan Desa Darmasaba pusat inovasi digital untuk pemberdayaan masyarakat</Text>
|
||||
<Text fz={'md'}>dan peningkatan ekonomi berbasis teknologi.</Text>
|
||||
</Box>
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Center, Grid, GridCol, Image, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
|
||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||
import { Box, Center, Grid, GridCol, Image, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||
import { useState } from 'react';
|
||||
import infoTeknoState from '@/app/admin/(dashboard)/_state/inovasi/info-tekno';
|
||||
import { IconSearch } from '@tabler/icons-react';
|
||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||
|
||||
function Page() {
|
||||
const [search, setSearch] = useState("")
|
||||
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||
const state = useProxy(infoTeknoState)
|
||||
const {
|
||||
data,
|
||||
@@ -34,17 +34,24 @@ function Page() {
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
<Box px={{ base: 'md', md: 100 }} >
|
||||
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Grid align='center'>
|
||||
<GridCol span={{ base: 12, md: 9 }}>
|
||||
<Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
|
||||
<Title
|
||||
order={1}
|
||||
c={colors["blue-button"]}
|
||||
fw={"bold"}
|
||||
ta={{ base: 'center', md: 'left' }}
|
||||
>
|
||||
Info Teknologi Tepat Guna
|
||||
</Text>
|
||||
</Title>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 3 }}>
|
||||
<TextInput
|
||||
@@ -53,13 +60,19 @@ function Page() {
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftSection={<IconSearch size={20} />}
|
||||
w={{ base: "50%", md: "100%" }}
|
||||
w={{ base: "100%", md: "100%" }}
|
||||
/>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
<Text fz={'md'}>Desa Darmasaba berkomitmen mengembangkan teknologi tepat guna yang sesuai dengan kebutuhan masyarakat,</Text>
|
||||
<Text fz={'md'}>mendukung pembangunan berkelanjutan, dan meningkatkan kualitas hidup warga.</Text>
|
||||
|
||||
<Text pt={20} fz={{ base: 'sm', md: 'md' }} ta={{ base: 'center', md: 'left' }} lh={1.5}>
|
||||
Desa Darmasaba berkomitmen mengembangkan teknologi tepat guna yang sesuai dengan kebutuhan masyarakat,
|
||||
</Text>
|
||||
<Text fz={{ base: 'sm', md: 'md' }} ta={{ base: 'center', md: 'left' }} lh={1.5}>
|
||||
mendukung pembangunan berkelanjutan, dan meningkatkan kualitas hidup warga.
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Stack gap={'lg'} p={'lg'}>
|
||||
<SimpleGrid
|
||||
@@ -74,12 +87,14 @@ function Page() {
|
||||
<Paper p={'xl'} key={k}>
|
||||
<Stack gap={"xs"}>
|
||||
<Image src={v.image.link || ''} pb={10} radius={10} alt='' loading="lazy" />
|
||||
<Text fz={'h3'} fw={'bold'}>{v.name}</Text>
|
||||
<Title order={3} fw={'bold'} ta="left">
|
||||
{v.name}
|
||||
</Title>
|
||||
<Box pr={'lg'} pb={10}>
|
||||
<Text
|
||||
size="md"
|
||||
fz={{ base: 'xs', md: 'sm' }}
|
||||
ta="justify"
|
||||
lh={1} // line height biar enak dibaca
|
||||
lh={1.5}
|
||||
style={{
|
||||
wordBreak: "break-word",
|
||||
whiteSpace: "normal",
|
||||
@@ -94,10 +109,11 @@ function Page() {
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => load(newPage)} // ini penting!
|
||||
onChange={(newPage) => load(newPage)}
|
||||
total={totalPages}
|
||||
my="md"
|
||||
/>
|
||||
@@ -106,4 +122,4 @@ function Page() {
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
export default Page;
|
||||
@@ -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,
|
||||
@@ -58,7 +58,7 @@ function Page() {
|
||||
/>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
<Text px={{ base: 'md', md: 100 }} ta={"justify"} fz="md" mt={4} >
|
||||
<Text px={{ base: 'md', md: 100 }} pt={20} ta={"justify"} fz="md" mt={4} >
|
||||
Pecalang dan Patwal (Patroli Pengawal) bertugas memastikan desa tetap aman, tertib, dan kondusif bagi seluruh warga.
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -71,14 +71,14 @@ function Page() {
|
||||
<Stack gap="md">
|
||||
<Box>
|
||||
<Text fw={600} fz="lg" >Judul</Text>
|
||||
<Text fz="sm" c="dimmed">{data.judul || '-'}</Text>
|
||||
<Text fz="sm" c="black">{data.judul || '-'}</Text>
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Box>
|
||||
<Text fw={600} fz="lg" >Tanggal</Text>
|
||||
<Text fz="sm" c="dimmed">
|
||||
<Text fz="sm" c="black">
|
||||
{data.tanggalWaktu
|
||||
? new Date(data.tanggalWaktu).toLocaleString('id-ID', { dateStyle: 'full', timeStyle: 'short' })
|
||||
: '-'}
|
||||
@@ -89,7 +89,7 @@ function Page() {
|
||||
|
||||
<Box>
|
||||
<Text fw={600} fz="lg" >Lokasi</Text>
|
||||
<Text fz="sm" c="dimmed">{data.lokasi || '-'}</Text>
|
||||
<Text fz="sm" c="black">{data.lokasi || '-'}</Text>
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
@@ -120,7 +120,7 @@ function Page() {
|
||||
|
||||
<Box>
|
||||
<Text fw={600} fz="lg" >Kronologi</Text>
|
||||
<Text fz="sm" c="dimmed" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: data.kronologi || '-' }} />
|
||||
<Text fz="sm" c="black" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: data.kronologi || '-' }} />
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
@@ -136,11 +136,11 @@ function Page() {
|
||||
radius="md"
|
||||
shadow="xs"
|
||||
withBorder
|
||||
bg="dark.5"
|
||||
bg={colors['blue-button-1']}
|
||||
>
|
||||
<Text
|
||||
fz="sm"
|
||||
c="dimmed"
|
||||
c="black"
|
||||
dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }}
|
||||
style={{wordBreak: "break-word", whiteSpace: "normal"}}
|
||||
/>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Center, Grid, GridCol, Image, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
|
||||
import { Box, Center, Grid, GridCol, Image, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import tipsKeamananState from '@/app/admin/(dashboard)/_state/keamanan/tips-keamanan';
|
||||
@@ -8,11 +8,10 @@ import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||
import { useState } from 'react';
|
||||
import { IconSearch } from '@tabler/icons-react';
|
||||
|
||||
|
||||
function Page() {
|
||||
const state = useProxy(tipsKeamananState)
|
||||
const [search, setSearch] = useState('')
|
||||
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
|
||||
const state = useProxy(tipsKeamananState);
|
||||
const [search, setSearch] = useState('');
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||
const {
|
||||
data,
|
||||
page,
|
||||
@@ -22,84 +21,114 @@ function Page() {
|
||||
} = state.findMany;
|
||||
|
||||
useShallowEffect(() => {
|
||||
load(page, 3, debouncedSearch)
|
||||
}, [page, debouncedSearch])
|
||||
load(page, 3, debouncedSearch);
|
||||
}, [page, debouncedSearch]);
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
|
||||
<Skeleton h={500} />
|
||||
</Stack>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Grid align='center' px={{ base: 'md', md: 100 }}>
|
||||
<Grid align="center" px={{ base: 'md', md: 100 }}>
|
||||
<GridCol span={{ base: 12, md: 9 }}>
|
||||
<Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
|
||||
<Title
|
||||
order={1}
|
||||
c={colors['blue-button']}
|
||||
style={{ lineHeight: '1.2' }}
|
||||
>
|
||||
Tips Keamanan
|
||||
</Text>
|
||||
</Title>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 3 }}>
|
||||
<TextInput
|
||||
radius={"lg"}
|
||||
placeholder='Cari Tips'
|
||||
radius="lg"
|
||||
placeholder="Cari Tips"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftSection={<IconSearch size={20} />}
|
||||
w={{ base: "50%", md: "100%" }}
|
||||
w={'100%'}
|
||||
/>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
<Text px={{ base: 'md', md: 100 }} ta={"justify"} fz="md" >
|
||||
|
||||
<Text
|
||||
px={{ base: 'md', md: 100 }}
|
||||
ta="justify"
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={{ base: '1.5', md: '1.6' }}
|
||||
mt="sm"
|
||||
>
|
||||
Keamanan dan ketertiban lingkungan di Desa Darmasaba dijaga melalui peran aktif Pecalang dan Patwal (Patroli Pengawal).
|
||||
</Text>
|
||||
<Text px={{ base: 'md', md: 100 }} ta={"justify"} fz="md" >
|
||||
<Text
|
||||
px={{ base: 'md', md: 100 }}
|
||||
ta="justify"
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={{ base: '1.5', md: '1.6' }}
|
||||
mt="xs"
|
||||
>
|
||||
Mereka bertugas memastikan desa tetap aman, tertib, dan kondusif bagi seluruh warga.
|
||||
</Text>
|
||||
</Box>
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Stack gap={'lg'}>
|
||||
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Stack gap="lg">
|
||||
<SimpleGrid
|
||||
pb={10}
|
||||
cols={{
|
||||
base: 1,
|
||||
md: 3,
|
||||
}}>
|
||||
{data.map((v, k) => {
|
||||
return (
|
||||
<Paper radius={10} key={k} bg={colors["white-trans-1"]}>
|
||||
<Stack gap={'xs'}>
|
||||
<Center p={10}>
|
||||
<Image src={v.image?.link} radius={10} loading="lazy"
|
||||
alt='' />
|
||||
</Center>
|
||||
<Box px={'xl'}>
|
||||
<Box pb={20}>
|
||||
<Text pb={10} c={colors["blue-button"]} fw={"bold"} fz={"h3"}>
|
||||
{v.judul}
|
||||
</Text>
|
||||
<Box>
|
||||
<Text pb={10} fz={"md"} style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: v.deskripsi }} />
|
||||
</Box>
|
||||
pb="10"
|
||||
cols={{ base: 1, md: 3 }}
|
||||
>
|
||||
{data.map((v, k) => (
|
||||
<Paper radius={10} key={k} bg={colors['white-trans-1']}>
|
||||
<Stack gap="xs">
|
||||
<Center p="10">
|
||||
<Image
|
||||
src={v.image?.link}
|
||||
radius={10}
|
||||
loading="lazy"
|
||||
alt=""
|
||||
/>
|
||||
</Center>
|
||||
<Box px="xl">
|
||||
<Box pb="20">
|
||||
<Title
|
||||
order={3}
|
||||
c={colors['blue-button']}
|
||||
style={{ lineHeight: '1.3' }}
|
||||
>
|
||||
{v.judul}
|
||||
</Title>
|
||||
<Box>
|
||||
<Text
|
||||
pb="10"
|
||||
fz={{ base: 'xs', md: 'md' }}
|
||||
lh={{ base: '1.5', md: '1.6' }}
|
||||
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
|
||||
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => load(newPage)} // ini penting!
|
||||
onChange={(newPage) => load(newPage)}
|
||||
total={totalPages}
|
||||
my="md"
|
||||
/>
|
||||
@@ -108,4 +137,4 @@ function Page() {
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
export default 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, Title } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconAlertCircle, IconCalendar, IconInfoCircle } from '@tabler/icons-react';
|
||||
import { useParams } from 'next/navigation';
|
||||
@@ -37,25 +37,25 @@ function Page() {
|
||||
<Stack gap="lg">
|
||||
<Paper radius="xl" shadow="md" withBorder>
|
||||
<Box style={{ borderTopLeftRadius: 16, borderTopRightRadius: 16 }} bg={colors['blue-button']}>
|
||||
<Text p="md" fz={{ base: 'h3', md: 'h2' }} c={colors['white-1']} fw="bold">
|
||||
<Title order={1} p="md" c={colors['white-1']} fw="bold">
|
||||
{state.findUnique.data.title || 'Detail Artikel Kesehatan'}
|
||||
</Text>
|
||||
</Title>
|
||||
</Box>
|
||||
|
||||
<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>
|
||||
@@ -64,7 +64,7 @@ function Page() {
|
||||
<Stack gap="lg">
|
||||
<Group gap="xs">
|
||||
<IconCalendar size={18} color={colors['blue-button']} />
|
||||
<Text c="dimmed" fz="sm">
|
||||
<Text fz={{ base: 'xs', md: 'sm' }} lh={1.5}>
|
||||
{new Date(state.findUnique.data.createdAt).toLocaleDateString('id-ID', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
@@ -74,50 +74,61 @@ function Page() {
|
||||
</Group>
|
||||
|
||||
<Stack gap="lg">
|
||||
|
||||
<Box>
|
||||
<Text fz="h4" fw="bold">Pendahuluan</Text>
|
||||
<Title order={2} fw="bold">Pendahuluan</Title>
|
||||
<Divider my="xs" />
|
||||
<Text fz="md" lh={1.6} ta="justify" dangerouslySetInnerHTML={{ __html: state.findUnique.data.introduction?.content }} />
|
||||
<Box pl={20}>
|
||||
<Text fz={{ base: 'sm', md: '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>
|
||||
<Title order={2} fw="bold">{state.findUnique.data.symptom?.title}</Title>
|
||||
<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={{ base: 'sm', md: '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>
|
||||
<Title order={2} fw="bold">{state.findUnique.data.prevention?.title}</Title>
|
||||
<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={{ base: 'sm', md: '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>
|
||||
<Title order={2} fw="bold">{state.findUnique.data.firstaid?.title}</Title>
|
||||
<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={{ base: 'sm', md: 'md' }} lh={1.6} ta="justify" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.firstaid?.content }} />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz="h4" fw="bold">{state.findUnique.data.mythvsfact?.title}</Text>
|
||||
<Title order={2} fw="bold">{state.findUnique.data.mythvsfact?.title}</Title>
|
||||
<Divider my="xs" />
|
||||
<Box pb="md">
|
||||
<Table highlightOnHover withTableBorder withColumnBorders striped>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh fz="sm" fw="bold">Mitos</TableTh>
|
||||
<TableTh fz="sm" fw="bold">Fakta</TableTh>
|
||||
<TableTh fz={{ base: 'xs', md: 'sm' }} fw="bold">Mitos</TableTh>
|
||||
<TableTh fz={{ base: 'xs', md: 'sm' }} fw="bold">Fakta</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{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={{ base: 'xs', md: '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={{ base: 'xs', md: 'sm' }} lh={1.6} style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.mythvsfact.fakta }} />
|
||||
</Box>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
) : (
|
||||
@@ -131,36 +142,35 @@ function Page() {
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz="h4" fw="bold">Kapan Harus ke Dokter?</Text>
|
||||
<Title order={2} fw="bold">Kapan Harus ke Dokter?</Title>
|
||||
<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 }} />
|
||||
<Text fz={{ base: 'sm', md: 'md' }} lh={1.5}>Segera bawa penderita ke fasilitas kesehatan jika mengalami:</Text>
|
||||
</Flex>
|
||||
<Box pl={20}>
|
||||
<Text fz={{ base: 'sm', md: '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']} />
|
||||
<Text fz="h4" c={colors['white-1']} fw="bold">Informasi Lebih Lanjut</Text>
|
||||
<Title order={3} c={colors['white-1']} fw="bold">Informasi Lebih Lanjut</Title>
|
||||
</Group>
|
||||
<Stack gap={4}>
|
||||
<Text fz="sm" c={colors['white-1']}>Hotline DBD: <b>(0361) 123456</b></Text>
|
||||
<Text fz="sm" c={colors['white-1']}>WhatsApp Center: <b>081234567890</b></Text>
|
||||
<Text fz="sm" c={colors['white-1']}>Email: <b>p2p@dinkes.badungkab.go.id</b></Text>
|
||||
<Text fz={{ base: 'xs', md: 'sm' }} c={colors['white-1']}>Hotline DBD: <b>(0361) 123456</b></Text>
|
||||
<Text fz={{ base: 'xs', md: 'sm' }} c={colors['white-1']}>WhatsApp Center: <b>081234567890</b></Text>
|
||||
<Text fz={{ base: 'xs', md: 'sm' }} c={colors['white-1']}>Email: <b>p2p@dinkes.badungkab.go.id</b></Text>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz="h4" fw="bold">Referensi</Text>
|
||||
<Title order={2} fw="bold">Referensi</Title>
|
||||
<Divider my="xs" />
|
||||
<List spacing="xs" size="sm" type="ordered">
|
||||
<List spacing="xs" fz={{ base: 'xs', md: 'sm' }} type="ordered">
|
||||
<ListItem>Kementerian Kesehatan RI. (2024). Pedoman Pencegahan dan Pengendalian DBD.</ListItem>
|
||||
<ListItem>World Health Organization. (2024). Dengue Guidelines for Diagnosis, Treatment, Prevention and Control.</ListItem>
|
||||
<ListItem>Dinas Kesehatan Kabupaten Badung. (2025). Laporan Surveilans DBD Triwulan I 2025.</ListItem>
|
||||
@@ -176,4 +186,4 @@ function Page() {
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
export default Page;
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
import artikelKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/artikelKesehatan';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Card, Divider, Group, Image, Loader, Paper, Stack, Text } from '@mantine/core';
|
||||
import { Box, Button, Card, Divider, Group, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconCalendar, IconChevronRight } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
@@ -17,9 +17,8 @@ function ArtikelKesehatanPage() {
|
||||
|
||||
if (!state.findMany.data) {
|
||||
return (
|
||||
<Box py="xl" ta="center">
|
||||
<Loader size="lg" color={colors['blue-button']} />
|
||||
<Text mt="md" c="dimmed" fz="md">Memuat artikel kesehatan...</Text>
|
||||
<Box py="lg">
|
||||
<Skeleton h={500} radius="lg" />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -28,13 +27,13 @@ function ArtikelKesehatanPage() {
|
||||
<Box>
|
||||
<Paper p="xl" bg={colors['white-trans-1']} radius="xl" shadow="md">
|
||||
<Stack gap="lg">
|
||||
<Text ta="center" fw={700} fz="32px" c={colors['blue-button']}>
|
||||
<Title ta="center" order={1} c={colors['blue-button']}>
|
||||
Artikel Kesehatan
|
||||
</Text>
|
||||
</Title>
|
||||
<Divider size="sm" color={colors['blue-button']} />
|
||||
{state.findMany.data.length === 0 ? (
|
||||
<Box py="xl" ta="center">
|
||||
<Text fz="lg" c="dimmed">
|
||||
<Text fz={{ base: 'sm', sm: 'md' }} c="dimmed">
|
||||
Belum ada artikel kesehatan yang tersedia
|
||||
</Text>
|
||||
</Box>
|
||||
@@ -51,17 +50,26 @@ function ArtikelKesehatanPage() {
|
||||
onMouseLeave={(e) => (e.currentTarget.style.transform = 'translateY(0)')}
|
||||
>
|
||||
<Card.Section>
|
||||
<Image style={{ borderTopLeftRadius: '10px', borderTopRightRadius: '10px' }} src={item.image?.link} alt={item.title} height={200} fit="cover" loading="lazy" />
|
||||
<Image
|
||||
style={{ borderTopLeftRadius: '10px', borderTopRightRadius: '10px' }}
|
||||
src={item.image?.link}
|
||||
alt={item.title}
|
||||
height={200}
|
||||
fit="cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
</Card.Section>
|
||||
<Stack gap="xs" mt="md">
|
||||
<Text fw="bold" fz="xl" c={colors['blue-button']}>{item.title}</Text>
|
||||
<Title order={3} c={colors['blue-button']}>
|
||||
{item.title}
|
||||
</Title>
|
||||
<Group gap="xs">
|
||||
<IconCalendar size={16} color='gray' />
|
||||
<Text fz="sm" c="dimmed">
|
||||
<Text fz={{ base: 'xs', sm: 'sm' }} c="dimmed">
|
||||
{new Date(item.createdAt).toLocaleDateString('id-ID', { year: 'numeric', month: 'long', day: 'numeric' })} • Dinas Kesehatan
|
||||
</Text>
|
||||
</Group>
|
||||
<Text fz="md" lineClamp={3}>
|
||||
<Text fz={{ base: 'sm', sm: 'md' }} lh={{ base: 'sm', sm: 'md' }} lineClamp={3}>
|
||||
{item.content}
|
||||
</Text>
|
||||
<Group justify="flex-start">
|
||||
@@ -85,4 +93,4 @@ function ArtikelKesehatanPage() {
|
||||
);
|
||||
}
|
||||
|
||||
export default ArtikelKesehatanPage;
|
||||
export default ArtikelKesehatanPage;
|
||||
@@ -16,7 +16,6 @@ interface Kontak {
|
||||
email: string;
|
||||
}
|
||||
|
||||
|
||||
interface Lokasi {
|
||||
mapsEmbed: string;
|
||||
}
|
||||
@@ -35,7 +34,7 @@ function Page() {
|
||||
state.findUnique.load(params?.id as string);
|
||||
}, []);
|
||||
|
||||
const data = state.findUnique.data as any; // Temporary any to fix type issues
|
||||
const data = state.findUnique.data as any;
|
||||
|
||||
const nama = data?.name || 'Fasilitas Kesehatan';
|
||||
const prosedur = data?.prosedurpendaftaran.content || '';
|
||||
@@ -111,11 +110,11 @@ function Page() {
|
||||
<Group gap="md" wrap="wrap">
|
||||
<Group gap="xs">
|
||||
<ThemeIcon variant="light" radius="xl"><IconMapPin size={18} /></ThemeIcon>
|
||||
<Text>{alamat}</Text>
|
||||
<Text fz={{ base: 'sm', md: 'md' }} lh={1.5}>{alamat}</Text>
|
||||
</Group>
|
||||
<Group gap="xs">
|
||||
<ThemeIcon variant="light" radius="xl"><IconDeviceLandlinePhone size={18} /></ThemeIcon>
|
||||
<Text>{kontak.telepon}</Text>
|
||||
<Text fz={{ base: 'sm', md: 'md' }} lh={1.5}>{kontak.telepon}</Text>
|
||||
<CopyButton value={kontak.telepon}>
|
||||
{({ copied, copy }) => (
|
||||
<Tooltip label={copied ? 'Disalin' : 'Salin nomor'}>
|
||||
@@ -126,7 +125,7 @@ function Page() {
|
||||
</Group>
|
||||
<Group gap="xs">
|
||||
<ThemeIcon variant="light" radius="xl"><IconBrandWhatsapp size={18} /></ThemeIcon>
|
||||
<Text>{kontak.whatsapp}</Text>
|
||||
<Text fz={{ base: 'sm', md: 'md' }} lh={1.5}>{kontak.whatsapp}</Text>
|
||||
<CopyButton value={kontak.whatsapp}>
|
||||
{({ copied, copy }) => (
|
||||
<Tooltip label={copied ? 'Disalin' : 'Salin WhatsApp'}>
|
||||
@@ -137,7 +136,7 @@ function Page() {
|
||||
</Group>
|
||||
<Group gap="xs">
|
||||
<ThemeIcon variant="light" radius="xl"><IconMail size={18} /></ThemeIcon>
|
||||
<Text>{kontak.email}</Text>
|
||||
<Text fz={{ base: 'sm', md: 'md' }} lh={1.5}>{kontak.email}</Text>
|
||||
<CopyButton value={kontak.email}>
|
||||
{({ copied, copy }) => (
|
||||
<Tooltip label={copied ? 'Disalin' : 'Salin email'}>
|
||||
@@ -163,31 +162,43 @@ function Page() {
|
||||
<Divider />
|
||||
<Group gap="xl" align="start">
|
||||
<Stack gap={2}>
|
||||
<Text c="dimmed" fz="sm">Nama Fasilitas</Text>
|
||||
<Text fw={600}>{nama}</Text>
|
||||
<Text c="dimmed" fz={{ base: 'xs', md: 'sm' }}>Nama Fasilitas</Text>
|
||||
<Text fz={{ base: 'sm', md: 'md' }} fw={600} lh={1.5}>{nama}</Text>
|
||||
</Stack>
|
||||
<Stack gap={2}>
|
||||
<Text c="dimmed" fz="sm">Jam Operasional</Text>
|
||||
<Text fw={600}>{jam}</Text>
|
||||
<Text c="dimmed" fz={{ base: 'xs', md: 'sm' }}>Jam Operasional</Text>
|
||||
<Text fz={{ base: 'sm', md: 'md' }} fw={600} lh={1.5}>{jam}</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
<Divider />
|
||||
<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={{ base: 'sm', md: 'md' }}
|
||||
lh={{ base: 1.6, md: 1.7 }}
|
||||
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
|
||||
dangerouslySetInnerHTML={{ __html: layananUnggulan }}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<Paper withBorder radius="md" p="md">
|
||||
<Group gap="sm">
|
||||
<IconMoodEmpty />
|
||||
<Text>Belum ada informasi fasilitas pendukung.</Text>
|
||||
<Text fz={{ base: 'sm', md: 'md' }} c="dimmed">Belum ada informasi layanan unggulan.</Text>
|
||||
</Group>
|
||||
</Paper>
|
||||
)}
|
||||
<Divider />
|
||||
<Title order={4}>Peta Lokasi</Title>
|
||||
<AspectRatio ratio={16 / 9}>
|
||||
<iframe src={lokasi.mapsEmbed} style={{ border: 0, width: '100%', height: '100%', borderRadius: 16 }} loading="lazy" aria-label="Peta Lokasi" />
|
||||
<iframe
|
||||
src={lokasi.mapsEmbed}
|
||||
style={{ border: 0, width: '100%', height: '100%', borderRadius: 16 }}
|
||||
loading="lazy"
|
||||
aria-label="Peta Lokasi"
|
||||
/>
|
||||
</AspectRatio>
|
||||
</Stack>
|
||||
</Card>
|
||||
@@ -199,9 +210,15 @@ function Page() {
|
||||
<Stack gap="md">
|
||||
<Title order={4}>Kontak Cepat</Title>
|
||||
<Group gap="sm" wrap="wrap">
|
||||
<Button variant="light" leftSection={<IconDeviceLandlinePhone size={18} />} component="a" href={`tel:${kontak.telepon}`} aria-label="Hubungi Telepon">Telepon</Button>
|
||||
<Button variant="light" leftSection={<IconBrandWhatsapp size={18} />} component="a" href={`https://wa.me/${kontak.whatsapp.replace(/\D/g, '')}`} target="_blank" aria-label="Hubungi WhatsApp">WhatsApp</Button>
|
||||
<Button variant="light" leftSection={<IconMail size={18} />} component="a" href={`mailto:${kontak.email}`} aria-label="Kirim Email">Email</Button>
|
||||
<Button variant="light" leftSection={<IconDeviceLandlinePhone size={18} />} component="a" href={`tel:${kontak.telepon}`} aria-label="Hubungi Telepon">
|
||||
<Text fz={{ base: 'xs', md: 'sm' }}>Telepon</Text>
|
||||
</Button>
|
||||
<Button variant="light" leftSection={<IconBrandWhatsapp size={18} />} component="a" href={`https://wa.me/${kontak.whatsapp.replace(/\D/g, '')}`} target="_blank" aria-label="Hubungi WhatsApp">
|
||||
<Text fz={{ base: 'xs', md: 'sm' }}>WhatsApp</Text>
|
||||
</Button>
|
||||
<Button variant="light" leftSection={<IconMail size={18} />} component="a" href={`mailto:${kontak.email}`} aria-label="Kirim Email">
|
||||
<Text fz={{ base: 'xs', md: 'sm' }}>Email</Text>
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Card>
|
||||
@@ -212,9 +229,15 @@ function Page() {
|
||||
<Table highlightOnHover withTableBorder withColumnBorders aria-label="Tabel Dokter">
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>Nama</TableTh>
|
||||
<TableTh>Spesialisasi</TableTh>
|
||||
<TableTh>Jadwal</TableTh>
|
||||
<TableTh>
|
||||
<Text fz={{ base: 'xs', md: 'sm' }} fw={600}>Nama</Text>
|
||||
</TableTh>
|
||||
<TableTh>
|
||||
<Text fz={{ base: 'xs', md: 'sm' }} fw={600}>Spesialisasi</Text>
|
||||
</TableTh>
|
||||
<TableTh>
|
||||
<Text fz={{ base: 'xs', md: 'sm' }} fw={600}>Jadwal</Text>
|
||||
</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
@@ -224,11 +247,15 @@ function Page() {
|
||||
<TableTd>
|
||||
<Group gap="xs">
|
||||
<IconUser size={16} />
|
||||
<Text>{dokter.name || '-'}</Text>
|
||||
<Text fz={{ base: 'sm', md: 'md' }}>{dokter.name || '-'}</Text>
|
||||
</Group>
|
||||
</TableTd>
|
||||
<TableTd>{dokter.specialist || '-'}</TableTd>
|
||||
<TableTd>{dokter.jadwal || '-'}</TableTd>
|
||||
<TableTd>
|
||||
<Text fz={{ base: 'sm', md: 'md' }}>{dokter.specialist || '-'}</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text fz={{ base: 'sm', md: 'md' }}>{dokter.jadwal || '-'}</Text>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))
|
||||
) : (
|
||||
@@ -236,7 +263,7 @@ function Page() {
|
||||
<TableTd colSpan={3}>
|
||||
<Group justify="center" gap="xs" c="dimmed">
|
||||
<IconSearch size={18} />
|
||||
<Text>Tidak ada data tenaga medis.</Text>
|
||||
<Text fz={{ base: 'sm', md: 'md' }}>Tidak ada data tenaga medis.</Text>
|
||||
</Group>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
@@ -251,12 +278,19 @@ 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={{ base: 'sm', md: 'md' }}
|
||||
lh={{ base: 1.6, md: 1.7 }}
|
||||
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
|
||||
dangerouslySetInnerHTML={{ __html: fasilitasPendukungHtml }}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<Paper withBorder radius="md" p="md">
|
||||
<Group gap="sm">
|
||||
<IconMoodEmpty />
|
||||
<Text>Belum ada informasi fasilitas pendukung.</Text>
|
||||
<Text fz={{ base: 'sm', md: 'md' }} c="dimmed">Belum ada informasi fasilitas pendukung.</Text>
|
||||
</Group>
|
||||
</Paper>
|
||||
)}
|
||||
@@ -270,16 +304,24 @@ function Page() {
|
||||
<Table highlightOnHover withTableBorder withColumnBorders aria-label="Tabel Layanan dan Tarif">
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>Layanan</TableTh>
|
||||
<TableTh>Tarif</TableTh>
|
||||
<TableTh>
|
||||
<Text fz={{ base: 'xs', md: 'sm' }} fw={600}>Layanan</Text>
|
||||
</TableTh>
|
||||
<TableTh>
|
||||
<Text fz={{ base: 'xs', md: 'sm' }} fw={600}>Tarif</Text>
|
||||
</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{Array.isArray(data?.tarifdanlayanan) && data.tarifdanlayanan.length > 0 ? (
|
||||
data.tarifdanlayanan.map((item: any) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>{item.layanan || '-'}</TableTd>
|
||||
<TableTd>{formatRupiah(item.tarif)}</TableTd>
|
||||
<TableTd>
|
||||
<Text fz={{ base: 'sm', md: 'md' }}>{item.layanan || '-'}</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text fz={{ base: 'sm', md: 'md' }}>{formatRupiah(item.tarif)}</Text>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))
|
||||
) : (
|
||||
@@ -287,7 +329,7 @@ function Page() {
|
||||
<TableTd colSpan={2}>
|
||||
<Group justify="center" gap="xs" c="dimmed">
|
||||
<IconSearch size={18} />
|
||||
<Text>Tidak ada data tarif.</Text>
|
||||
<Text fz={{ base: 'sm', md: 'md' }}>Tidak ada data tarif.</Text>
|
||||
</Group>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
@@ -297,7 +339,7 @@ function Page() {
|
||||
{gratisBpjs && (
|
||||
<Group gap="xs">
|
||||
<ThemeIcon variant="light" radius="xl"><IconCheck size={18} /></ThemeIcon>
|
||||
<Text fw={600}>Gratis dengan BPJS Kesehatan</Text>
|
||||
<Text fz={{ base: 'sm', md: 'md' }} fw={600}>Gratis dengan BPJS Kesehatan</Text>
|
||||
</Group>
|
||||
)}
|
||||
</Stack>
|
||||
@@ -313,9 +355,16 @@ 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={{ base: 'sm', md: 'md' }}
|
||||
lh={{ base: 1.6, md: 1.7 }}
|
||||
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
|
||||
dangerouslySetInnerHTML={{ __html: prosedur }}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<Text fz="md" c="dimmed">Belum ada prosedur pendaftaran</Text>
|
||||
<Text fz={{ base: 'sm', md: 'md' }} c="dimmed">Belum ada prosedur pendaftaran</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
@@ -324,4 +373,4 @@ function Page() {
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
export default Page;
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
|
||||
import colors from '@/con/colors';
|
||||
import { Badge, Box, Button, Card, Divider, Group, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { Badge, Box, Button, Card, Divider, Group, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconChevronRight, IconClock, IconMapPin } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
@@ -17,12 +17,8 @@ function FasilitasKesehatanPage() {
|
||||
|
||||
if (!state.findMany.data) {
|
||||
return (
|
||||
<Box py="xl" px="md">
|
||||
<Stack gap="md">
|
||||
<Skeleton height={80} radius="lg" />
|
||||
<Skeleton height={80} radius="lg" />
|
||||
<Skeleton height={80} radius="lg" />
|
||||
</Stack>
|
||||
<Box py="lg">
|
||||
<Skeleton h={500} radius="lg" />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -31,14 +27,24 @@ function FasilitasKesehatanPage() {
|
||||
<Box>
|
||||
<Paper bg={colors['white-trans-1']} p="xl" radius="xl" shadow="md" h="100%">
|
||||
<Stack gap="lg">
|
||||
<Text ta="center" fw={700} fz="32px" c={colors['blue-button']}>
|
||||
<Title
|
||||
order={1}
|
||||
ta="center"
|
||||
fw={700}
|
||||
c={colors['blue-button']}
|
||||
style={{ lineHeight: '1.2' }}
|
||||
>
|
||||
Fasilitas Kesehatan
|
||||
</Text>
|
||||
</Title>
|
||||
<Divider size="sm" color={colors['blue-button']} />
|
||||
<Stack gap="lg">
|
||||
{state.findMany.data.length === 0 ? (
|
||||
<Box py="xl" ta="center">
|
||||
<Text fz="lg" c="dimmed">
|
||||
<Text
|
||||
fz={{ base: 'sm', sm: 'md' }}
|
||||
c={colors['blue-button']}
|
||||
lh={{ base: '1.5', sm: '1.6' }}
|
||||
>
|
||||
Belum ada fasilitas kesehatan yang tersedia
|
||||
</Text>
|
||||
</Box>
|
||||
@@ -65,22 +71,36 @@ function FasilitasKesehatanPage() {
|
||||
>
|
||||
<Stack gap="sm">
|
||||
<Group justify="space-between" align="center">
|
||||
<Text fw={700} fz="lg" c={colors['blue-button']}>
|
||||
<Title
|
||||
order={3}
|
||||
fw={700}
|
||||
c={colors['blue-button']}
|
||||
fz={{ base: 'sm', sm: 'md' }}
|
||||
lh={{ base: '1.3', sm: '1.3' }}
|
||||
>
|
||||
{item.name}
|
||||
</Text>
|
||||
<Badge color="blue" radius="sm" variant="light" fz="xs">
|
||||
</Title>
|
||||
<Badge color="blue" radius="sm" variant="light" size="xs">
|
||||
Aktif
|
||||
</Badge>
|
||||
</Group>
|
||||
<Group gap="xs">
|
||||
<IconMapPin size={18} stroke={1.5} />
|
||||
<Text fz="sm">
|
||||
<Text
|
||||
fz={{ base: 'xs', sm: 'sm' }}
|
||||
lh={{ base: '1.5', sm: '1.5' }}
|
||||
c="text"
|
||||
>
|
||||
{item.informasiumum.alamat}
|
||||
</Text>
|
||||
</Group>
|
||||
<Group gap="xs">
|
||||
<IconClock size={18} stroke={1.5} />
|
||||
<Text fz="sm">
|
||||
<Text
|
||||
fz={{ base: 'xs', sm: 'sm' }}
|
||||
lh={{ base: '1.5', sm: '1.5' }}
|
||||
c="text"
|
||||
>
|
||||
{item.informasiumum.jamOperasional}
|
||||
</Text>
|
||||
</Group>
|
||||
@@ -110,4 +130,4 @@ function FasilitasKesehatanPage() {
|
||||
);
|
||||
}
|
||||
|
||||
export default FasilitasKesehatanPage;
|
||||
export default FasilitasKesehatanPage;
|
||||
@@ -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} >
|
||||
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
Paper,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Text
|
||||
Text,
|
||||
Title
|
||||
} from '@mantine/core';
|
||||
import { useDisclosure, useShallowEffect } from '@mantine/hooks';
|
||||
import { IconMail, IconPhone, IconUser } from '@tabler/icons-react';
|
||||
@@ -51,47 +52,97 @@ function Page() {
|
||||
style={{ borderTopLeftRadius: 16, borderTopRightRadius: 16 }}
|
||||
bg={colors['blue-button']}
|
||||
>
|
||||
<Text p="md" fz={{ base: "h3", md: "h2" }} c={colors['white-1']} fw="bold">
|
||||
<Title
|
||||
p="md"
|
||||
order={1}
|
||||
c={colors['white-1']}
|
||||
fw="bold"
|
||||
ta={{ base: 'center', md: 'left' }}
|
||||
>
|
||||
Detail & Pendaftaran Kegiatan
|
||||
</Text>
|
||||
</Title>
|
||||
</Box>
|
||||
<Box p="lg">
|
||||
<Stack gap="xl">
|
||||
<Stack gap="sm">
|
||||
<Text fz="lg" fw="bold">Informasi Kegiatan</Text>
|
||||
<Title order={2} fw="bold">Informasi Kegiatan</Title>
|
||||
<Divider />
|
||||
<Text fz="md" fw="bold">Nama Kegiatan: <Text span>{state.findUnique.data.informasijadwalkegiatan.name}</Text></Text>
|
||||
<Text fz="md" fw="bold">Tanggal: <Text span>{state.findUnique.data.informasijadwalkegiatan.tanggal}</Text></Text>
|
||||
<Text fz="md" fw="bold">Waktu: <Text span>{state.findUnique.data.informasijadwalkegiatan.waktu}</Text></Text>
|
||||
<Text fz="md" fw="bold">Lokasi: <Text span>{state.findUnique.data.informasijadwalkegiatan.lokasi}</Text></Text>
|
||||
<Text fw="bold">
|
||||
Nama Kegiatan:
|
||||
<Text span fw="normal">
|
||||
{state.findUnique.data.informasijadwalkegiatan.name}
|
||||
</Text>
|
||||
</Text>
|
||||
<Text fw="bold">
|
||||
Tanggal:
|
||||
<Text span fw="normal">
|
||||
{state.findUnique.data.informasijadwalkegiatan.tanggal}
|
||||
</Text>
|
||||
</Text>
|
||||
<Text fw="bold">
|
||||
Waktu:
|
||||
<Text span fw="normal">
|
||||
{state.findUnique.data.informasijadwalkegiatan.waktu}
|
||||
</Text>
|
||||
</Text>
|
||||
<Text fw="bold">
|
||||
Lokasi:
|
||||
<Text span fw="normal">
|
||||
{state.findUnique.data.informasijadwalkegiatan.lokasi}
|
||||
</Text>
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Stack gap="sm">
|
||||
<Text fz="lg" fw="bold">Deskripsi Kegiatan</Text>
|
||||
<Title order={2} fw="bold">Deskripsi Kegiatan</Title>
|
||||
<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"
|
||||
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>
|
||||
<Title order={2} fw="bold">Layanan yang Tersedia</Title>
|
||||
<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"
|
||||
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>
|
||||
<Title order={2} fw="bold">Syarat & Ketentuan</Title>
|
||||
<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"
|
||||
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>
|
||||
<Title order={2} fw="bold">Dokumen yang Perlu Dibawa</Title>
|
||||
<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"
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
dangerouslySetInnerHTML={{ __html: state.findUnique.data.dokumenjadwalkegiatan.content }}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<Stack gap="sm">
|
||||
<Text fz="lg" fw="bold">Pendaftaran Kegiatan</Text>
|
||||
<Title order={2} fw="bold">Pendaftaran Kegiatan</Title>
|
||||
<Divider />
|
||||
<Group>
|
||||
<Button onClick={open}>Buat Pendaftaran</Button>
|
||||
@@ -104,18 +155,21 @@ function Page() {
|
||||
|
||||
<Paper p="lg" radius="md" bg={colors['blue-button-trans']} shadow="sm">
|
||||
<Stack gap="xs">
|
||||
<Text fz="lg" c={colors['white-1']} fw="bold">Informasi Kontak</Text>
|
||||
<Group gap="xs">
|
||||
<Title order={3} c={colors['white-1']} fw="bold">Informasi Kontak</Title>
|
||||
<Group gap="xs" justify="flex-start">
|
||||
<IconUser size={18} color="white" />
|
||||
<Text fz="md" c={colors['white-1']}>Penanggung Jawab: <Text span fw="bold">Bidan Komang Ayu</Text></Text>
|
||||
<Text c={colors['white-1']}>
|
||||
Penanggung Jawab:
|
||||
<Text span fw="bold">Bidan Komang Ayu</Text>
|
||||
</Text>
|
||||
</Group>
|
||||
<Group gap="xs">
|
||||
<IconPhone size={18} color="white" />
|
||||
<Text fz="md" c={colors['white-1']}>081234567890</Text>
|
||||
<Text c={colors['white-1']}>081234567890</Text>
|
||||
</Group>
|
||||
<Group gap="xs">
|
||||
<IconMail size={18} color="white" />
|
||||
<Text fz="md" c={colors['white-1']}>puskesmasabiansemal3@gmail.com</Text>
|
||||
<Text c={colors['white-1']}>puskesmasabiansemal3@gmail.com</Text>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
@@ -128,4 +182,4 @@ function Page() {
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
export default Page;
|
||||
@@ -2,15 +2,14 @@
|
||||
'use client'
|
||||
import pendaftaranJadwalKegiatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/pendafataranJadwalKegiatan';
|
||||
import colors from '@/con/colors';
|
||||
import { Button, Divider, Stack, Text, Textarea, TextInput } from '@mantine/core';
|
||||
import { Button, Divider, Stack, Text, Textarea, TextInput, Title } from '@mantine/core';
|
||||
import { useEffect } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
function CreatePendaftaran() {
|
||||
const stateCreate = useProxy(pendaftaranJadwalKegiatanState);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
useEffect(() => {
|
||||
stateCreate.findMany.load();
|
||||
}, []);
|
||||
|
||||
@@ -32,15 +31,19 @@ useEffect(() => {
|
||||
|
||||
return (
|
||||
<Stack gap="sm">
|
||||
<Text fz="lg" fw="bold">Formulir Pendaftaran</Text>
|
||||
<Title order={2} ta="left">Formulir Pendaftaran</Title>
|
||||
<Divider />
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
label="Nama Balita"
|
||||
placeholder="Masukkan nama balita"
|
||||
size="md"
|
||||
value={stateCreate.create.form.name}
|
||||
onChange={(e) => stateCreate.create.form.name = e.target.value}
|
||||
label="Nama Balita"
|
||||
placeholder="Masukkan nama balita"
|
||||
size="md"
|
||||
value={stateCreate.create.form.name}
|
||||
onChange={(e) => stateCreate.create.form.name = e.target.value}
|
||||
styles={{
|
||||
label: { fontSize: '14px', lineHeight: 1.4, fontWeight: 500 },
|
||||
input: { fontSize: '16px', lineHeight: 1.5 },
|
||||
}}
|
||||
/>
|
||||
<TextInput
|
||||
type='date'
|
||||
@@ -50,41 +53,63 @@ useEffect(() => {
|
||||
w={{ base: '100%', md: '85%', lg: '75%', xl: '50%' }}
|
||||
value={stateCreate.create.form.tanggal}
|
||||
onChange={(e) => stateCreate.create.form.tanggal = e.target.value}
|
||||
styles={{
|
||||
label: { fontSize: '14px', lineHeight: 1.4, fontWeight: 500 },
|
||||
input: { fontSize: '16px', lineHeight: 1.5 },
|
||||
}}
|
||||
/>
|
||||
<TextInput
|
||||
label="Nama Orang Tua / Wali"
|
||||
placeholder="Masukkan nama orang tua / wali"
|
||||
size="md"
|
||||
value={stateCreate.create.form.namaOrangtua}
|
||||
onChange={(e) => stateCreate.create.form.namaOrangtua = e.target.value}
|
||||
label="Nama Orang Tua / Wali"
|
||||
placeholder="Masukkan nama orang tua / wali"
|
||||
size="md"
|
||||
value={stateCreate.create.form.namaOrangtua}
|
||||
onChange={(e) => stateCreate.create.form.namaOrangtua = e.target.value}
|
||||
styles={{
|
||||
label: { fontSize: '14px', lineHeight: 1.4, fontWeight: 500 },
|
||||
input: { fontSize: '16px', lineHeight: 1.5 },
|
||||
}}
|
||||
/>
|
||||
<TextInput
|
||||
label="Nomor Telepon"
|
||||
placeholder="Masukkan nomor telepon"
|
||||
size="md"
|
||||
value={stateCreate.create.form.nomor}
|
||||
onChange={(e) => stateCreate.create.form.nomor = e.target.value}
|
||||
label="Nomor Telepon"
|
||||
placeholder="Masukkan nomor telepon"
|
||||
size="md"
|
||||
value={stateCreate.create.form.nomor}
|
||||
onChange={(e) => stateCreate.create.form.nomor = e.target.value}
|
||||
styles={{
|
||||
label: { fontSize: '14px', lineHeight: 1.4, fontWeight: 500 },
|
||||
input: { fontSize: '16px', lineHeight: 1.5 },
|
||||
}}
|
||||
/>
|
||||
<TextInput
|
||||
label="Alamat"
|
||||
placeholder="Masukkan alamat lengkap"
|
||||
size="md"
|
||||
value={stateCreate.create.form.alamat}
|
||||
onChange={(e) => stateCreate.create.form.alamat = e.target.value}
|
||||
label="Alamat"
|
||||
placeholder="Masukkan alamat lengkap"
|
||||
size="md"
|
||||
value={stateCreate.create.form.alamat}
|
||||
onChange={(e) => stateCreate.create.form.alamat = e.target.value}
|
||||
styles={{
|
||||
label: { fontSize: '14px', lineHeight: 1.4, fontWeight: 500 },
|
||||
input: { fontSize: '16px', lineHeight: 1.5 },
|
||||
}}
|
||||
/>
|
||||
<Textarea
|
||||
label="Catatan Khusus (Opsional)"
|
||||
placeholder="Masukkan catatan jika ada"
|
||||
size="md"
|
||||
value={stateCreate.create.form.catatan}
|
||||
onChange={(e) => stateCreate.create.form.catatan = e.target.value}
|
||||
label="Catatan Khusus (Opsional)"
|
||||
placeholder="Masukkan catatan jika ada"
|
||||
size="md"
|
||||
value={stateCreate.create.form.catatan}
|
||||
onChange={(e) => stateCreate.create.form.catatan = e.target.value}
|
||||
styles={{
|
||||
label: { fontSize: '14px', lineHeight: 1.4, fontWeight: 500 },
|
||||
input: { fontSize: '16px', lineHeight: 1.5 },
|
||||
}}
|
||||
/>
|
||||
<Button size="md" radius="lg" bg={colors['blue-button']} onClick={handleSubmit}>
|
||||
Daftar Sekarang
|
||||
<Text fz={{ base: 'sm', md: 'md' }} fw={600} c="white">
|
||||
Daftar Sekarang
|
||||
</Text>
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreatePendaftaran;
|
||||
export default CreatePendaftaran;
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
import jadwalkegiatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/jadwalKegiatan';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Card, Divider, Group, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { Box, Button, Card, Divider, Group, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconChevronRight, IconClockHour4, IconMapPin } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
@@ -27,13 +27,13 @@ function JadwalKegiatanPage() {
|
||||
<Box>
|
||||
<Paper bg={colors['white-trans-1']} p="xl" radius="xl" shadow="md" h="auto" mih="100vh">
|
||||
<Stack gap="lg">
|
||||
<Text ta="center" fw={700} fz="32px" c={colors['blue-button']}>
|
||||
<Title ta="center" order={1} c={colors['blue-button']} fw={700}>
|
||||
Jadwal Kegiatan Warga
|
||||
</Text>
|
||||
</Title>
|
||||
<Divider size="sm" color={colors['blue-button']} />
|
||||
{state.findMany.data.length === 0 ? (
|
||||
<Box py="xl" ta="center">
|
||||
<Text fz="lg" c="dimmed">
|
||||
<Text fz={{ base: 'sm', sm: 'md' }} c="dimmed">
|
||||
Belum ada jadwal kegiatan yang tersedia
|
||||
</Text>
|
||||
</Box>
|
||||
@@ -48,11 +48,11 @@ function JadwalKegiatanPage() {
|
||||
style={{ backdropFilter: 'blur(8px)' }}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
<Group justify="space-between">
|
||||
<Text fw={700} fz="xl" c={colors['blue-button']}>
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<Title order={2} c={colors['blue-button']} fw={700} fz={{ base: 'md', sm: 'xl' }}>
|
||||
{item.informasijadwalkegiatan.name}
|
||||
</Text>
|
||||
<Text fw={600} fz="sm" c={colors['blue-button']}>
|
||||
</Title>
|
||||
<Text fw={600} fz={{ base: 'xs', sm: 'sm' }} c={colors['blue-button']} lh="1.4">
|
||||
{new Date(item.informasijadwalkegiatan.tanggal).toLocaleDateString('id-ID', {
|
||||
day: '2-digit',
|
||||
month: 'long',
|
||||
@@ -63,12 +63,16 @@ function JadwalKegiatanPage() {
|
||||
|
||||
<Group gap="xs">
|
||||
<IconClockHour4 size={18} />
|
||||
<Text fz="sm">{item.informasijadwalkegiatan.waktu}</Text>
|
||||
<Text fz={{ base: 'xs', sm: 'sm' }} lh="1.5">
|
||||
{item.informasijadwalkegiatan.waktu}
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
<Group gap="xs">
|
||||
<IconMapPin size={18} />
|
||||
<Text fz="sm">{item.informasijadwalkegiatan.lokasi}</Text>
|
||||
<Text fz={{ base: 'xs', sm: 'sm' }} lh="1.5">
|
||||
{item.informasijadwalkegiatan.lokasi}
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
<Divider my="sm" />
|
||||
@@ -98,4 +102,4 @@ function JadwalKegiatanPage() {
|
||||
);
|
||||
}
|
||||
|
||||
export default JadwalKegiatanPage;
|
||||
export default JadwalKegiatanPage;
|
||||
@@ -6,9 +6,7 @@ import { Box, Center, ColorSwatch, Flex, Paper, SimpleGrid, Skeleton, Stack, Tex
|
||||
import { useEffect, useState } from 'react';
|
||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||
|
||||
// import { useRouter } from 'next/navigation';
|
||||
import { useMediaQuery, useShallowEffect } from '@mantine/hooks';
|
||||
|
||||
import persentasekelahiran from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
@@ -17,7 +15,6 @@ import GrafikPenyakit from './grafik-penyakit/page';
|
||||
import JadwalKegiatan from './jadwal-kegiatan-page/page';
|
||||
import ArtikelKesehatanPage from './artikel-kesehatan-page/page';
|
||||
|
||||
|
||||
function Page() {
|
||||
type DataTahunan = {
|
||||
tahun: string;
|
||||
@@ -31,7 +28,6 @@ function Page() {
|
||||
}>;
|
||||
};
|
||||
|
||||
// Count occurrences per year
|
||||
const countByYear = (data: any[], dateField: string) => {
|
||||
const counts: Record<string, number> = {};
|
||||
data?.forEach(item => {
|
||||
@@ -43,28 +39,23 @@ function Page() {
|
||||
|
||||
const statePersentase = useProxy(persentasekelahiran);
|
||||
const [chartData, setChartData] = useState<DataTahunan[]>([]);
|
||||
const isTablet = useMediaQuery('(max-width: 1024px)');
|
||||
const isMobile = useMediaQuery('(max-width: 768px)');
|
||||
|
||||
|
||||
useShallowEffect(() => {
|
||||
statePersentase.kelahiran.findMany.load(1, 1000); // Load all kelahiran data
|
||||
statePersentase.kematian.findMany.load(1, 1000); // Load all kematian data
|
||||
statePersentase.kelahiran.findMany.load(1, 1000);
|
||||
statePersentase.kematian.findMany.load(1, 1000);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (statePersentase.kelahiran.findMany.data && statePersentase.kematian.findMany.data) {
|
||||
// Count kelahiran and kematian by year
|
||||
const kelahiranByYear = countByYear(statePersentase.kelahiran.findMany.data, 'tanggal');
|
||||
const kematianByYear = countByYear(statePersentase.kematian.findMany.data, 'tanggal');
|
||||
|
||||
// Get all unique years
|
||||
const allYears = new Set([
|
||||
...Object.keys(kelahiranByYear),
|
||||
...Object.keys(kematianByYear)
|
||||
]);
|
||||
|
||||
// Create data structure for the chart
|
||||
const dataByYear = Array.from(allYears).reduce<Record<string, DataTahunan>>((acc, year) => {
|
||||
acc[year] = {
|
||||
tahun: year,
|
||||
@@ -93,32 +84,44 @@ function Page() {
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
|
||||
|
||||
{/* Page Title */}
|
||||
<Title
|
||||
order={1}
|
||||
ta="center"
|
||||
c={colors['blue-button']}
|
||||
fw="bold"
|
||||
lh={1.2}
|
||||
>
|
||||
Data Kesehatan Masyarakat Puskesmas Darmasaba
|
||||
</Text>
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Stack gap={'lg'}>
|
||||
</Title>
|
||||
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Stack gap="lg">
|
||||
{/* Bar Chart Kematian Kelahiran */}
|
||||
<Box>
|
||||
<Paper p={"xl"} bg={colors['white-trans-1']}>
|
||||
<Paper p="xl" bg={colors['white-trans-1']}>
|
||||
<Box pb={30}>
|
||||
<Title order={2} mb="md">Data Kematian dan Kelahiran</Title>
|
||||
<Title order={2} mb="md" ta="center">
|
||||
Data Kematian dan Kelahiran
|
||||
</Title>
|
||||
|
||||
{chartData.length === 0 ? (
|
||||
<Text c="dimmed" ta="center" py="xl">
|
||||
<Text c="dimmed" ta="center" py="xl" size="md">
|
||||
Belum ada data yang tersedia untuk ditampilkan
|
||||
</Text>
|
||||
) : (
|
||||
<>
|
||||
{/* Main Chart */}
|
||||
<Center>
|
||||
<Box h={400}>
|
||||
<Box style={{
|
||||
width: isMobile ? '90vw' : isTablet ? '700px' : '800px',
|
||||
width: isMobile ? '90vw' : '800px',
|
||||
maxWidth: '100%',
|
||||
margin: '0 auto'
|
||||
}}>
|
||||
@@ -137,16 +140,21 @@ function Page() {
|
||||
</Center>
|
||||
</>
|
||||
)}
|
||||
<Flex pb={30} justify={'center'} gap={'xl'} align={'center'}>
|
||||
|
||||
<Flex pb={30} justify="center" gap="xl" align="center" wrap="wrap">
|
||||
<Box>
|
||||
<Flex gap={{ base: 0, md: 5 }} align={'center'}>
|
||||
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Angka Kematian</Text>
|
||||
<ColorSwatch color="#EF3E3E" size={30} />
|
||||
<Flex gap={{ base: 'xs', md: 'sm' }} align="center">
|
||||
<Text fw="bold" fz={{ base: 'sm', md: 'md' }}>
|
||||
Angka Kematian
|
||||
</Text>
|
||||
<ColorSwatch color="#EF3E3E" size={30} />
|
||||
</Flex>
|
||||
</Box>
|
||||
<Box>
|
||||
<Flex gap={{ base: 0, md: 5 }} align={'center'}>
|
||||
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Angka Kelahiran</Text>
|
||||
<Flex gap={{ base: 'xs', md: 'sm' }} align="center">
|
||||
<Text fw="bold" fz={{ base: 'sm', md: 'md' }}>
|
||||
Angka Kelahiran
|
||||
</Text>
|
||||
<ColorSwatch color="#3290CA" size={30} />
|
||||
</Flex>
|
||||
</Box>
|
||||
@@ -154,20 +162,13 @@ function Page() {
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
<GrafikPenyakit />
|
||||
{/* Artikel Kesehatan */}
|
||||
|
||||
<Box>
|
||||
<SimpleGrid
|
||||
cols={{
|
||||
base: 1,
|
||||
md: 3,
|
||||
}}
|
||||
>
|
||||
{/* Fasilitas Kesehatan */}
|
||||
<SimpleGrid cols={{ base: 1, md: 3 }}>
|
||||
<FasilitasKesehatan />
|
||||
{/* Jadwal Kegiatan */}
|
||||
<JadwalKegiatan />
|
||||
{/* Artikel Kesehatan */}
|
||||
<ArtikelKesehatanPage />
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
@@ -177,4 +178,4 @@ function Page() {
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
export default Page;
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
import infoWabahPenyakit from '@/app/admin/(dashboard)/_state/kesehatan/info-wabah-penyakit/infoWabahPenyakit';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { Box, Button, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
@@ -51,10 +51,15 @@ function DetailInfoWabahPenyakitUser() {
|
||||
shadow="sm"
|
||||
>
|
||||
<Stack gap="lg">
|
||||
{/* Judul */}
|
||||
<Text fz="xl" fw="bold" c={colors['blue-button']} ta="center">
|
||||
{/* Judul — H1 */}
|
||||
<Title
|
||||
order={1}
|
||||
fw="bold"
|
||||
c={colors['blue-button']}
|
||||
ta="center"
|
||||
>
|
||||
{data.name || 'Kontak Darurat'}
|
||||
</Text>
|
||||
</Title>
|
||||
|
||||
{/* Gambar */}
|
||||
{data.image?.link && (
|
||||
@@ -69,18 +74,26 @@ 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>
|
||||
<Stack gap={"xs"}>
|
||||
{/* Section Title — H2 */}
|
||||
<Title order={3} fw="bold" ta="left">
|
||||
Deskripsi
|
||||
</Title>
|
||||
<Box pl={20}>
|
||||
<Text
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={{ base: '1.5', md: '1.6' }}
|
||||
c="dark"
|
||||
ta="justify"
|
||||
dangerouslySetInnerHTML={{ __html: data.deskripsiLengkap || '-' }}
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default DetailInfoWabahPenyakitUser;
|
||||
export default DetailInfoWabahPenyakitUser;
|
||||
@@ -17,7 +17,8 @@ import {
|
||||
Skeleton,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput
|
||||
TextInput,
|
||||
Title
|
||||
} from '@mantine/core';
|
||||
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||
import { IconInfoCircle, IconSearch } from '@tabler/icons-react';
|
||||
@@ -30,7 +31,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(() => {
|
||||
@@ -53,15 +54,19 @@ function Page() {
|
||||
|
||||
<Grid align="center" px={{ base: 'md', md: 100 }}>
|
||||
<GridCol span={{ base: 12, md: 8 }}>
|
||||
<Text
|
||||
fz={{ base: '1.8rem', md: '2.8rem' }}
|
||||
<Title
|
||||
order={1}
|
||||
c={colors['blue-button']}
|
||||
fw="bold"
|
||||
lh={1.2}
|
||||
lh={{ base: 1.2, md: 1.2 }}
|
||||
>
|
||||
Informasi Wabah & Penyakit
|
||||
</Text>
|
||||
<Text fz="md" mt={4}>
|
||||
</Title>
|
||||
<Text
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={{ base: 1.5, md: 1.5 }}
|
||||
mt={4}
|
||||
>
|
||||
Dapatkan informasi terbaru mengenai wabah dan penyakit yang sedang
|
||||
diawasi.
|
||||
</Text>
|
||||
@@ -84,9 +89,9 @@ function Page() {
|
||||
<Center py="6xl">
|
||||
<Stack align="center" gap="sm">
|
||||
<IconInfoCircle size={50} color={colors['blue-button']} />
|
||||
<Text fz="lg" fw={500} >
|
||||
<Title order={2} fz={{ base: 'md', md: 'lg' }} fw={500}>
|
||||
Tidak ada data yang cocok dengan pencarian Anda.
|
||||
</Text>
|
||||
</Title>
|
||||
</Stack>
|
||||
</Center>
|
||||
) : (
|
||||
@@ -131,15 +136,24 @@ function Page() {
|
||||
|
||||
{/* Judul dan badge */}
|
||||
<Group justify="space-between" mt="sm">
|
||||
<Text fw={700} fz="lg" c={colors['blue-button']}>
|
||||
<Title
|
||||
order={3}
|
||||
c={colors['blue-button']}
|
||||
fw={700}
|
||||
fz={{ base: 'sm', md: 'lg' }}
|
||||
>
|
||||
{v.name}
|
||||
</Text>
|
||||
</Title>
|
||||
<Badge color="blue" variant="light" radius="sm">
|
||||
Wabah
|
||||
</Badge>
|
||||
</Group>
|
||||
|
||||
<Text fz="sm" c="dimmed">
|
||||
<Text
|
||||
fz={{ base: 'xs', md: 'sm' }}
|
||||
c="dimmed"
|
||||
lh={{ base: 1.4, md: 1.4 }}
|
||||
>
|
||||
Diposting:{' '}
|
||||
{new Date(v.createdAt).toLocaleDateString('id-ID', {
|
||||
day: '2-digit',
|
||||
@@ -153,8 +167,8 @@ function Page() {
|
||||
{/* Bagian deskripsi dan tombol */}
|
||||
<Box style={{ display: 'flex', flexDirection: 'column', flexGrow: 1 }}>
|
||||
<Text
|
||||
fz="sm"
|
||||
lh={1.5}
|
||||
fz={{ base: 'xs', md: 'sm' }}
|
||||
lh={{ base: 1.5, md: 1.5 }}
|
||||
lineClamp={3}
|
||||
dangerouslySetInnerHTML={{ __html: v.deskripsiSingkat }}
|
||||
style={{ flexGrow: 1 }}
|
||||
@@ -174,14 +188,11 @@ function Page() {
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
))}
|
||||
</SimpleGrid>
|
||||
|
||||
)}
|
||||
</Box>
|
||||
|
||||
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
@@ -192,9 +203,8 @@ function Page() {
|
||||
mt="lg"
|
||||
/>
|
||||
</Center>
|
||||
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
export default Page;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user