Compare commits

...

7 Commits

107 changed files with 5854 additions and 2701 deletions

View File

@@ -0,0 +1,10 @@
[
{
"id": "cme8bt5o5000007lb9xp11unb",
"name": "Laki-laki"
},
{
"id": "cme8btctl000107lbh2hocgg8",
"name": "Perempuan"
}
]

View File

@@ -0,0 +1,18 @@
[
{
"id": "cme8buup6000207lb54q9b0az",
"name": "Sangat Baik"
},
{
"id": "cme8bv15o000307lbft9b0vzy",
"name": "Baik"
},
{
"id": "cme8bvjvu000507lbgfsveog6",
"name": "Kurang Baik"
},
{
"id": "cme8bvvm6000607lbh6rn2ubm",
"name": "Sangat Kurang Baik"
}
]

View File

@@ -0,0 +1,14 @@
[
{
"id": "cme8bwgwu000707lbawc6fz3a",
"name": "Muda"
},
{
"id": "cme8hnx09000b07jl3ipifb1k",
"name": "Dewasa"
},
{
"id": "cme8ho7dv000c07jlc7lr4b4w",
"name": "Lansia"
}
]

View File

@@ -50,23 +50,22 @@ model AppMenuChild {
// ========================================= FILE STORAGE ========================================= //
model FileStorage {
id String @id @default(cuid())
name String @unique
realName String
path String
mimeType String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
isActive Boolean @default(true)
link String
category String // "image" / "document" / "other"
Berita Berita[]
PotensiDesa PotensiDesa[]
Posyandu Posyandu[]
StrukturPPID StrukturPPID[]
GalleryFoto GalleryFoto[]
id String @id @default(cuid())
name String @unique
realName String
path String
mimeType String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
isActive Boolean @default(true)
link String
category String // "image" / "document" / "other"
Berita Berita[]
PotensiDesa PotensiDesa[]
Posyandu Posyandu[]
StrukturPPID StrukturPPID[]
GalleryFoto GalleryFoto[]
Pelapor Pelapor[]
Penghargaan Penghargaan[]
ProfileDesaImage ProfileDesaImage[]
@@ -98,10 +97,9 @@ model FileStorage {
APBDesImage APBDes[] @relation("APBDesImage")
APBDesFile APBDes[] @relation("APBDesFile")
PrestasiDesa PrestasiDesa[]
DataPerpustakaan DataPerpustakaan[]
PegawaiPPID PegawaiPPID[]
DataPerpustakaan DataPerpustakaan[]
PegawaiPPID PegawaiPPID[]
PerbekelDariMasaKeMasa PerbekelDariMasaKeMasa[]
}
//========================================= MENU LANDING PAGE ========================================= //
@@ -221,6 +219,53 @@ model KategoriPrestasiDesa {
PrestasiDesa PrestasiDesa[]
}
//========================================= INDEKS KEPUASAAN MASYARAKAT ========================================= //
model Responden {
id String @id @default(cuid())
name String @unique
tanggal DateTime // misal: 2025-05-01
jenisKelamin JenisKelaminResponden @relation(fields: [jenisKelaminId], references: [id])
jenisKelaminId String
rating PilihanRatingResponden @relation(fields: [ratingId], references: [id])
ratingId String
kelompokUmur UmurResponden @relation(fields: [kelompokUmurId], references: [id])
kelompokUmurId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model JenisKelaminResponden {
id String @id @default(cuid())
name String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
Responden Responden[]
}
model PilihanRatingResponden {
id String @id @default(cuid())
name String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
Responden Responden[]
}
model UmurResponden {
id String @id @default(cuid())
name String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
Responden Responden[]
}
//========================================= MENU PPID ========================================= //
//========================================= STRUKTUR PPID ========================================= //
@@ -508,6 +553,19 @@ model ProfilPerbekel {
isActive Boolean @default(true)
}
model PerbekelDariMasaKeMasa {
id String @id @default(cuid())
nama String @db.Text
periode String @db.Text
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
daerah String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= BERITA ========================================= //
model Berita {
id String @id @default(cuid())

View File

@@ -16,6 +16,9 @@ import potensi from "./data/list-potensi.json";
import dasarHukumPPID from "./data/ppid/dasar-hukum-ppid/dasarhukumPPID.json";
import profilePPID from "./data/ppid/profile-ppid/profilePPid.json";
import visiMisiPPID from "./data/ppid/visi-misi-ppid/visimisiPPID.json";
import jenisKelamin from "./data/ppid/ikm/jenis-kelamin/jenis-kelamin.json";
import pilihanRatingResponden from "./data/ppid/ikm/pilihan-rating-responden/rating-responden.json";
import umurResponden from "./data/ppid/ikm/umur-responden/umur-responden.json";
import pelayananPerizinanBerusaha from "./data/desa/layanan/pelayananPerizinanBerusaha.json";
import pelayananPendudukNonPermanen from "./data/desa/layanan/pelayanaPendudukNonPermanen.json";
import sejarahDesa from "./data/desa/profile/sejarah_desa.json";
@@ -546,6 +549,54 @@ import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSak
}
console.log("visi misi PPID success ...");
for (const j of jenisKelamin) {
await prisma.jenisKelaminResponden.upsert({
where: {
id: j.id,
},
update: {
name: j.name,
},
create: {
id: j.id,
name: j.name,
},
});
}
console.log("jenis kelamin responden success ...");
for (const r of pilihanRatingResponden) {
await prisma.pilihanRatingResponden.upsert({
where: {
id: r.id,
},
update: {
name: r.name,
},
create: {
id: r.id,
name: r.name,
},
});
}
console.log("pilihan rating responden success ...");
for (const u of umurResponden) {
await prisma.umurResponden.upsert({
where: {
id: u.id,
},
update: {
name: u.name,
},
create: {
id: u.id,
name: u.name,
},
});
}
console.log("umur responden success ...");
for (const v of dasarHukumPPID) {
await prisma.dasarHukumPPID.upsert({
where: {

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -68,10 +69,34 @@ const foto = proxy({
};
}>[]
| null,
async load() {
const res = await ApiFetch.api.desa.gallery.foto["find-many"].get();
if (res.status === 200) {
foto.findMany.data = res.data?.data ?? [];
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
foto.findMany.loading = true; // ✅ Akses langsung via nama path
foto.findMany.page = page;
foto.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.desa.gallery.foto["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
foto.findMany.data = res.data.data ?? [];
foto.findMany.totalPages = res.data.totalPages ?? 1;
} else {
foto.findMany.data = [];
foto.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch foto paginated:", err);
foto.findMany.data = [];
foto.findMany.totalPages = 1;
} finally {
foto.findMany.loading = false;
}
},
},
@@ -215,6 +240,28 @@ const foto = proxy({
foto.update.form = { ...defaultFormFoto };
},
},
findRecent: {
data: [] as Prisma.GalleryFotoGetPayload<{
include: {
imageGalleryFoto: true;
};
}>[],
loading: false,
async load() {
try {
this.loading = true;
const res = await ApiFetch.api.desa.gallery.foto["find-recent"].get();
if (res.status === 200 && res.data?.success) {
this.data = res.data.data ?? [];
}
} catch (error) {
console.error("Gagal fetch foto recent:", error);
} finally {
this.loading = false;
}
},
},
});
const video = proxy({
@@ -257,10 +304,34 @@ const video = proxy({
};
}>[]
| null,
async load() {
const res = await ApiFetch.api.desa.gallery.video["find-many"].get();
if (res.status === 200) {
video.findMany.data = res.data?.data ?? [];
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
video.findMany.loading = true; // ✅ Akses langsung via nama path
video.findMany.page = page;
video.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.desa.gallery.video["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
video.findMany.data = res.data.data ?? [];
video.findMany.totalPages = res.data.totalPages ?? 1;
} else {
video.findMany.data = [];
video.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch video paginated:", err);
video.findMany.data = [];
video.findMany.totalPages = 1;
} finally {
video.findMany.loading = false;
}
},
},

View File

@@ -1,7 +1,9 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
import { Prisma } from "@prisma/client";
import ApiFetch from "@/lib/api-fetch";
// ========================================= SEJARAH DESA ========================================= //
const sejarahDesaForm = z.object({
@@ -106,16 +108,13 @@ const sejarahDesa = proxy({
this.error = null;
try {
const response = await fetch(
`/api/desa/profile/sejarah/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
}
);
const response = await fetch(`/api/desa/profile/sejarah/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
@@ -409,16 +408,13 @@ const lambangDesa = proxy({
this.error = null;
try {
const response = await fetch(
`/api/desa/profile/lambang/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
}
);
const response = await fetch(`/api/desa/profile/lambang/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
@@ -588,14 +584,11 @@ const maskotDesa = proxy({
this.error = null;
try {
const response = await fetch(
`/api/desa/profile/maskot/${this.id}`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(this.form),
}
);
const response = await fetch(`/api/desa/profile/maskot/${this.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(this.form),
});
const result = await response.json();
@@ -818,12 +811,247 @@ const profilPerbekel = proxy({
},
});
//========================================= MANTAN PERBEKEL ========================================= //
const mantanPerbekelForm = z.object({
nama: z.string().min(3, "Nama minimal 3 karakter"),
daerah: z.string().min(3, "Daerah minimal 3 karakter"),
periode: z.string().min(3, "Periode minimal 3 karakter"),
imageId: z.string().min(1, "Gambar wajib dipilih"),
});
const mantanPerbekelDefaultForm = {
nama: "",
daerah: "",
periode: "",
imageId: "",
};
const mantanPerbekel = proxy({
create: {
form: { ...mantanPerbekelDefaultForm },
loading: false,
async create() {
const cek = mantanPerbekelForm.safeParse(mantanPerbekel.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
mantanPerbekel.create.loading = true;
const res = await ApiFetch.api.desa.mantanperbekel["create"].post(
mantanPerbekel.create.form
);
if (res.status === 200) {
mantanPerbekel.findMany.load();
return toast.success("Foto berhasil disimpan!");
}
return toast.error("Gagal menyimpan foto");
} catch (error) {
console.log((error as Error).message);
} finally {
mantanPerbekel.create.loading = false;
}
},
resetForm() {
mantanPerbekel.create.form = { ...mantanPerbekelDefaultForm };
},
},
findMany: {
data: null as
| Prisma.PerbekelDariMasaKeMasaGetPayload<{
include: {
image: true;
};
}>[]
| null,
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
mantanPerbekel.findMany.loading = true; // ✅ Akses langsung via nama path
mantanPerbekel.findMany.page = page;
mantanPerbekel.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.desa.mantanperbekel["findMany"].get({
query,
});
if (res.status === 200 && res.data?.success) {
mantanPerbekel.findMany.data = res.data.data ?? [];
mantanPerbekel.findMany.totalPages = res.data.totalPages ?? 1;
} else {
mantanPerbekel.findMany.data = [];
mantanPerbekel.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch mantan perbekel paginated:", err);
mantanPerbekel.findMany.data = [];
mantanPerbekel.findMany.totalPages = 1;
} finally {
mantanPerbekel.findMany.loading = false;
}
},
},
findUnique: {
data: null as Prisma.PerbekelDariMasaKeMasaGetPayload<{
include: {
image: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/desa/mantanperbekel/${id}`);
if (res.ok) {
const data = await res.json();
mantanPerbekel.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch mantan perbekel:", res.statusText);
mantanPerbekel.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching mantan perbekel:", error);
mantanPerbekel.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
mantanPerbekel.delete.loading = true;
const response = await fetch(`/api/desa/mantanperbekel/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok) {
toast.success(result.message || "Mantan perbekel berhasil dihapus");
await mantanPerbekel.findMany.load(); // refresh list
} else {
toast.error(result.message || "Gagal menghapus mantan perbekel");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus mantan perbekel");
} finally {
mantanPerbekel.delete.loading = false;
}
},
},
update: {
id: "",
form: { ...mantanPerbekelDefaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/desa/mantanperbekel/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
nama: data.nama,
daerah: data.daerah,
periode: data.periode,
imageId: data.imageId || "",
};
return data;
} else {
throw new Error(result.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading foto:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = mantanPerbekelForm.safeParse(mantanPerbekel.update.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
mantanPerbekel.update.loading = true;
const response = await fetch(`/api/desa/mantanperbekel/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama: this.form.nama,
daerah: this.form.daerah,
periode: this.form.periode,
imageId: this.form.imageId,
}),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success(result.message || "Mantan perbekel berhasil diupdate");
await mantanPerbekel.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal mengupdate mantan perbekel");
}
} catch (error) {
console.error("Error updating mantan perbekel:", error);
toast.error(
error instanceof Error
? error.message
: "Gagal mengupdate mantan perbekel"
);
return false;
} finally {
mantanPerbekel.update.loading = false;
}
},
reset() {
mantanPerbekel.update.id = "";
mantanPerbekel.update.form = { ...mantanPerbekelDefaultForm };
},
},
});
const stateProfileDesa = proxy({
lambangDesa,
maskotDesa,
profilPerbekel,
visiMisiDesa,
sejarahDesa,
mantanPerbekel,
});
export default stateProfileDesa;

View File

@@ -0,0 +1,828 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
// Template form responden
const templateResponden = z.object({
name: z.string().min(1, "Nama harus diisi"),
tanggal: z.string().min(1, "Tanggal harus diisi"),
jenisKelaminId: z.string().min(1, "Jenis kelamin harus diisi"),
ratingId: z.string().min(1, "Rating harus diisi"),
kelompokUmurId: z.string().min(1, "Kelompok umur harus diisi"),
});
const defaultFormResponden = {
name: "",
tanggal: "",
jenisKelaminId: "",
ratingId: "",
kelompokUmurId: "",
};
const responden = proxy({
create: {
form: { ...defaultFormResponden },
loading: false,
async create() {
const cek = templateResponden.safeParse(responden.create.form);
if (!cek.success) {
const err = cek.error.issues.map((i) => i.message).join("\n");
toast.error(err);
return;
}
try {
responden.create.loading = true;
const res = await ApiFetch.api.landingpage.responden["create"].post(
responden.create.form
);
if (res.status === 200) {
toast.success("Responden berhasil ditambahkan");
await responden.findMany.load();
} else {
toast.error(res.data?.message ?? "Gagal tambah responden");
}
} catch (error) {
console.error("Gagal create:", error);
toast.error("Terjadi kesalahan saat menambahkan responden");
} finally {
responden.create.loading = false;
}
},
},
findMany: {
data: null as any[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
load: async (page = 1, limit = 10) => {
// Change to arrow function
responden.findMany.loading = true; // Use the full path to access the property
responden.findMany.page = page;
try {
const res = await ApiFetch.api.landingpage.responden["findMany"].get({
query: { page, limit },
});
if (res.status === 200 && res.data?.success) {
responden.findMany.data = res.data.data || [];
responden.findMany.total = res.data.total || 0;
responden.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error("Failed to load responden:", res.data?.message);
responden.findMany.data = [];
responden.findMany.total = 0;
responden.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading responden:", error);
responden.findMany.data = [];
responden.findMany.total = 0;
responden.findMany.totalPages = 1;
} finally {
responden.findMany.loading = false;
}
},
},
findUnique: {
data: null as Prisma.RespondenGetPayload<{
include: {
jenisKelamin: true;
rating: true;
kelompokUmur: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/landingpage/responden/${id}`);
if (res.ok) {
const data = await res.json();
responden.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
responden.findUnique.data = null;
}
} catch (error) {
console.error("Error loading responden:", error);
responden.findUnique.data = null;
}
},
},
update: {
id: "",
form: { ...defaultFormResponden },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
responden.update.loading = true;
const response = await fetch(`/api/landingpage/responden/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
name: data.name,
tanggal: data.tanggal,
jenisKelaminId: data.jenisKelaminId,
ratingId: data.ratingId,
kelompokUmurId: data.kelompokUmurId,
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading responden:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
} finally {
responden.update.loading = false;
}
},
async submit() {
const id = this.id;
if (!id) {
toast.warn("ID tidak valid");
return null;
}
const cek = templateResponden.safeParse(this.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return null;
}
this.loading = true;
try {
const response = await fetch(`/api/landingpage/responden/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
});
const result = await response.json();
if (!response.ok || !result?.success) {
throw new Error(result?.message || "Gagal update data");
}
toast.success("Berhasil update data!");
await responden.findMany.load();
return result.data;
} catch (error) {
console.error("Error update data:", error);
toast.error("Gagal update data responden");
} finally {
this.loading = false;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
responden.delete.loading = true;
const response = await fetch(`/api/landingpage/responden/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "responden berhasil dihapus");
await responden.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus responden");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus responden");
} finally {
responden.delete.loading = false;
}
},
},
});
// Template form jenis kelamin responden
const templateJenisKelaminResponden = z.object({
name: z.string().min(1, "Nama harus diisi"),
});
const defaultFormJenisKelaminResponden = {
name: "",
};
const jenisKelaminResponden = proxy({
create: {
form: { ...defaultFormJenisKelaminResponden },
loading: false,
async create() {
const cek = templateJenisKelaminResponden.safeParse(
jenisKelaminResponden.create.form
);
if (!cek.success) {
const err = cek.error.issues.map((i) => i.message).join("\n");
toast.error(err);
return;
}
jenisKelaminResponden.create.loading = true;
try {
jenisKelaminResponden.create.loading = true;
const res = await ApiFetch.api.landingpage.jeniskelaminresponden[
"create"
].post(jenisKelaminResponden.create.form);
if (res.status === 200) {
toast.success("Jenis kelamin responden berhasil ditambahkan");
await jenisKelaminResponden.findMany.load();
} else {
toast.error(
res.data?.message ?? "Gagal tambah jenis kelamin responden"
);
}
} catch (error) {
console.error("Gagal create:", error);
toast.error(
"Terjadi kesalahan saat menambahkan jenis kelamin responden"
);
} finally {
jenisKelaminResponden.create.loading = false;
}
},
},
findMany: {
data: null as any[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
load: async (page = 1, limit = 10) => {
// Change to arrow function
jenisKelaminResponden.findMany.loading = true; // Use the full path to access the property
jenisKelaminResponden.findMany.page = page;
try {
const res = await ApiFetch.api.landingpage.jeniskelaminresponden[
"findMany"
].get({
query: { page, limit },
});
if (res.status === 200 && res.data?.success) {
jenisKelaminResponden.findMany.data = res.data.data || [];
jenisKelaminResponden.findMany.total = res.data.total || 0;
jenisKelaminResponden.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error(
"Failed to load jenis kelamin responden:",
res.data?.message
);
jenisKelaminResponden.findMany.data = [];
jenisKelaminResponden.findMany.total = 0;
jenisKelaminResponden.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading jenis kelamin responden:", error);
jenisKelaminResponden.findMany.data = [];
jenisKelaminResponden.findMany.total = 0;
jenisKelaminResponden.findMany.totalPages = 1;
} finally {
jenisKelaminResponden.findMany.loading = false;
}
},
},
findUnique: {
data: null as Prisma.JenisKelaminRespondenGetPayload<{
omit: {
isActive: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/landingpage/jeniskelaminresponden/${id}`);
if (res.ok) {
const data = await res.json();
jenisKelaminResponden.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
jenisKelaminResponden.findUnique.data = null;
}
} catch (error) {
console.error("Error loading jenis kelamin responden:", error);
jenisKelaminResponden.findUnique.data = null;
}
},
},
update: {
id: "",
form: { ...defaultFormJenisKelaminResponden },
loading: false,
async byId() {
// Method implementation if needed
},
async submit() {
const id = this.id;
if (!id) {
toast.warn("ID tidak valid");
return null;
}
const cek = templateJenisKelaminResponden.safeParse(this.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return null;
}
this.loading = true;
try {
const response = await fetch(
`/api/landingpage/jeniskelaminresponden/${id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
}
);
const result = await response.json();
if (!response.ok || !result?.success) {
throw new Error(result?.message || "Gagal update data");
}
toast.success("Berhasil update data!");
await jenisKelaminResponden.findMany.load();
return result.data;
} catch (error) {
console.error("Error update data:", error);
toast.error("Gagal update data jenis kelamin responden");
} finally {
this.loading = false;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
jenisKelaminResponden.delete.loading = true;
const response = await fetch(
`/api/landingpage/jeniskelaminresponden/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "jenis kelamin responden berhasil dihapus"
);
await jenisKelaminResponden.findMany.load(); // refresh list
} else {
toast.error(
result?.message || "Gagal menghapus jenis kelamin responden"
);
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus jenis kelamin responden");
} finally {
jenisKelaminResponden.delete.loading = false;
}
},
},
});
// Template form pilihan rating responden
const templatePilihanRatingResponden = z.object({
name: z.string().min(1, "Nama harus diisi"),
});
const defaultFormPilihanRatingResponden = {
name: "",
};
const pilihanRatingResponden = proxy({
create: {
form: { ...defaultFormPilihanRatingResponden },
loading: false,
async create() {
const cek = templatePilihanRatingResponden.safeParse(
pilihanRatingResponden.create.form
);
if (!cek.success) {
const err = cek.error.issues.map((i) => i.message).join("\n");
toast.error(err);
return;
}
pilihanRatingResponden.create.loading = true;
try {
pilihanRatingResponden.create.loading = true;
const res = await ApiFetch.api.landingpage.pilihanratingresponden[
"create"
].post(pilihanRatingResponden.create.form);
if (res.status === 200) {
toast.success("Jenis kelamin responden berhasil ditambahkan");
await pilihanRatingResponden.findMany.load();
} else {
toast.error(
res.data?.message ?? "Gagal tambah jenis kelamin responden"
);
}
} catch (error) {
console.error("Gagal create:", error);
toast.error(
"Terjadi kesalahan saat menambahkan jenis kelamin responden"
);
} finally {
pilihanRatingResponden.create.loading = false;
}
},
},
findMany: {
data: null as any[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
load: async (page = 1, limit = 10) => {
// Change to arrow function
pilihanRatingResponden.findMany.loading = true; // Use the full path to access the property
pilihanRatingResponden.findMany.page = page;
try {
const res = await ApiFetch.api.landingpage.pilihanratingresponden[
"findMany"
].get({
query: { page, limit },
});
if (res.status === 200 && res.data?.success) {
pilihanRatingResponden.findMany.data = res.data.data || [];
pilihanRatingResponden.findMany.total = res.data.total || 0;
pilihanRatingResponden.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error(
"Failed to load pilihan rating responden:",
res.data?.message
);
pilihanRatingResponden.findMany.data = [];
pilihanRatingResponden.findMany.total = 0;
pilihanRatingResponden.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading pilihan rating responden:", error);
pilihanRatingResponden.findMany.data = [];
pilihanRatingResponden.findMany.total = 0;
pilihanRatingResponden.findMany.totalPages = 1;
} finally {
pilihanRatingResponden.findMany.loading = false;
}
},
},
findUnique: {
data: null as Prisma.PilihanRatingRespondenGetPayload<{
omit: {
isActive: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/landingpage/pilihanratingresponden/${id}`);
if (res.ok) {
const data = await res.json();
pilihanRatingResponden.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
pilihanRatingResponden.findUnique.data = null;
}
} catch (error) {
console.error("Error loading pilihan rating responden:", error);
pilihanRatingResponden.findUnique.data = null;
}
},
},
update: {
id: "",
form: { ...defaultFormPilihanRatingResponden },
loading: false,
async byId() {
// Method implementation if needed
},
async submit() {
const id = this.id;
if (!id) {
toast.warn("ID tidak valid");
return null;
}
const cek = templatePilihanRatingResponden.safeParse(this.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return null;
}
this.loading = true;
try {
const response = await fetch(
`/api/landingpage/pilihanratingresponden/${id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
}
);
const result = await response.json();
if (!response.ok || !result?.success) {
throw new Error(result?.message || "Gagal update data");
}
toast.success("Berhasil update data!");
await pilihanRatingResponden.findMany.load();
return result.data;
} catch (error) {
console.error("Error update data:", error);
toast.error("Gagal update data pilihan rating responden");
} finally {
this.loading = false;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
pilihanRatingResponden.delete.loading = true;
const response = await fetch(
`/api/landingpage/pilihanratingresponden/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "pilihan rating responden berhasil dihapus"
);
await pilihanRatingResponden.findMany.load(); // refresh list
} else {
toast.error(
result?.message || "Gagal menghapus pilihan rating responden"
);
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus pilihan rating responden");
} finally {
pilihanRatingResponden.delete.loading = false;
}
},
},
});
// Template form kelompok umur responden
const templateKelompokUmurResponden = z.object({
name: z.string().min(1, "Nama harus diisi"),
});
const defaultFormKelompokUmurResponden = {
name: "",
};
const kelompokUmurResponden = proxy({
create: {
form: { ...defaultFormKelompokUmurResponden },
loading: false,
async create() {
const cek = templateKelompokUmurResponden.safeParse(
kelompokUmurResponden.create.form
);
if (!cek.success) {
const err = cek.error.issues.map((i) => i.message).join("\n");
toast.error(err);
return;
}
kelompokUmurResponden.create.loading = true;
try {
kelompokUmurResponden.create.loading = true;
const res = await ApiFetch.api.landingpage.umurresponden["create"].post(
kelompokUmurResponden.create.form
);
if (res.status === 200) {
toast.success("Kelompok umur responden berhasil ditambahkan");
await kelompokUmurResponden.findMany.load();
} else {
toast.error(
res.data?.message ?? "Gagal tambah kelompok umur responden"
);
}
} catch (error) {
console.error("Gagal create:", error);
toast.error(
"Terjadi kesalahan saat menambahkan kelompok umur responden"
);
} finally {
kelompokUmurResponden.create.loading = false;
}
},
},
findMany: {
data: null as any[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
load: async (page = 1, limit = 10) => {
// Change to arrow function
kelompokUmurResponden.findMany.loading = true; // Use the full path to access the property
kelompokUmurResponden.findMany.page = page;
try {
const res = await ApiFetch.api.landingpage.umurresponden[
"findMany"
].get({
query: { page, limit },
});
if (res.status === 200 && res.data?.success) {
kelompokUmurResponden.findMany.data = res.data.data || [];
kelompokUmurResponden.findMany.total = res.data.total || 0;
kelompokUmurResponden.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error(
"Failed to load kelompok umur responden:",
res.data?.message
);
kelompokUmurResponden.findMany.data = [];
kelompokUmurResponden.findMany.total = 0;
kelompokUmurResponden.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading kelompok umur responden:", error);
kelompokUmurResponden.findMany.data = [];
kelompokUmurResponden.findMany.total = 0;
kelompokUmurResponden.findMany.totalPages = 1;
} finally {
kelompokUmurResponden.findMany.loading = false;
}
},
},
findUnique: {
data: null as Prisma.UmurRespondenGetPayload<{
omit: {
isActive: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/landingpage/umurresponden/${id}`);
if (res.ok) {
const data = await res.json();
kelompokUmurResponden.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
kelompokUmurResponden.findUnique.data = null;
}
} catch (error) {
console.error("Error loading kelompok umur responden:", error);
kelompokUmurResponden.findUnique.data = null;
}
},
},
update: {
id: "",
form: { ...defaultFormKelompokUmurResponden },
loading: false,
async byId() {
// Method implementation if needed
},
async submit() {
const id = this.id;
if (!id) {
toast.warn("ID tidak valid");
return null;
}
const cek = templateKelompokUmurResponden.safeParse(this.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return null;
}
this.loading = true;
try {
const response = await fetch(`/api/landingpage/umurresponden/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
});
const result = await response.json();
if (!response.ok || !result?.success) {
throw new Error(result?.message || "Gagal update data");
}
toast.success("Berhasil update data!");
await kelompokUmurResponden.findMany.load();
return result.data;
} catch (error) {
console.error("Error update data:", error);
toast.error("Gagal update data kelompok umur responden");
} finally {
this.loading = false;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
kelompokUmurResponden.delete.loading = true;
const response = await fetch(
`/api/landingpage/umurresponden/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "kelompok umur responden berhasil dihapus"
);
await kelompokUmurResponden.findMany.load(); // refresh list
} else {
toast.error(
result?.message || "Gagal menghapus kelompok umur responden"
);
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus kelompok umur responden");
} finally {
kelompokUmurResponden.delete.loading = false;
}
},
},
});
const indeksKepuasanState = proxy({
responden,
kelompokUmurResponden,
jenisKelaminResponden,
pilihanRatingResponden
})
export default indeksKepuasanState

View File

@@ -8,7 +8,7 @@ import { z } from "zod";
const templateGrafikJenisKelamin = z.object({
laki: z.string().min(1, "Data laki-laki harus diisi"),
perempuan: z.string().min(1, "Data perempuan harus diisi"),
});
});
const defaultForm = {
laki: "",
@@ -17,10 +17,12 @@ const defaultForm = {
const grafikBerdasarkanJenisKelamin = proxy({
create: {
form: {...defaultForm},
form: { ...defaultForm },
loading: false,
async create(){
const cek = templateGrafikJenisKelamin.safeParse(grafikBerdasarkanJenisKelamin.create.form);
async create() {
const cek = templateGrafikJenisKelamin.safeParse(
grafikBerdasarkanJenisKelamin.create.form
);
if (!cek.success) {
const err = cek.error.issues.map((i) => i.message).join("\n");
toast.error(err);
@@ -33,14 +35,20 @@ const grafikBerdasarkanJenisKelamin = proxy({
"create"
].post(grafikBerdasarkanJenisKelamin.create.form);
if (res.status === 200) {
toast.success("Grafik berdasarkan jenis kelamin berhasil ditambahkan");
toast.success(
"Grafik berdasarkan jenis kelamin berhasil ditambahkan"
);
await grafikBerdasarkanJenisKelamin.findMany.load();
} else {
toast.error(res.data?.message ?? "Gagal tambah grafik berdasarkan jenis kelamin");
toast.error(
res.data?.message ?? "Gagal tambah grafik berdasarkan jenis kelamin"
);
}
} catch (error) {
console.error("Gagal create:", error);
toast.error("Terjadi kesalahan saat menambahkan grafik berdasarkan jenis kelamin");
toast.error(
"Terjadi kesalahan saat menambahkan grafik berdasarkan jenis kelamin"
);
} finally {
grafikBerdasarkanJenisKelamin.create.loading = false;
}
@@ -52,8 +60,9 @@ const grafikBerdasarkanJenisKelamin = proxy({
totalPages: 1,
total: 0,
loading: false,
load: async (page = 1, limit = 10) => { // Change to arrow function
grafikBerdasarkanJenisKelamin.findMany.loading = true; // Use the full path to access the property
load: async (page = 1, limit = 10) => {
// Change to arrow function
grafikBerdasarkanJenisKelamin.findMany.loading = true; // Use the full path to access the property
grafikBerdasarkanJenisKelamin.findMany.page = page;
try {
const res = await ApiFetch.api.ppid.grafikberdasarkanjeniskelamin[
@@ -61,13 +70,17 @@ const grafikBerdasarkanJenisKelamin = proxy({
].get({
query: { page, limit },
});
if (res.status === 200 && res.data?.success) {
grafikBerdasarkanJenisKelamin.findMany.data = res.data.data || [];
grafikBerdasarkanJenisKelamin.findMany.total = res.data.total || 0;
grafikBerdasarkanJenisKelamin.findMany.totalPages = res.data.totalPages || 1;
grafikBerdasarkanJenisKelamin.findMany.totalPages =
res.data.totalPages || 1;
} else {
console.error("Failed to load grafik berdasarkan jenis kelamin:", res.data?.message);
console.error(
"Failed to load grafik berdasarkan jenis kelamin:",
res.data?.message
);
grafikBerdasarkanJenisKelamin.findMany.data = [];
grafikBerdasarkanJenisKelamin.findMany.total = 0;
grafikBerdasarkanJenisKelamin.findMany.totalPages = 1;
@@ -106,7 +119,7 @@ const grafikBerdasarkanJenisKelamin = proxy({
},
update: {
id: "",
form: {...defaultForm},
form: { ...defaultForm },
loading: false,
async byId() {
// Method implementation if needed
@@ -119,20 +132,24 @@ const grafikBerdasarkanJenisKelamin = proxy({
}
const cek = templateGrafikJenisKelamin.safeParse(this.form);
if (!cek.success) {
const err = `[${cek.error.issues.map((v) => `${v.path.join(".")}`).join("\n")}] required`;
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return null;
}
this.loading = true;
try {
const response = await fetch(
`/api/ppid/grafikberdasarkanjeniskelamin/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
});
`/api/ppid/grafikberdasarkanjeniskelamin/${id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
}
);
const result = await response.json();
if (!response.ok || !result?.success) {
throw new Error(result?.message || "Gagal update data");
@@ -156,29 +173,40 @@ const grafikBerdasarkanJenisKelamin = proxy({
try {
grafikBerdasarkanJenisKelamin.delete.loading = true;
const response = await fetch(`/api/ppid/grafikberdasarkanjeniskelamin/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const response = await fetch(
`/api/ppid/grafikberdasarkanjeniskelamin/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Grafik berdasarkan jenis kelamin berhasil dihapus");
toast.success(
result.message ||
"Grafik berdasarkan jenis kelamin berhasil dihapus"
);
await grafikBerdasarkanJenisKelamin.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus grafik berdasarkan jenis kelamin");
toast.error(
result?.message ||
"Gagal menghapus grafik berdasarkan jenis kelamin"
);
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus grafik berdasarkan jenis kelamin");
toast.error(
"Terjadi kesalahan saat menghapus grafik berdasarkan jenis kelamin"
);
} finally {
grafikBerdasarkanJenisKelamin.delete.loading = false;
}
},
}
},
});
export default grafikBerdasarkanJenisKelamin;

View File

@@ -0,0 +1,63 @@
import ApiFetch from "@/lib/api-fetch";
import { proxy } from "valtio";
interface FileItem {
id: string;
name: string;
path: string;
link: string;
mimeType: string;
category: string;
realName: string;
isActive: boolean;
createdAt: string | Date;
updatedAt: string | Date;
deletedAt: string | Date | null;
}
const stateFileStorage = proxy<{
list: FileItem[] | null;
page: number;
limit: number;
total: number | undefined;
load: (params?: { search?: string }) => Promise<void>;
del: (params: { id: string }) => Promise<void>;
}>({
list: null,
page: 1,
limit: 10,
total: undefined,
async load(params?: { search?: string }) {
const { search = "" } = params ?? {};
try {
const { data } = await ApiFetch.api.fileStorage.findMany.get({
query: {
page: this.page,
limit: this.limit,
search,
category: 'image'
},
});
if (data?.data) {
this.list = data.data as FileItem[];
this.total = data.meta?.totalPages;
}
} catch (error) {
console.error('Error loading files:', error);
this.list = [];
this.total = 0;
}
},
async del({ id }: { id: string }) {
try {
await ApiFetch.api.fileStorage.delete({ id });
await this.load();
} catch (error) {
console.error('Error deleting file:', error);
throw error;
}
},
});
export default stateFileStorage;

View File

@@ -4,8 +4,9 @@ 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 { Box, Button, Center, FileInput, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { Box, Button, Center, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconImageInPicture, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
@@ -18,6 +19,11 @@ function EditFoto() {
const params = useParams();
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
name: fotoState.update.form.name || '',
deskripsi: fotoState.update.form.deskripsi || '',
imagesId: fotoState.update.form.imagesId || ''
});
useEffect(() => {
const loadFoto = async () => {
@@ -26,6 +32,11 @@ function EditFoto() {
try {
const data = await fotoState.update.load(id);
if (data) {
setFormData({
name: data.name || '',
deskripsi: data.deskripsi || '',
imagesId: data.imageGalleryFoto?.id || ''
});
if (data?.imageGalleryFoto?.link) {
setPreviewImage(data.imageGalleryFoto.link);
}
@@ -40,6 +51,12 @@ function EditFoto() {
const handleSubmit = async () => {
try {
fotoState.update.form = {
...fotoState.update.form,
name: formData.name,
deskripsi: formData.deskripsi,
imagesId: formData.imagesId
};
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({
file,
@@ -74,30 +91,55 @@ function EditFoto() {
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Judul Foto</Text>}
placeholder='Masukkan judul foto'
value={fotoState.update.form.name}
value={formData.name}
onChange={(e) =>
(fotoState.update.form.name = e.target.value)
(formData.name = e.target.value)
}
/>
<FileInput
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar</Text>}
value={file}
onChange={async (e) => {
if (!e) return;
setFile(e);
const base64 = await e.arrayBuffer().then((buf) =>
"data:image/png;base64," + Buffer.from(buf).toString("base64")
);
setPreviewImage(base64);
}}
/>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
</Center>
)}
<Box>
<Text>Upload Foto</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
</Center>
)}
</Box>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Foto</Text>
<EditEditor

View File

@@ -3,8 +3,9 @@ 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 { Box, Button, Center, FileInput, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } 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';
@@ -69,25 +70,62 @@ function CreateFoto() {
fotoState.create.form.name = val.target.value;
}}
/>
<FileInput
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar</Text>}
value={file}
onChange={async (e) => {
if (!e) return;
setFile(e);
const base64 = await e.arrayBuffer().then((buf) =>
"data:image/png;base64," + Buffer.from(buf).toString("base64")
);
setPreviewImage(base64);
}}
/>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
</Center>
)}
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Foto</Text>
<CreateEditor

View File

@@ -1,93 +1,124 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import JudulListTab from '../../../_com/judulListTab';
import { useProxy } from 'valtio/utils';
import stateGallery from '../../../_state/desa/gallery';
import { useShallowEffect } from '@mantine/hooks';
import HeaderSearch from '../../../_com/header';
import { useState } from 'react';
"use client";
import colors from "@/con/colors";
import stateFileStorage from "@/state/state-list-image";
import {
ActionIcon,
Box,
Flex,
Group,
Image,
Pagination,
Paper,
SimpleGrid,
Stack,
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";
function Foto() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Posisi Organisasi'
placeholder='pencarian'
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();
export default function ListImage() {
const { list, total } = useSnapshot(stateFileStorage);
useShallowEffect(() => {
fotoState.findMany.load()
}, [])
stateFileStorage.load();
}, []);
const filteredData = (fotoState.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.deskripsi.toLowerCase().includes(keyword)
);
});
if (!fotoState.findMany.data) {
return (
<Box py={10}>
<Skeleton h={500} />
</Box>
)
}
let timeOut: NodeJS.Timer;
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulListTab
title='List Foto'
href='/admin/desa/gallery/foto/create'
placeholder='pencarian'
searchIcon={<IconSearch size={16} />}
<Stack p={"lg"}>
<Flex justify="space-between">
<Title order={3}>List Foto</Title>
<TextInput
radius={"lg"}
leftSection={<IconSearch />}
rightSection={
<ActionIcon
variant="transparent"
onClick={() => {
stateFileStorage.load();
}}
>
<IconX />
</ActionIcon>
}
placeholder="Pencarian"
onChange={(e) => {
if (timeOut) clearTimeout(timeOut);
timeOut = setTimeout(() => {
stateFileStorage.load({ search: e.target.value });
}, 200);
}}
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Judul Foto</TableTh>
<TableTh>Tanggal Foto</TableTh>
<TableTh>Deskripsi Foto</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>{new Date(item.createdAt).toDateString()}</TableTd>
<TableTd>
<Text dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/desa/gallery/foto/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Flex>
<Paper bg={colors['white-1']} p={'md'}>
<SimpleGrid
cols={{
base: 3,
md: 5,
lg: 10,
}}
>
{list &&
list.map((v, k) => {
return (
<Paper key={k} shadow="sm">
<Stack pos={"relative"} gap={0} justify="space-between">
<motion.div
onClick={() => {
// copy to clipboard
navigator.clipboard.writeText(v.url);
toast("Berhasil disalin");
}}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.8 }}
>
<Image
h={100}
src={v.url + "?size=100"}
alt={v.name}
fit="cover"
loading="lazy"
style={{
objectFit: "cover",
objectPosition: "center",
}}
/>
</motion.div>
<Box p={"md"} h={54}>
<Text lineClamp={2} fz={"xs"}>
{v.name}
</Text>
</Box>
<Group justify="end">
<IconTrash
color="red"
onClick={() => {
stateFileStorage.del({ name: v.name }).finally(() => {
toast("Berhasil dihapus");
});
}}
/>
</Group>
</Stack>
</Paper>
);
})}
</SimpleGrid>
</Paper>
</Box>
{total && (
<Pagination
total={total}
onChange={(e) => {
stateFileStorage.page = e;
stateFileStorage.load();
}}
/>
)}
</Stack>
);
}
export default Foto;

View File

@@ -1,5 +1,5 @@
'use client'
import LayoutTabsGallery from "../../ppid/_com/layoutTabsGallery"
import LayoutTabsGallery from "./lib/layoutTabs"
export default function Layout({ children }: { children: React.ReactNode }) {
return (

View File

@@ -5,30 +5,20 @@ import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core'
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
function LayoutTabs({ children }: { children: React.ReactNode }) {
function LayoutTabsGallery({ children }: { children: React.ReactNode }) {
const router = useRouter()
const pathname = usePathname()
const tabs = [
{
label: "Grafik Hasil Kepuasan Masyarakat",
value: "grafikhasilkepuasamanmasyarakat",
href: "/admin/ppid/ikm-desa-darmasaba/grafik_hasil_kepuasan_masyarakat"
label: "Foto",
value: "foto",
href: "/admin/desa/gallery/foto"
},
{
label: "Grafik Berdasarkan Jenis Kelamin Responden",
value: "grafikberdasarkanjeniskelaminresponden",
href: "/admin/ppid/ikm-desa-darmasaba/grafik_berdasarkan_jenis_kelamin_responden"
label: "Video",
value: "video",
href: "/admin/desa/gallery/video"
},
{
label: "Grafik Berdasarkan Pilihan Responden",
value: "grafikberdasarkanpilihanresponden",
href: "/admin/ppid/ikm-desa-darmasaba/grafik_berdasarkan_responden"
},
{
label: "Grafik Berdasarkan Umur Responden",
value: "grafikberdasarkanumurresponden",
href: "/admin/ppid/ikm-desa-darmasaba/grafik_berdasarkan_umur"
}
];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
@@ -50,7 +40,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
return (
<Stack>
<Title order={3}>Indeks Kepuasan Masyarakat (IKM) Desa Darmasaba</Title>
<Title order={3}>Gallery</Title>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
{tabs.map((e, i) => (
@@ -69,4 +59,4 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
);
}
export default LayoutTabs;
export default LayoutTabsGallery;

View File

@@ -1,14 +1,14 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { Box, Button, Center, Pagination, Paper, Skeleton, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import JudulListTab from '../../../_com/judulListTab';
import { useProxy } from 'valtio/utils';
import stateGallery from '../../../_state/desa/gallery';
import { useShallowEffect } from '@mantine/hooks';
import HeaderSearch from '../../../_com/header';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import stateGallery from '../../../_state/desa/gallery';
function Video() {
const [search, setSearch] = useState("");
@@ -29,35 +29,34 @@ function Video() {
function ListVideo({ search }: { search: string }) {
const videoState = useProxy(stateGallery.video)
const router = useRouter();
const {
data,
page,
totalPages,
loading,
load,
} = videoState.findMany;
useShallowEffect(() => {
videoState.findMany.load()
}, [])
load(page, 10, search)
}, [page, search])
const filteredData = (videoState.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.deskripsi.toLowerCase().includes(keyword)
);
});
const filteredData = (data || [])
if (!videoState.findMany.data) {
if (loading || !data) {
return (
<Box py={10}>
<Skeleton h={500} />
</Box>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulListTab
<JudulList
title='List Video'
href='/admin/desa/gallery/video/create'
placeholder='pencarian'
searchIcon={<IconSearch size={16} />}
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
@@ -71,10 +70,25 @@ function ListVideo({ search }: { search: string }) {
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>{new Date(item.createdAt).toDateString()}</TableTd>
<TableTd>
<Text dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
<Box w={200}>
<Text lineClamp={1}>{item.name}</Text>
</Box>
</TableTd>
<TableTd>
<Box w={200}>
{new Date(item.createdAt).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'long',
year: 'numeric',
})}
</Box>
</TableTd>
<TableTd>
<Box w={200}>
<Text lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</Box>
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/desa/gallery/video/${item.id}`)}>
@@ -86,6 +100,15 @@ function ListVideo({ search }: { search: string }) {
</TableTbody>
</Table>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)} // ini penting!
total={totalPages}
mt="md"
mb="md"
/>
</Center>
</Box>
);
}

View File

@@ -18,6 +18,11 @@ function LayoutTabsDetail({ children }: { children: React.ReactNode }) {
label: "Profile Perbekel",
value: "profileperbekel",
href: "/admin/desa/profile/profile-perbekel"
},
{
label: "Profile Perbekel Dari Masa Ke Masa",
value: "profile-perbekel-dari-masa-ke-masa",
href: "/admin/desa/profile/profile-perbekel-dari-masa-ke-masa"
}
];
const curentTab = tabs.find(tab => tab.href === pathname)

View File

@@ -0,0 +1,170 @@
'use client'
/* eslint-disable react-hooks/exhaustive-deps */
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconImageInPicture, 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 EditPerbekelDariMasaKeMasa() {
const state = useProxy(stateProfileDesa.mantanPerbekel)
const router = useRouter();
const params = useParams();
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
nama: state.update.form.nama || '',
daerah: state.update.form.daerah || '',
periode: state.update.form.periode || '',
imageId: state.update.form.imageId || ''
});
useEffect(() => {
const loadFoto = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await state.update.load(id);
if (data) {
setFormData({
nama: data.nama || '',
daerah: data.daerah || '',
periode: data.periode || '',
imageId: data.imageId || ''
});
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 handleSubmit = async () => {
try {
state.update.form = {
...state.update.form,
nama: formData.nama,
daerah: formData.daerah,
periode: formData.periode,
imageId: formData.imageId
};
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");
}
state.update.form.imageId = uploaded.id;
}
await state.update.update();
toast.success('Perbekel dari masa ke masa berhasil diperbarui!');
router.push('/admin/desa/profile/profile-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');
}
};
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Perbekel Dari Masa Ke Masa</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama</Text>}
placeholder='Masukkan nama'
value={formData.nama}
onChange={(e) =>
(formData.nama = e.target.value)
}
/>
<Box>
<Text>Upload Foto</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
</Center>
)}
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Daerah</Text>}
placeholder='Masukkan daerah'
value={formData.daerah}
onChange={(e) =>
(formData.daerah = e.target.value)
}
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Periode</Text>}
placeholder='Masukkan periode'
value={formData.periode}
onChange={(e) =>
(formData.periode = e.target.value)
}
/>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditPerbekelDariMasaKeMasa;

View File

@@ -0,0 +1,111 @@
'use client'
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors';
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function DetailPerbekelDariMasa() {
const state = useProxy(stateProfileDesa.mantanPerbekel)
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
useShallowEffect(() => {
state.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
state.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/desa/profile/profile-perbekel-dari-masa-ke-masa")
}
}
if (!state.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Perbekel Dari Masa Ke Masa</Text>
{state.findUnique.data ? (
<Paper key={state.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Nama Perbekel</Text>
<Text fz={"lg"}>{state.findUnique.data?.nama}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Daerah</Text>
<Text fz={"lg"}>{state.findUnique.data?.daerah}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Periode</Text>
<Text fz={"lg"}>{state.findUnique.data?.periode}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
<Image w={{ base: 300, md: 350}} src={state.findUnique.data?.image?.link} alt="gambar" />
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (state.findUnique.data) {
setSelectedId(state.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={state.delete.loading || !state.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (state.findUnique.data) {
router.push(`/admin/desa/profile/profile-perbekel-dari-masa-ke-masa/${state.findUnique.data.id}/edit`);
}
}}
disabled={!state.findUnique.data}
color={"green"}
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus perbekel dari masa ke masa ini?'
/>
</Box>
);
}
export default DetailPerbekelDariMasa;

View File

@@ -0,0 +1,155 @@
'use client'
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } 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 CreateVideo() {
const state = useProxy(stateProfileDesa.mantanPerbekel)
const router = useRouter();
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const resetForm = () => {
state.create.form = {
nama: "",
daerah: "",
periode: "",
imageId: "",
};
setPreviewImage(null)
setFile(null)
};
const handleSubmit = async () => {
if (!file) {
return toast.warn("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 upload gambar");
}
state.create.form.imageId = uploaded.id;
await state.create.create();
resetForm();
router.push("/admin/desa/profile/profile-perbekel-dari-masa-ke-masa");
};
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Perbekel Dari Masa Ke Masa</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Perbekel</Text>}
placeholder='Masukkan nama perbekel'
value={state.create.form.nama}
onChange={(val) => {
state.create.form.nama = val.target.value;
}}
/>
<TextInput
label="Daerah"
placeholder="Masukkan daerah"
value={state.create.form.daerah}
onChange={(e) => {
state.create.form.daerah = e.currentTarget.value;
}}
required
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Periode</Text>}
placeholder='Masukkan periode'
value={state.create.form.periode}
onChange={(e) =>
(state.create.form.periode = e.target.value)
}
/>
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateVideo;

View File

@@ -0,0 +1,106 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, 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 JudulList from '../../../_com/judulList';
import stateProfileDesa from '../../../_state/desa/profile';
function PerbekelDariMasaKeMasa() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Perbekel Dari Masa Ke Masa'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListPerbekelDariMasaKeMasa search={search} />
</Box>
);
}
function ListPerbekelDariMasaKeMasa({ search }: { search: string }) {
const state = useProxy(stateProfileDesa.mantanPerbekel)
const router = useRouter();
const {
data,
page,
totalPages,
loading,
load,
} = state.findMany;
useShallowEffect(() => {
load(page, 10, search)
}, [page, search])
const filteredData = (data || [])
if (loading || !data) {
return (
<Box py={10}>
<Skeleton h={500} />
</Box>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Perbekel Dari Masa Ke Masa'
href='/admin/desa/profile/profile-perbekel-dari-masa-ke-masa/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Perbekel</TableTh>
<TableTh>Periode</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={200}>
<Text lineClamp={1}>{item.nama}</Text>
</Box>
</TableTd>
<TableTd>
<Box w={200}>
<Text lineClamp={1}>{item.periode}</Text>
</Box>
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/desa/profile/profile-perbekel-dari-masa-ke-masa/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)} // ini penting!
total={totalPages}
mt="md"
mb="md"
/>
</Center>
</Box>
);
}
export default PerbekelDariMasaKeMasa;

View File

@@ -33,7 +33,7 @@ function Page() {
</GridCol>
</Grid>
{perbekel && (
<Box >
<Box>
<Paper p={"xl"} bg={colors['BG-trans']}>
<Box px={{ base: "md", md: 100 }}>
<Grid>

View File

@@ -0,0 +1,63 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
function LayoutTabsIKM({ children }: { children: React.ReactNode }) {
const router = useRouter()
const pathname = usePathname()
const tabs = [
{
label: "Indeks Kepuasan Masyarakat",
value: "indekskepuasannamasyarakat",
href: "/admin/ppid/ikm-desa-darmasaba/indeks-kepuasan-masyarakat"
},
{
label: "Responden",
value: "responden",
href: "/admin/ppid/ikm-desa-darmasaba/responden"
},
];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value)
if (tab) {
router.push(tab.href)
}
setActiveTab(value)
}
useEffect(() => {
const match = tabs.find(tab => tab.href === pathname)
if (match) {
setActiveTab(match.value)
}
}, [pathname])
return (
<Stack>
<Title order={3}>IKM Desa Darmasaba</Title>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
{tabs.map((e, i) => (
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
))}
</TabsList>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value}>
{/* Konten dummy, bisa diganti tergantung routing */}
<></>
</TabsPanel>
))}
</Tabs>
{children}
</Stack>
);
}
export default LayoutTabsIKM;

View File

@@ -1,78 +0,0 @@
'use client'
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useEffect } from 'react';
import { useRouter, useParams } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import grafikBerdasarkanJenisKelamin from '@/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanJenisKelamin';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Title, TextInput } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
function EditGrafikBerdasarkanJenisKelaminResponden() {
const router = useRouter()
const params = useParams() as { id: string }
const stategrafikBerdasarkanJenisKelamin = useProxy(grafikBerdasarkanJenisKelamin)
const id = params.id
useEffect(() => {
if(id){
stategrafikBerdasarkanJenisKelamin.findUnique.load(id).then(() => {
const data = stategrafikBerdasarkanJenisKelamin.findUnique.data
if(data){
stategrafikBerdasarkanJenisKelamin.update.form = {
laki: data.laki || '',
perempuan: data.perempuan || '',
}
}
})
}
}, [id])
const handleSubmit = async () => {
stategrafikBerdasarkanJenisKelamin.update.id = id;
await stategrafikBerdasarkanJenisKelamin.update.submit();
router.push('/admin/ppid/ikm-desa-darmasaba/grafik_berdasarkan_jenis_kelamin_responden')
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack size={20} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: '100%', md: '50%' }} p={'md'}>
<Stack>
<Title order={3}>Grafik Hasil Kepuasan Masyarakat Terhadap Pelayanan Publik</Title>
<TextInput
label="Laki-laki"
type='number'
placeholder="masukkan jumlah"
value={stategrafikBerdasarkanJenisKelamin.update.form.laki}
onChange={(val) => {
stategrafikBerdasarkanJenisKelamin.update.form.laki = val.currentTarget.value;
}}
/>
<TextInput
label="Perempuan"
type="number"
placeholder="masukkan jumlah"
value={stategrafikBerdasarkanJenisKelamin.update.form.perempuan}
onChange={(val) => {
stategrafikBerdasarkanJenisKelamin.update.form.perempuan = val.currentTarget.value;
}}
/>
<Button
mt={10}
bg={colors['blue-button']}
onClick={handleSubmit}
>
Submit
</Button>
</Stack>
</Paper>
</Box>
);
}
export default EditGrafikBerdasarkanJenisKelaminResponden;

View File

@@ -1,83 +0,0 @@
'use client'
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
import React from 'react';
import { useRouter } from 'next/navigation';
import grafikBerdasarkanJenisKelamin from '@/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanJenisKelamin';
import { useProxy } from 'valtio/utils';
import { useState } from 'react';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Title, TextInput } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
function GrafikBerdasarkanJenisKelaminRespondenCreate() {
const router = useRouter();
const stategrafikBerdasarkanJenisKelamin = useProxy(grafikBerdasarkanJenisKelamin)
const [donutData, setDonutData] = useState<any[]>([]);
const resetForm = () => {
stategrafikBerdasarkanJenisKelamin.create.form = {
...stategrafikBerdasarkanJenisKelamin.create.form,
laki: "",
perempuan: "",
}
}
const handleSubmit = async () => {
try {
const id = await stategrafikBerdasarkanJenisKelamin.create.create();
if (typeof id !== 'undefined') {
const idStr = String(id);
await stategrafikBerdasarkanJenisKelamin.findUnique.load(idStr);
if (stategrafikBerdasarkanJenisKelamin.findUnique.data) {
setDonutData([stategrafikBerdasarkanJenisKelamin.findUnique.data]);
}
}
resetForm();
router.push("/admin/ppid/ikm-desa-darmasaba/grafik_berdasarkan_jenis_kelamin_responden");
} catch (error) {
console.error('Error submitting form:', error);
}
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack size={20} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: '100%', md: '50%' }} p={'md'}>
<Stack>
<Title order={3}>Grafik Hasil Kepuasan Masyarakat Terhadap Pelayanan Publik</Title>
<TextInput
label="Laki-laki"
type='number'
placeholder="masukkan jumlah"
value={stategrafikBerdasarkanJenisKelamin.create.form.laki}
onChange={(val) => {
stategrafikBerdasarkanJenisKelamin.create.form.laki = val.currentTarget.value;
}}
/>
<TextInput
label="Perempuan"
type="number"
placeholder="masukkan jumlah"
value={stategrafikBerdasarkanJenisKelamin.create.form.perempuan}
onChange={(val) => {
stategrafikBerdasarkanJenisKelamin.create.form.perempuan = val.currentTarget.value;
}}
/>
<Button
mt={10}
bg={colors['blue-button']}
onClick={handleSubmit}
>
Submit
</Button>
</Stack>
</Paper>
</Box>
);
}
export default GrafikBerdasarkanJenisKelaminRespondenCreate;

View File

@@ -1,227 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import grafikBerdasarkanJenisKelamin from '@/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanJenisKelamin';
import colors from '@/con/colors';
import { Box, Button, Center, Flex, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { Cell, Pie, PieChart } from 'recharts';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function GrafikBerdasarkanJenisKelamin() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Grafik Berdasarkan Jenis Kelamin Responden'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListGrafikBerdasarkanJenisKelamin search={search} />
</Box>
);
}
function ListGrafikBerdasarkanJenisKelamin({ search }: { search: string }) {
const stategrafikBerdasarkanJenisKelamin = useProxy(grafikBerdasarkanJenisKelamin)
const [donutData, setDonutData] = useState<any[]>([]);
const [mounted, setMounted] = useState(false);
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter();
const { data, page, totalPages, loading, load } = stategrafikBerdasarkanJenisKelamin.findMany
useShallowEffect(() => {
setMounted(true);
load(page, 10)
}, [page]);
useEffect(() => {
if (data) {
const totalLaki = data.reduce((acc: number, cur: any) => acc + Number(cur.laki || 0), 0);
const totalPerempuan = data.reduce((acc: number, cur: any) => acc + Number(cur.perempuan || 0), 0);
setDonutData([
{ name: 'laki', value: totalLaki, color: colors['blue-button'], key: 'laki' },
{ name: 'perempuan', value: totalPerempuan, color: '#10A85AFF', key: 'perempuan' }
]);
}
}, [data])
const filteredData = (data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.laki.toString().toLowerCase().includes(keyword) ||
item.perempuan.toString().toLowerCase().includes(keyword)
);
});
const handleDelete = async () => {
if (selectedId) {
await grafikBerdasarkanJenisKelamin.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
stategrafikBerdasarkanJenisKelamin.findMany.load()
}
}
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={730} />
</Stack>
);
}
if (data.length === 0) {
return (
<Box py={10}>
<Paper p="md">
<Stack>
<JudulList
title='List Data Berdasarkan Jenis Kelamin Responden'
href='/admin/ppid/ikm-desa-darmasaba/grafik_berdasarkan_jenis_kelamin_responden/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Laki-laki</TableTh>
<TableTh>Perempuan</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
</Table>
<Text ta="center">Tidak ada data berdasarkan jenis kelamin responden yang tersedia</Text>
</Stack>
</Paper>
</Box >
);
}
return (
<Box>
<Stack gap={"xs"}>
<Paper bg={colors['white-1']} p={"md"} h={{ base: 730, md: 650 }}>
<JudulList
title='List Data Berdasarkan Jenis Kelamin Responden'
href='/admin/ppid/ikm-desa-darmasaba/grafik_berdasarkan_jenis_kelamin_responden/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh>
<TableTh style={{ width: '20%', textAlign: 'center' }}>Laki-laki</TableTh>
<TableTh style={{ width: '20%', textAlign: 'center' }}>Perempuan</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Edit</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length === 0 ? (
<TableTr>
<TableTd colSpan={6}>
<Text ta='center' c='dimmed'>Belum ada data grafik responden</Text>
</TableTd>
</TableTr>
) : (
filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd style={{ width: '5%', textAlign: 'center' }}>{index + 1}</TableTd>
<TableTd style={{ width: '20%', textAlign: 'center' }}>{item.laki}</TableTd>
<TableTd style={{ width: '20%', textAlign: 'center' }}>{item.perempuan}</TableTd>
<TableTd style={{ width: '15%', textAlign: 'center' }}>
<Button color='green' onClick={() => router.push(`/admin/ppid/ikm-desa-darmasaba/grafik_berdasarkan_jenis_kelamin_responden/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd style={{ width: '15%', textAlign: 'center' }}>
<Button
color='red'
disabled={stategrafikBerdasarkanJenisKelamin.delete.loading}
onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconTrash size={20} />
</Button>
</TableTd>
</TableTr>
))
)}
</TableTbody>
</Table>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={totalPages}
mt="md"
mb="md"
/>
</Center>
{/* Chart */}
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Title pb={10} order={3}>Grafik Berdasarkan Jenis Kelamin Responden</Title>
{mounted && donutData.length === 0 ? (<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text>) : (<Box style={{ width: '100%', height: 'auto', minHeight: 200 }}>
<PieChart
width={800} height={300}
data={donutData}
>
<Pie
dataKey="value"
nameKey="name"
data={donutData}
cx={500}
cy={150}
innerRadius={60}
outerRadius={115}
label={true}
>
{donutData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
</PieChart>
<Flex gap={"md"} align={"center"}>
<Box bg={colors['blue-button']} w={20} h={20} />
<Text>Laki-laki: {donutData.find((entry) => entry.name === 'laki')?.value}</Text>
</Flex>
<Flex gap={"md"} align={"center"}>
<Box bg={'#10A85AFF'} w={20} h={20} />
<Text>Perempuan: {donutData.find((entry) => entry.name === 'perempuan')?.value}</Text>
</Flex>
</Box>
)}
</Stack>
</Paper>
</Box>
</Stack>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleDelete}
text='Apakah anda yakin ingin menghapus grafik berdasarkan hasil responden ini?'
/>
</Box>
);
}
export default GrafikBerdasarkanJenisKelamin;

View File

@@ -1,98 +0,0 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import React, { useEffect } from 'react';
import { useRouter, useParams } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import grafikBerdasarkanResponden from '@/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanResponden';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Title, TextInput } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
function EditGrafikBerdasarkanResponden() {
const router = useRouter()
const params = useParams() as { id: string }
const stateGrafikResponden = useProxy(grafikBerdasarkanResponden)
const id = params.id
useEffect(() => {
if(id){
stateGrafikResponden.findUnique.load(id).then(() => {
const data = stateGrafikResponden.findUnique.data
if(data){
stateGrafikResponden.update.form = {
sangatbaik: data.sangatbaik || '',
baik: data.baik || '',
kurangbaik: data.kurangbaik || '',
tidakbaik: data.tidakbaik || '',
}
}
})
}
}, [id])
const handleSubmit = async () => {
stateGrafikResponden.update.id = id;
await stateGrafikResponden.update.submit();
router.push('/admin/ppid/ikm-desa-darmasaba/grafik_berdasarkan_responden')
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack size={20} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: '100%', md: '50%' }} p={'md'}>
<Stack>
<Title order={3}>Grafik Hasil Kepuasan Masyarakat Terhadap Pelayanan Publik</Title>
<TextInput
label="Sangat Baik"
type='number'
placeholder="masukkan jumlah"
value={stateGrafikResponden.update.form.sangatbaik}
onChange={(val) => {
stateGrafikResponden.update.form.sangatbaik = val.currentTarget.value;
}}
/>
<TextInput
label="Baik"
type="number"
placeholder="masukkan jumlah"
value={stateGrafikResponden.update.form.baik}
onChange={(val) => {
stateGrafikResponden.update.form.baik = val.currentTarget.value;
}}
/>
<TextInput
label="Kurang Baik"
type="number"
placeholder="masukkan jumlah"
value={stateGrafikResponden.update.form.kurangbaik}
onChange={(val) => {
stateGrafikResponden.update.form.kurangbaik = val.currentTarget.value;
}}
/>
<TextInput
label="Tidak Baik"
type="number"
placeholder="masukkan jumlah"
value={stateGrafikResponden.update.form.tidakbaik}
onChange={(val) => {
stateGrafikResponden.update.form.tidakbaik = val.currentTarget.value;
}}
/>
<Button
mt={10}
bg={colors['blue-button']}
onClick={handleSubmit}
>
Submit
</Button>
</Stack>
</Paper>
</Box>
);
}
export default EditGrafikBerdasarkanResponden;

View File

@@ -1,98 +0,0 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import grafikBerdasarkanResponden from '@/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanResponden';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function GrafikBerdasarkanRespondenCreate() {
const router = useRouter()
const stategrafikBerdasarkanResponden = useProxy(grafikBerdasarkanResponden)
const [donutData, setDonutData] = useState<any[]>([]);
const resetForm = () => {
stategrafikBerdasarkanResponden.create.form = {
...stategrafikBerdasarkanResponden.create.form,
sangatbaik: "",
baik: "",
kurangbaik: "",
tidakbaik: "",
}
}
const handleSubmit = async () => {
const id = await stategrafikBerdasarkanResponden.create.create();
if (id) {
const idStr = String(id);
await stategrafikBerdasarkanResponden.findUnique.load(idStr);
if (stategrafikBerdasarkanResponden.findUnique.data) {
setDonutData([stategrafikBerdasarkanResponden.findUnique.data]);
}
}
resetForm();
router.push("/admin/ppid/ikm-desa-darmasaba/grafik_berdasarkan_responden")
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack size={20} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: '100%', md: '50%' }} p={'md'}>
<Stack>
<Title order={3}>Grafik Hasil Kepuasan Masyarakat Berdasarkan Responden</Title>
<TextInput
label="Sangat Baik"
type='number'
placeholder="masukkan jumlah"
value={stategrafikBerdasarkanResponden.create.form.sangatbaik}
onChange={(val) => {
stategrafikBerdasarkanResponden.create.form.sangatbaik = val.currentTarget.value;
}}
/>
<TextInput
label="Baik"
type="number"
placeholder="masukkan jumlah"
value={stategrafikBerdasarkanResponden.create.form.baik}
onChange={(val) => {
stategrafikBerdasarkanResponden.create.form.baik = val.currentTarget.value;
}}
/>
<TextInput
label="Kurang Baik"
type="number"
placeholder="masukkan jumlah"
value={stategrafikBerdasarkanResponden.create.form.kurangbaik}
onChange={(val) => {
stategrafikBerdasarkanResponden.create.form.kurangbaik = val.currentTarget.value;
}}
/>
<TextInput
label="Tidak Baik"
type="number"
placeholder="masukkan jumlah"
value={stategrafikBerdasarkanResponden.create.form.tidakbaik}
onChange={(val) => {
stategrafikBerdasarkanResponden.create.form.tidakbaik = val.currentTarget.value;
}}
/>
<Button
mt={10}
bg={colors['blue-button']}
onClick={handleSubmit}
>
Submit
</Button>
</Stack>
</Paper>
</Box>
);
}
export default GrafikBerdasarkanRespondenCreate;

View File

@@ -1,251 +0,0 @@
'use client'
/* eslint-disable @typescript-eslint/no-explicit-any */
import colors from '@/con/colors';
import { Box, Button, Center, Flex, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { Cell, Pie, PieChart } from 'recharts';
import { useSnapshot } from 'valtio';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import grafikBerdasarkanResponden from '../../../_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanResponden';
function GrafikBerdasarkanResponden() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Grafik Berdasarkan Pilihan Responden'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListGrafikBerdasarkanResponden search={search} />
</Box>
);
}
function ListGrafikBerdasarkanResponden({ search }: { search: string }) {
const stategrafikBerdasarkanResponden = useSnapshot(grafikBerdasarkanResponden)
const [donutData, setDonutData] = useState<any[]>([]);
const [mounted, setMounted] = useState(false);
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter();
const { data, page, totalPages, loading, load } = stategrafikBerdasarkanResponden.findMany
useShallowEffect(() => {
setMounted(true)
load(page, 10)
}, [page])
const filteredData = (data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.sangatbaik.toString().toLowerCase().includes(keyword) ||
item.baik.toString().toLowerCase().includes(keyword) ||
item.kurangbaik.toString().toLowerCase().includes(keyword) ||
item.tidakbaik.toString().toLowerCase().includes(keyword)
);
});
useEffect(() => {
if (data) {
const totalSangatBaik = data.reduce((acc: number, cur: any) => acc + Number(cur.sangatbaik || 0), 0);
const totalBaik = data.reduce((acc: number, cur: any) => acc + Number(cur.baik || 0), 0);
const totalKurangBaik = data.reduce((acc: number, cur: any) => acc + Number(cur.kurangbaik || 0), 0);
const totalTidakBaik = data.reduce((acc: number, cur: any) => acc + Number(cur.tidakbaik || 0), 0);
setDonutData([
{ name: 'sangatbaik', value: totalSangatBaik, color: colors['blue-button'], key: 'sangatbaik' },
{ name: 'baik', value: totalBaik, color: '#10A85AFF', key: 'baik' },
{ name: 'kurangbaik', value: totalKurangBaik, color: '#B3AA12FF', key: 'kurangbaik' },
{ name: 'tidakbaik', value: totalTidakBaik, color: '#B21313FF', key: 'tidakbaik' }
]);
}
}, [data])
const handleDelete = async () => {
if (selectedId) {
await stategrafikBerdasarkanResponden.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
// Refresh data agar chart & tabel ikut update
stategrafikBerdasarkanResponden.findMany.load();
}
}
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={730} />
</Stack>
);
}
if (data.length === 0) {
return (
<Box py={10}>
<Paper p="md" >
<Stack>
<JudulList
title='List Data Berdasarkan Responden'
href='/admin/ppid/ikm-desa-darmasaba/grafik_berdasarkan_responden/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Sangat Baik</TableTh>
<TableTh>Baik</TableTh>
<TableTh>Kurang Baik</TableTh>
<TableTh>Tidak Baik</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
</Table>
<Text ta="center">Tidak ada data grafik berdasarkan responden yang tersedia</Text>
</Stack>
</Paper>
</Box >
);
}
return (
<Box>
<Stack gap={"xs"}>
<Paper bg={colors['white-1']} p={"md"} h={{ base: 730, md: 650 }}>
<JudulList
title='List Data Berdasarkan Pilihan Responden'
href='/admin/ppid/ikm-desa-darmasaba/grafik_berdasarkan_responden/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Sangat Baik</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Baik</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Kurang Baik</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Tidak Baik</TableTh>
<TableTh style={{ width: '5%', textAlign: 'center' }}>Edit</TableTh>
<TableTh style={{ width: '5%', textAlign: 'center' }}>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length === 0 ? (
<TableTr>
<TableTd colSpan={6}>
<Text ta='center' c='dimmed'>Belum ada data grafik responden</Text>
</TableTd>
</TableTr>
) : (
filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd style={{ width: '5%', textAlign: 'center' }}>{index + 1}</TableTd>
<TableTd style={{ width: '5%', textAlign: 'center' }}>{item.sangatbaik}</TableTd>
<TableTd style={{ width: '15%', textAlign: 'center' }}>{item.baik}</TableTd>
<TableTd style={{ width: '15%', textAlign: 'center' }}>{item.kurangbaik}</TableTd>
<TableTd style={{ width: '15%', textAlign: 'center' }}>{item.tidakbaik}</TableTd>
<TableTd style={{ width: '5%', textAlign: 'center' }}>
<Button color='green' onClick={() => router.push(`/admin/ppid/ikm-desa-darmasaba/grafik_berdasarkan_responden/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd style={{ width: '5%', textAlign: 'center' }}>
<Button
color='red'
disabled={stategrafikBerdasarkanResponden.delete.loading}
onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconTrash size={20} />
</Button>
</TableTd>
</TableTr>
))
)}
</TableTbody>
</Table>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={totalPages}
mt="md"
mb="md"
/>
</Center>
{/* Chart */}
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Title pb={10} order={3}>Grafik Berdasarkan Pilihan Responden</Title>
{mounted && donutData.length > 0 ? (<Box style={{ width: '100%', height: 'auto', minHeight: 200 }}>
<PieChart
width={800} height={300}
data={donutData}
>
<Pie
dataKey="value"
nameKey="name"
data={donutData}
cx={500}
cy={150}
innerRadius={60}
outerRadius={115}
label={true}
>
{donutData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
</PieChart>
<Flex gap={"md"} align={"center"}>
<Box bg={colors['blue-button']} w={20} h={20} />
<Text>Sangat Baik: {donutData.find((entry) => entry.name === 'sangatbaik')?.value}</Text>
</Flex>
<Flex gap={"md"} align={"center"}>
<Box bg={'#10A85AFF'} w={20} h={20} />
<Text>Baik: {donutData.find((entry) => entry.name === 'baik')?.value}</Text>
</Flex>
<Flex gap={"md"} align={"center"}>
<Box bg={'#B3AA12FF'} w={20} h={20} />
<Text>Kurang Baik: {donutData.find((entry) => entry.name === 'kurangbaik')?.value}</Text>
</Flex>
<Flex gap={"md"} align={"center"}>
<Box bg={'#B21313FF'} w={20} h={20} />
<Text>Tidak Baik: {donutData.find((entry) => entry.name === 'tidakbaik')?.value}</Text>
</Flex>
</Box>
) : (
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text>
)}
</Stack>
</Paper>
</Box>
</Stack>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleDelete}
text='Apakah anda yakin ingin menghapus grafik berdasarkan hasil responden ini?'
/>
</Box>
);
}
export default GrafikBerdasarkanResponden;

View File

@@ -1,97 +0,0 @@
'use client'
/* eslint-disable react-hooks/exhaustive-deps */
import grafikBerdasarkanUmur from '@/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanUmur';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Title, TextInput } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import React, { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
function EditGrafikBerdasarakanUmur() {
const router = useRouter()
const params = useParams() as { id: string }
const stategrafikBerdasarkanUmur = useProxy(grafikBerdasarkanUmur)
const id = params.id
useEffect(() => {
if(id){
stategrafikBerdasarkanUmur.findUnique.load(id).then(() => {
const data = stategrafikBerdasarkanUmur.findUnique.data
if(data){
stategrafikBerdasarkanUmur.update.form = {
remaja: data.remaja || '',
dewasa: data.dewasa || '',
orangtua: data.orangtua || '',
lansia: data.lansia || '',
}
}
})
}
}, [id])
const handleSubmit = async () => {
stategrafikBerdasarkanUmur.update.id = id;
await stategrafikBerdasarkanUmur.update.submit();
router.push('/admin/ppid/ikm-desa-darmasaba/grafik_berdasarkan_umur')
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack size={20} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: '100%', md: '50%' }} p={'md'}>
<Stack>
<Title order={3}>Grafik Hasil Kepuasan Masyarakat Terhadap Pelayanan Publik</Title>
<TextInput
label="Remaja"
type='number'
placeholder="masukkan jumlah"
value={stategrafikBerdasarkanUmur.update.form.remaja}
onChange={(val) => {
stategrafikBerdasarkanUmur.update.form.remaja = val.currentTarget.value;
}}
/>
<TextInput
label="Dewasa"
type="number"
placeholder="masukkan jumlah"
value={stategrafikBerdasarkanUmur.update.form.dewasa}
onChange={(val) => {
stategrafikBerdasarkanUmur.update.form.dewasa = val.currentTarget.value;
}}
/>
<TextInput
label="Orangtua"
type='number'
placeholder="masukkan jumlah"
value={stategrafikBerdasarkanUmur.update.form.orangtua}
onChange={(val) => {
stategrafikBerdasarkanUmur.update.form.orangtua = val.currentTarget.value;
}}
/>
<TextInput
label="Lansia"
type="number"
placeholder="masukkan jumlah"
value={stategrafikBerdasarkanUmur.update.form.lansia}
onChange={(val) => {
stategrafikBerdasarkanUmur.update.form.lansia = val.currentTarget.value;
}}
/>
<Button
mt={10}
bg={colors['blue-button']}
onClick={handleSubmit}
>
Submit
</Button>
</Stack>
</Paper>
</Box>
);
}
export default EditGrafikBerdasarakanUmur;

View File

@@ -1,98 +0,0 @@
'use client'
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
import grafikBerdasarkanUmur from '@/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanUmur';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Title, TextInput } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import React, { useState } from 'react';
import { useProxy } from 'valtio/utils';
function GrafikBerdasarakanUmurCreate() {
const stategrafikBerdasarkanUmur = useProxy(grafikBerdasarkanUmur)
const [donutData, setDonutData] = useState<any[]>([]);
const router = useRouter()
const resetForm = () => {
stategrafikBerdasarkanUmur.create.form = {
...stategrafikBerdasarkanUmur.create.form,
remaja: "",
dewasa: "",
orangtua: "",
lansia: "",
}
}
const handleSubmit = async () => {
const id = await stategrafikBerdasarkanUmur.create.create();
if (id) {
const idStr = String(id);
await stategrafikBerdasarkanUmur.findUnique.load(idStr);
if (stategrafikBerdasarkanUmur.findUnique.data) {
setDonutData([stategrafikBerdasarkanUmur.findUnique.data]);
}
}
resetForm();
router.push("/admin/ppid/ikm-desa-darmasaba/grafik_berdasarkan_umur")
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack size={20} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: '100%', md: '50%' }} p={'md'}>
<Stack>
<Title order={3}>Grafik Hasil Kepuasan Masyarakat Terhadap Pelayanan Publik</Title>
<TextInput
label="Remaja"
type='number'
placeholder="masukkan jumlah"
value={stategrafikBerdasarkanUmur.create.form.remaja}
onChange={(val) => {
stategrafikBerdasarkanUmur.create.form.remaja = val.currentTarget.value;
}}
/>
<TextInput
label="Dewasa"
type="number"
placeholder="masukkan jumlah"
value={stategrafikBerdasarkanUmur.create.form.dewasa}
onChange={(val) => {
stategrafikBerdasarkanUmur.create.form.dewasa = val.currentTarget.value;
}}
/>
<TextInput
label="Orangtua"
type="number"
placeholder="masukkan jumlah"
value={stategrafikBerdasarkanUmur.create.form.orangtua}
onChange={(val) => {
stategrafikBerdasarkanUmur.create.form.orangtua = val.currentTarget.value;
}}
/>
<TextInput
label="Lansia"
type="number"
placeholder="masukkan jumlah"
value={stategrafikBerdasarkanUmur.create.form.lansia}
onChange={(val) => {
stategrafikBerdasarkanUmur.create.form.lansia = val.currentTarget.value;
}}
/>
<Button
mt={10}
bg={colors['blue-button']}
onClick={handleSubmit}
>
Submit
</Button>
</Stack>
</Paper>
</Box>
);
}
export default GrafikBerdasarakanUmurCreate;

View File

@@ -1,252 +0,0 @@
'use client'
/* eslint-disable @typescript-eslint/no-explicit-any */
import grafikBerdasarkanUmur from '@/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanUmur';
import colors from '@/con/colors';
import { Box, Button, Center, Flex, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { Cell, Pie, PieChart } from 'recharts';
import { useProxy } from 'valtio/utils';
import JudulListTab from '../../../_com/judulListTab';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
function GrafikBerdasarakanUmur() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Grafik Berdasarkan Umur Responden'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListGrafikBerdasarakanUmur search={search} />
</Box>
);
}
function ListGrafikBerdasarakanUmur({ search }: { search: string }) {
const stategrafikBerdasarkanUmur = useProxy(grafikBerdasarkanUmur)
const [donutData, setDonutData] = useState<any[]>([]);
const [mounted, setMounted] = useState(false);
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter()
const { data, page, totalPages, loading, load } = stategrafikBerdasarkanUmur.findMany
useShallowEffect(() => {
setMounted(true);
load(page, 10)
}, [page]);
useEffect(() => {
if (data) {
const totalRemaja = data.reduce((acc: number, cur: any) => acc + Number(cur.remaja || 0), 0);
const totalDewasa = data.reduce((acc: number, cur: any) => acc + Number(cur.dewasa || 0), 0);
const totalOrangtua = data.reduce((acc: number, cur: any) => acc + Number(cur.orangtua || 0), 0);
const totalLansia = data.reduce((acc: number, cur: any) => acc + Number(cur.lansia || 0), 0);
setDonutData([
{ name: 'remaja', value: totalRemaja, color: colors['blue-button'], key: 'remaja' },
{ name: 'dewasa', value: totalDewasa, color: '#D32711FF', key: 'dewasa' },
{ name: 'orangtua', value: totalOrangtua, color: '#B46B04FF', key: 'orangtua' },
{ name: 'lansia', value: totalLansia, color: '#038617FF', key: 'lansia' }
]);
}
}, [data])
const filteredData = (data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.remaja.toString().toLowerCase().includes(keyword) ||
item.dewasa.toString().toLowerCase().includes(keyword) ||
item.orangtua.toString().toLowerCase().includes(keyword) ||
item.lansia.toString().toLowerCase().includes(keyword)
);
});
const handleDelete = async () => {
if (selectedId) {
await grafikBerdasarkanUmur.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
stategrafikBerdasarkanUmur.findMany.load()
}
}
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={730} />
</Stack>
);
}
if (data.length === 0) {
return (
<Box py={10}>
<Paper p="md" >
<Stack>
<JudulList
title='List Data Berdasarkan Umur Responden'
href='/admin/ppid/ikm-desa-darmasaba/grafik_berdasarkan_umur/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Remaja</TableTh>
<TableTh>Dewasa</TableTh>
<TableTh>Orangtua</TableTh>
<TableTh>Lansia</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
</Table>
<Text ta="center">Tidak ada data grafik berdasarkan umur responden yang tersedia</Text>
</Stack>
</Paper>
</Box >
);
}
return (
<Box>
<Stack gap={"xs"}>
<Paper bg={colors['white-1']} p={"md"} h={{ base: 730, md: 650 }}>
<JudulListTab
title='List Data Berdasarkan Umur Responden'
href='/admin/ppid/ikm-desa-darmasaba/grafik_berdasarkan_umur/create'
placeholder='pencarian'
searchIcon={<IconSearch size={16} />}
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh style={{ width: '2%', textAlign: 'center' }}>No</TableTh>
<TableTh style={{ width: '5%', textAlign: 'center' }}>Remaja</TableTh>
<TableTh style={{ width: '5%', textAlign: 'center' }}>Dewasa</TableTh>
<TableTh style={{ width: '5%', textAlign: 'center' }}>Orangtua</TableTh>
<TableTh style={{ width: '5%', textAlign: 'center' }}>Lansia</TableTh>
<TableTh style={{ width: '5%', textAlign: 'center' }}>Edit</TableTh>
<TableTh style={{ width: '5%', textAlign: 'center' }}>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length === 0 ? (
<TableTr>
<TableTd colSpan={6}>
<Text ta='center' c='dimmed'>Belum ada data grafik responden</Text>
</TableTd>
</TableTr>
) : (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd style={{ textAlign: 'center' }}>{filteredData.indexOf(item) + 1}</TableTd>
<TableTd style={{ textAlign: 'center' }}>{item.remaja}</TableTd>
<TableTd style={{ textAlign: 'center' }}>{item.dewasa}</TableTd>
<TableTd style={{ textAlign: 'center' }}>{item.orangtua}</TableTd>
<TableTd style={{ textAlign: 'center' }}>{item.lansia}</TableTd>
<TableTd style={{ textAlign: 'center' }}>
<Button color='green' onClick={() => router.push(`/admin/ppid/ikm-desa-darmasaba/grafik_berdasarkan_umur/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd style={{ textAlign: 'center' }}>
<Button
color='red'
disabled={stategrafikBerdasarkanUmur.delete.loading}
onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconTrash size={20} />
</Button>
</TableTd>
</TableTr>
))
)}
</TableTbody>
</Table>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={totalPages}
mt="md"
mb="md"
/>
</Center>
{/* Chart */}
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Title pb={10} order={3}>Grafik Umur Berdasarkan Responden</Title>
{mounted && donutData.length > 0 ? (<Box style={{ width: '100%', height: 'auto', minHeight: 200 }}>
<PieChart
width={800} height={300}
data={donutData}
>
<Pie
dataKey="value"
nameKey="name"
data={donutData}
cx={500}
cy={150}
innerRadius={60}
outerRadius={115}
label={true}
>
{donutData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
</PieChart>
<Flex gap={"md"} align={"center"}>
<Box bg={colors['blue-button']} w={20} h={20} />
<Text>Remaja: {donutData.find((entry) => entry.name === 'remaja')?.value}</Text>
</Flex>
<Flex gap={"md"} align={"center"}>
<Box bg={'#D32711FF'} w={20} h={20} />
<Text>Dewasa: {donutData.find((entry) => entry.name === 'dewasa')?.value}</Text>
</Flex>
<Flex gap={"md"} align={"center"}>
<Box bg={'#B46B04FF'} w={20} h={20} />
<Text>Orangtua: {donutData.find((entry) => entry.name === 'orangtua')?.value}</Text>
</Flex>
<Flex gap={"md"} align={"center"}>
<Box bg={'#038617FF'} w={20} h={20} />
<Text>Lansia: {donutData.find((entry) => entry.name === 'lansia')?.value}</Text>
</Flex>
</Box>
) : (
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text>
)}
</Stack>
</Paper>
</Box>
</Stack>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleDelete}
text='Apakah anda yakin ingin menghapus grafik berdasarkan hasil responden ini?'
/>
</Box>
);
}
export default GrafikBerdasarakanUmur;

View File

@@ -1,81 +0,0 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import grafikHasilKepuasanMasyarakat from '@/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikHasilKepuasan';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter, useParams } from 'next/navigation';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
function EditGrafikHasilKepuasan() {
const router = useRouter()
const params = useParams() as { id: string }
const grafikHasilKepuasan = useProxy(grafikHasilKepuasanMasyarakat)
const id = params.id
// Load data saat komponen mount
useEffect(() => {
if (id) {
grafikHasilKepuasan.findUnique.load(id).then(() => {
const data = grafikHasilKepuasan.findUnique.data
if (data) {
grafikHasilKepuasan.update.form = {
label: data.label || '',
kepuasan: data.kepuasan || '',
}
}
})
}
}, [id])
const handleSubmit = async () => {
// Set the ID before submitting
grafikHasilKepuasan.update.id = id;
await grafikHasilKepuasan.update.submit();
router.push('/admin/ppid/ikm-desa-darmasaba/grafik_hasil_kepuasan_masyarakat')
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack size={20} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: '100%', md: '50%' }} p={'md'}>
<Stack>
<Title order={3}>Edit Grafik Hasil Kepuasan Masyarakat</Title>
<TextInput
label="Label"
placeholder="masukkan label"
value={grafikHasilKepuasan.update.form.label}
onChange={(val) => {
grafikHasilKepuasan.update.form.label = val.currentTarget.value;
}}
/>
<TextInput
label="Jumlah Kepuasan"
type="number"
placeholder="masukkan jumlah kepuasan"
value={grafikHasilKepuasan.update.form.kepuasan}
onChange={(val) => {
grafikHasilKepuasan.update.form.kepuasan = val.currentTarget.value;
}}
/>
<Button
mt={10}
bg={colors['blue-button']}
onClick={handleSubmit}
>
Simpan Perubahan
</Button>
</Stack>
</Paper>
</Box>
)
}
export default EditGrafikHasilKepuasan;

View File

@@ -1,83 +0,0 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import grafikHasilKepuasanMasyarakat from '@/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikHasilKepuasan';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function GrafikHasilKepuasan() {
const router = useRouter()
const grafikHasilKepuasan = useProxy(grafikHasilKepuasanMasyarakat)
const [chartData, setChartData] = useState<any[]>([]);
const resetForm = () => {
grafikHasilKepuasan.create.form = {
...grafikHasilKepuasan.create.form,
label: "",
kepuasan: "",
}
}
const handleSubmit = async () => {
const id = await grafikHasilKepuasan.create.create();
if (id) {
// Ensure id is a string
const idStr = String(id);
await grafikHasilKepuasan.findUnique.load(idStr);
if (grafikHasilKepuasan.findUnique.data) {
setChartData([grafikHasilKepuasan.findUnique.data]);
}
}
resetForm();
router.push("/admin/ppid/ikm-desa-darmasaba/grafik_hasil_kepuasan_masyarakat")
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack size={20} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: '100%', md: '50%' }} p={'md'}>
<Stack>
<Title order={3}>Grafik Hasil Kepuasan Masyarakat Terhadap Pelayanan Publik</Title>
<TextInput
label="Label"
placeholder="masukkan label"
value={grafikHasilKepuasan.create.form.label}
onChange={(val) => {
grafikHasilKepuasan.create.form.label = val.currentTarget.value;
}}
/>
<TextInput
label="Jumlah Kepuasan"
type="number"
placeholder="masukkan jumlah kepuasan"
value={grafikHasilKepuasan.create.form.kepuasan}
onChange={(val) => {
grafikHasilKepuasan.create.form.kepuasan = val.currentTarget.value;
}}
/>
<Button
mt={10}
bg={colors['blue-button']}
onClick={handleSubmit}
>
Submit
</Button>
</Stack>
</Paper>
</Box>
);
}
export default GrafikHasilKepuasan;

View File

@@ -1,211 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { useMediaQuery, useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { Bar, BarChart, Legend, Tooltip, XAxis, YAxis } from 'recharts';
import { useSnapshot } from 'valtio';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import grafikHasilKepuasanMasyarakat from '../../../_state/ppid/indeks_kepuasan_masyarakat/grafikHasilKepuasan';
function GrafikHasilKepuasanMasyarakat() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Grafik Hasil Kepuasan Masyarakat'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListGrafikHasilKepuasanMasyarakat search={search} />
</Box>
);
}
function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
type IKMGrafik = {
id: string;
label: string;
kepuasan: number;
}
const stateGrafikHasilKepuasan = useSnapshot(grafikHasilKepuasanMasyarakat)
const [mounted, setMounted] = useState(false);
const [chartData, setChartData] = useState<IKMGrafik[]>([]);
const isTablet = useMediaQuery('(max-width: 1024px)')
const isMobile = useMediaQuery('(max-width: 768px)')
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter();
const { data, page, totalPages, loading, load } = stateGrafikHasilKepuasan.findMany
useShallowEffect(() => {
setMounted(true)
load(page, 10)
}, [page])
useEffect(() => {
if (data) {
setChartData(
data.map((item) => ({
id: item.id,
label: item.label,
kepuasan: Number(item.kepuasan),
}))
);
}
}, [data]);
const filteredData = (data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.label.toLowerCase().includes(keyword) ||
item.kepuasan.toString().toLowerCase().includes(keyword)
);
});
const handleDelete = () => {
if (selectedId) {
stateGrafikHasilKepuasan.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
stateGrafikHasilKepuasan.findMany.load()
}
}
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={730} />
</Stack>
);
}
if (data.length === 0) {
return (
<Box py={10}>
<Paper p="md" >
<Stack>
<JudulList
title='List Data Hasil Kepuasan Masyarakat'
href='/admin/ppid/ikm-desa-darmasaba/grafik_hasil_kepuasan_masyarakat/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Label</TableTh>
<TableTh>Jumlah Kepuasan</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
</Table>
<Text ta="center">Tidak ada data grafik hasil kepuasan masyarakat yang tersedia</Text>
</Stack>
</Paper>
</Box >
);
}
return (
<Box>
<Stack gap={"xs"}>
<Paper bg={colors['white-1']} p={'md'} h={{ base: 730, md: 650 }}>
<JudulList
title='List Data Hasil Kepuasan Masyarakat'
href='/admin/ppid/ikm-desa-darmasaba/grafik_hasil_kepuasan_masyarakat/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh>
<TableTh style={{ width: '20%', textAlign: 'center' }}>Label</TableTh>
<TableTh style={{ width: '20%', textAlign: 'center' }}>Jumlah Kepuasan</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Edit</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd style={{ textAlign: 'center' }}>{index + 1}</TableTd>
<TableTd style={{ textAlign: 'center' }}>{item.label}</TableTd>
<TableTd style={{ textAlign: 'center' }}>{item.kepuasan}</TableTd>
<TableTd style={{ textAlign: 'center' }}>
<Button color='green' onClick={() => router.push(`/admin/ppid/ikm-desa-darmasaba/grafik_hasil_kepuasan_masyarakat/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd style={{ textAlign: 'center' }}>
<Button
color='red'
disabled={stateGrafikHasilKepuasan.delete.loading}
onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconTrash size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={totalPages}
mt="md"
mb="md"
/>
</Center>
{/* Chart */}
<Box style={{ width: '100%', minWidth: 300, height: 500, minHeight: 300 }}>
<Paper style={{ width: '100%', minWidth: 300, height: 500, minHeight: 300 }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title pb={10} order={3}>Grafik Hasil Kepuasan Masyarakat</Title>
{mounted && chartData.length > 0 ? (
<BarChart width={isMobile ? 300 : isTablet ? 300 : 300} height={380} data={chartData} >
<XAxis dataKey="label" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="kepuasan" fill={colors['blue-button']} name="Kepuasan" />
</BarChart>
) : (
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text>
)}
</Stack>
</Paper>
</Box>
</Stack>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleDelete}
text='Apakah anda yakin ingin menghapus grafik hasil kepuasan masyarakat ini?'
/>
</Box>
);
}
export default GrafikHasilKepuasanMasyarakat;

View File

@@ -0,0 +1,252 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client';
import colors from '@/con/colors';
import { PieChart, BarChart } from '@mantine/charts'; // ✅ Ganti recharts dengan Mantine
import {
Box,
Center,
Flex,
Paper,
SimpleGrid,
Skeleton,
Stack,
Text,
Title,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import indeksKepuasanState from '../../../_state/landing-page/indeks-kepuasan';
interface ChartDataItem {
name: string;
value: number;
color: string;
label?: string;
}
function Page() {
const state = useProxy(indeksKepuasanState.responden);
const { data, loading } = state.findMany;
const [donutDataJenisKelamin, setDonutDataJenisKelamin] = useState<ChartDataItem[]>([]);
const [donutDataRating, setDonutDataRating] = useState<ChartDataItem[]>([]);
const [donutDataKelompokUmur, setDonutDataKelompokUmur] = useState<ChartDataItem[]>([]);
const [barChartData, setBarChartData] = useState<Array<{ month: string; count: number }>>([]);
useShallowEffect(() => {
if (data) {
// Hitung total berdasarkan jenis kelamin
const totalLaki = data.filter((item: any) => item.jenisKelamin?.name?.toLowerCase() === 'laki-laki').length;
const totalPerempuan = data.filter((item: any) => item.jenisKelamin?.name?.toLowerCase() === 'perempuan').length;
// Hitung total berdasarkan rating
const totalSangatBaik = data.filter((item: any) => item.rating?.name?.toLowerCase() === 'sangat baik').length;
const totalBaik = data.filter((item: any) => item.rating?.name?.toLowerCase() === 'baik').length;
const totalKurangBaik = data.filter((item: any) => item.rating?.name?.toLowerCase() === 'kurang baik').length;
const totalSangatKurangBaik = data.filter((item: any) => item.rating?.name?.toLowerCase() === 'sangat kurang baik').length;
// Hitung total berdasarkan kelompok umur
const totalMuda = data.filter((item: any) => item.kelompokUmur?.name?.toLowerCase() === 'muda').length;
const totalDewasa = data.filter((item: any) => item.kelompokUmur?.name?.toLowerCase() === 'dewasa').length;
const totalLansia = data.filter((item: any) => item.kelompokUmur?.name?.toLowerCase() === 'lansia').length;
// Update gender chart data
setDonutDataJenisKelamin([
{ name: 'Laki-laki', value: totalLaki, color: colors['blue-button'] },
{ name: 'Perempuan', value: totalPerempuan, color: '#10A85AFF' },
]);
// Update rating chart data
setDonutDataRating([
{ name: 'Sangat Baik', value: totalSangatBaik, color: colors['blue-button'] },
{ name: 'Baik', value: totalBaik, color: '#10A85AFF' },
{ name: 'Kurang Baik', value: totalKurangBaik, color: '#FFA500' },
{ name: 'Sangat Kurang Baik', value: totalSangatKurangBaik, color: '#FF4500' },
]);
// Update age group chart data
setDonutDataKelompokUmur([
{ name: 'Muda', value: totalMuda, color: colors['blue-button'] },
{ name: 'Dewasa', value: totalDewasa, color: '#10A85AFF' },
{ name: 'Lansia', value: totalLansia, color: '#FFA500' },
]);
// Process data for bar chart (group by month)
const monthYearMap = new Map<string, number>();
data.forEach((item: any) => {
// Try both createdAt and tanggal fields
const dateValue = item.tanggal || item.createdAt;
if (!dateValue) return;
const parsedDate = new Date(dateValue);
if (isNaN(parsedDate.getTime())) return;
const month = parsedDate.getMonth() + 1;
const year = parsedDate.getFullYear();
const monthYearKey = `${year}-${String(month).padStart(2, '0')}`;
monthYearMap.set(monthYearKey, (monthYearMap.get(monthYearKey) || 0) + 1);
});
// Convert map to array and sort by date
const barData = Array.from(monthYearMap.entries())
.map(([key, count]) => {
const [year, month] = key.split('-');
const monthName = new Date(Number(year), Number(month) - 1, 1)
.toLocaleString('id-ID', { month: 'long' });
return {
month: `${monthName} ${year}`,
count,
sortKey: parseInt(`${year}${String(month).padStart(2, '0')}`, 10)
};
})
.sort((a, b) => a.sortKey - b.sortKey)
.map(({ month, count }) => ({ month, count }));
setBarChartData(barData);
}
}, [data]);
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={730} />
</Stack>
);
}
if (data.length === 0) {
return (
<Stack py={10}>
<Text c="dimmed" ta="center" my="md">
Belum ada data untuk ditampilkan
</Text>
</Stack>
);
}
return (
<Stack gap="xs">
{/* Bar Chart - Data per Tanggal */}
<Paper bg={colors['white-1']} p="md" radius="md" mb="md">
<Title order={4} mb="md" ta="center">Jumlah Responden per Bulan</Title>
<Box h={300}>
<BarChart
h={300}
data={barChartData}
dataKey="month"
series={[{ name: 'count', color: colors['blue-button'] }]}
tickLine="y"
xAxisLabel="Bulan"
yAxisLabel="Jumlah Responden"
withTooltip
tooltipAnimationDuration={200}
/>
</Box>
</Paper>
<SimpleGrid cols={{ base: 1, md: 3 }}>
{/* Chart Jenis Kelamin */}
<Paper bg={colors['white-1']} p="md" radius="md">
<Stack>
<Title order={4}>Jenis Kelamin</Title>
{donutDataJenisKelamin.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md">
Belum ada data untuk ditampilkan dalam grafik
</Text>
) : (
<Box>
<Center>
<PieChart
withLabels
withTooltip
labelsType="percent"
size={250}
data={donutDataJenisKelamin}
/>
</Center>
<Stack gap="sm" mt="md">
{donutDataJenisKelamin.map((entry) => (
<Flex key={entry.name} gap="md" align="center">
<Box bg={entry.color} w={20} h={20} style={{ flexShrink: 0 }} />
<Text size="sm">{entry.name}: {entry.value}</Text>
</Flex>
))}
</Stack>
</Box>
)}
</Stack>
</Paper>
{/* Chart Rating */}
<Paper bg={colors['white-1']} p="md" radius="md">
<Stack>
<Title order={4}>Pilihan</Title>
{donutDataRating.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md">
Belum ada data untuk ditampilkan dalam grafik
</Text>
) : (
<Box>
<Center>
<PieChart
withLabels
withTooltip
labelsType="percent"
size={250}
data={donutDataRating}
/>
</Center>
<Stack gap="sm" mt="md">
{donutDataRating.map((entry) => (
<Flex key={entry.name} gap="md" align="center">
<Box bg={entry.color} w={20} h={20} style={{ flexShrink: 0 }} />
<Text size="sm">{entry.name}: {entry.value}</Text>
</Flex>
))}
</Stack>
</Box>
)}
</Stack>
</Paper>
{/* Chart Kelompok Umur */}
<Paper bg={colors['white-1']} p="md" radius="md">
<Stack>
<Title order={4}>Umur</Title>
{donutDataKelompokUmur.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md">
Belum ada data untuk ditampilkan dalam grafik
</Text>
) : (
<Box>
<Center>
<PieChart
withLabels
withTooltip
labelsType="percent"
size={250}
data={donutDataKelompokUmur}
/>
</Center>
<Stack gap="sm" mt="md">
{donutDataKelompokUmur.map((entry) => (
<Flex key={entry.name} gap="md" align="center">
<Box bg={entry.color} w={20} h={20} style={{ flexShrink: 0 }} />
<Text size="sm">{entry.name}: {entry.value}</Text>
</Flex>
))}
</Stack>
</Box>
)}
</Stack>
</Paper>
</SimpleGrid>
</Stack>
);
}
export default Page;

View File

@@ -1,10 +1,13 @@
'use client'
import LayoutTabs from "../_com/layoutTabs";
import React from 'react';
import LayoutTabsIKM from './_lib/layoutTabs';
export default function Layout({children} : {children: React.ReactNode}) {
return (
<LayoutTabs>
{children}
</LayoutTabs>
)
}
function Layout({ children }: { children: React.ReactNode }) {
return (
<LayoutTabsIKM>
{children}
</LayoutTabsIKM>
);
}
export default Layout;

View File

@@ -0,0 +1,190 @@
'use client'
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useEffect, useState } from 'react';
import { useRouter, useParams } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Title, TextInput, Text, Select } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan';
import { toast } from 'react-toastify';
interface FormResponden {
name: string;
tanggal: string;
jenisKelaminId: string;
ratingId: string;
kelompokUmurId: string;
}
function EditResponden() {
const router = useRouter()
const params = useParams() as { id: string }
const state = useProxy(indeksKepuasanState.responden)
const id = params.id
const [formData, setFormData] = useState<FormResponden>({
name: '',
tanggal: '',
jenisKelaminId: '',
ratingId: '',
kelompokUmurId: '',
})
useEffect(() => {
indeksKepuasanState.jenisKelaminResponden.findMany.load();
indeksKepuasanState.pilihanRatingResponden.findMany.load();
indeksKepuasanState.kelompokUmurResponden.findMany.load();
const loadResponden = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await state.update.load(id);
if (data) {
const formattedDate = data.tanggal && !isNaN(new Date(data.tanggal).getTime())
? new Date(data.tanggal).toISOString().split('T')[0]
: '';
// ⬇️ FIX PENTING: tambahkan ini
state.update.id = id;
state.update.form = {
name: data.name,
tanggal: formattedDate,
jenisKelaminId: data.jenisKelaminId,
ratingId: data.ratingId,
kelompokUmurId: data.kelompokUmurId,
};
setFormData({
name: data.name,
tanggal: data.tanggal,
jenisKelaminId: data.jenisKelaminId,
ratingId: data.ratingId,
kelompokUmurId: data.kelompokUmurId,
});
}
} catch (error) {
console.error("Error loading program penghijauan:", error);
toast.error("Gagal memuat data program penghijauan");
}
}
loadResponden();
}, [params?.id]);
const handleSubmit = async () => {
state.update.id = id;
await state.update.submit();
router.push('/admin/ppid/ikm-desa-darmasaba/responden')
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack size={20} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: '100%', md: '50%' }} p={'md'}>
<Stack>
<Title order={3}>Edit Responden</Title>
<TextInput
label="Nama"
type='text'
placeholder="masukkan nama"
value={formData.name}
onChange={(val) => {
setFormData({
...formData,
name: val.currentTarget.value
})
}}
/>
<TextInput
type="date"
value={formData.tanggal}
onChange={(e) => {
setFormData({
...formData,
tanggal: e.currentTarget.value, // ✅ sudah format YYYY-MM-DD
});
}}
/>
<Select
key={"jenisKelamin"}
label={<Text fw="bold" fz="sm">Jenis Kelamin</Text>}
placeholder="Pilih jenis kelamin"
value={formData.jenisKelaminId}
onChange={(val) => setFormData({ ...formData, jenisKelaminId: val || "" })}
data={
(indeksKepuasanState.jenisKelaminResponden.findMany.data || [])
.filter(Boolean) // Hapus null/undefined
.map((v) => ({
value: v.id || '',
label: typeof v.name === 'string' ? v.name : 'Tanpa Nama'
}))
}
disabled={indeksKepuasanState.jenisKelaminResponden.findMany.loading} // ✅ disable saat loading
clearable
searchable
required
error={!formData.jenisKelaminId ? "Pilih jenis kelamin" : undefined}
/>
<Select
key={"rating"}
value={formData.ratingId}
onChange={(val) => setFormData({ ...formData, ratingId: val || "" })}
label={<Text fw={"bold"} fz={"sm"}>Rating</Text>}
placeholder='Pilih rating'
data={
(indeksKepuasanState.pilihanRatingResponden.findMany.data || [])
.filter(Boolean)
.map((v) => ({
value: v.id || '',
label: typeof v.name === 'string' ? v.name : 'Tanpa Nama'
}))
}
disabled={indeksKepuasanState.pilihanRatingResponden.findMany.loading}
clearable
searchable
required
error={!formData.ratingId ? "Pilih rating" : undefined}
/>
<Select
key={"kelompokUmur"}
value={formData.kelompokUmurId}
onChange={(val) => setFormData({ ...formData, kelompokUmurId: val || "" })}
label={<Text fw={"bold"} fz={"sm"}>Kelompok Umur</Text>}
placeholder='Pilih kelompok umur'
data={
(indeksKepuasanState.kelompokUmurResponden.findMany.data || [])
.filter(Boolean)
.map((v) => ({
value: v.id || '',
label: typeof v.name === 'string' ? v.name : 'Tanpa Nama'
}))
}
disabled={indeksKepuasanState.kelompokUmurResponden.findMany.loading}
clearable
searchable
required
error={!formData.kelompokUmurId ? "Pilih kelompok umur" : undefined}
/>
<Button
mt={10}
bg={colors['blue-button']}
onClick={handleSubmit}
>
Submit
</Button>
</Stack>
</Paper>
</Box>
);
}
export default EditResponden;

View File

@@ -0,0 +1,116 @@
'use client'
import { ModalKonfirmasiHapus } from "@/app/admin/(dashboard)/_com/modalKonfirmasiHapus"
import indeksKepuasanState from "@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan"
import colors from "@/con/colors"
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from "@mantine/core"
import { useShallowEffect } from "@mantine/hooks"
import { IconArrowBack, IconEdit, IconX } from "@tabler/icons-react"
import { useRouter, useParams } from "next/navigation"
import { useState } from "react"
import { useProxy } from "valtio/utils"
export default function DetailResponden() {
const [modalHapus, setModalHapus] = useState(false)
const stateDetail = useProxy(indeksKepuasanState.responden)
const router = useRouter()
const params = useParams()
const [selectedId, setSelectedId] = useState<string | null>(null)
useShallowEffect(() => {
stateDetail.findUnique.load(params?.id as string)
}, [params?.id])
const handleHapus = () => {
if (selectedId) {
stateDetail.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/ppid/ikm-desa-darmasaba/responden")
}
}
if (!stateDetail.findUnique.data) {
return (
<Stack>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Responden</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Responden</Text>
<Text fz={"lg"}>{stateDetail.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Tanggal</Text>
<Text fz={"lg"}>{
stateDetail.findUnique.data?.tanggal
? new Date(stateDetail.findUnique.data.tanggal).toLocaleDateString('id-ID')
: '-'
}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Jenis Kelamin</Text>
<Text fz={"lg"}>{stateDetail.findUnique.data?.jenisKelamin?.name}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Rating</Text>
<Text fz={"lg"}>{stateDetail.findUnique.data?.rating?.name}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Kelompok Umur</Text>
<Text fz={"lg"}>{stateDetail.findUnique.data?.kelompokUmur?.name}</Text>
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (stateDetail.findUnique.data) {
setSelectedId(stateDetail.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={stateDetail.delete.loading || !stateDetail.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (stateDetail.findUnique.data) {
router.push(`/admin/ppid/ikm-desa-darmasaba/responden/${stateDetail.findUnique.data.id}/edit`);
}
}}
disabled={!stateDetail.findUnique.data}
color={"green"}
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus responden ini?"
/>
</Box>
)
}

View File

@@ -0,0 +1,148 @@
'use client'
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
import React from 'react';
import { useRouter } from 'next/navigation';
import grafikBerdasarkanJenisKelamin from '@/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanJenisKelamin';
import { useProxy } from 'valtio/utils';
import { useState } from 'react';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Title, TextInput, Select, Text } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan';
import { useShallowEffect } from '@mantine/hooks';
function RespondenCreate() {
const router = useRouter();
const stategrafikBerdasarkanResponden = useProxy(indeksKepuasanState.responden)
const [donutData, setDonutData] = useState<any[]>([]);
const resetForm = () => {
stategrafikBerdasarkanResponden.create.form = {
...stategrafikBerdasarkanResponden.create.form,
name: "",
tanggal: "",
jenisKelaminId: "",
ratingId: "",
kelompokUmurId: "",
}
}
useShallowEffect(() => {
indeksKepuasanState.jenisKelaminResponden.findMany.load()
indeksKepuasanState.pilihanRatingResponden.findMany.load()
indeksKepuasanState.kelompokUmurResponden.findMany.load()
})
const handleSubmit = async () => {
try {
const id = await stategrafikBerdasarkanResponden.create.create();
if (typeof id !== 'undefined') {
const idStr = String(id);
await stategrafikBerdasarkanResponden.findUnique.load(idStr);
if (stategrafikBerdasarkanResponden.findUnique.data) {
setDonutData([stategrafikBerdasarkanResponden.findUnique.data]);
}
}
resetForm();
router.push("/admin/ppid/ikm-desa-darmasaba/responden");
} catch (error) {
console.error('Error submitting form:', error);
}
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack size={20} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: '100%', md: '50%' }} p={'md'}>
<Stack>
<Title order={3}>Grafik Hasil Kepuasan Masyarakat Terhadap Pelayanan Publik</Title>
<TextInput
label="Nama"
type='text'
placeholder="masukkan nama"
value={stategrafikBerdasarkanResponden.create.form.name}
onChange={(val) => {
stategrafikBerdasarkanResponden.create.form.name = val.currentTarget.value;
}}
/>
<TextInput
label="Tanggal"
type="date"
placeholder="masukkan tanggal"
value={stategrafikBerdasarkanResponden.create.form.tanggal}
onChange={(val) => {
stategrafikBerdasarkanResponden.create.form.tanggal = val.currentTarget.value;
}}
/>
<Select
key={"jenisKelamin"}
label={"Jenis Kelamin"}
placeholder={indeksKepuasanState.jenisKelaminResponden.findMany.loading ? 'Memuat...' : 'Pilih jenis kelamin'}
value={stategrafikBerdasarkanResponden.create.form.jenisKelaminId || ""}
onChange={(val) => {
stategrafikBerdasarkanResponden.create.form.jenisKelaminId = val ?? "";
}}
data={
(indeksKepuasanState.jenisKelaminResponden.findMany.data || [])
.filter(Boolean) // Hapus null, undefined, dll
.map((item) => ({
value: item.id,
label: item.name || 'Tanpa Nama',
}))
}
disabled={indeksKepuasanState.jenisKelaminResponden.findMany.loading}
/>
<Select
key={"rating_responden"}
label={"Rating"}
placeholder={indeksKepuasanState.pilihanRatingResponden.findMany.loading ? 'Memuat...' : 'Pilih rating'}
value={stategrafikBerdasarkanResponden.create.form.ratingId || ""}
onChange={(val) => {
stategrafikBerdasarkanResponden.create.form.ratingId = val ?? "";
}}
data={
(indeksKepuasanState.pilihanRatingResponden.findMany.data || [])
.filter(Boolean) // Hapus null, undefined, dll
.map((item) => ({
value: item.id,
label: item.name || 'Tanpa Nama',
}))
}
disabled={indeksKepuasanState.pilihanRatingResponden.findMany.loading}
/>
<Select
key={"kelompokUmur"}
label={"Kelompok Umur"}
placeholder={indeksKepuasanState.kelompokUmurResponden.findMany.loading ? 'Memuat...' : 'Pilih kelompok umur'}
value={stategrafikBerdasarkanResponden.create.form.kelompokUmurId || ""}
onChange={(val) => {
stategrafikBerdasarkanResponden.create.form.kelompokUmurId = val ?? "";
}}
data={
(indeksKepuasanState.kelompokUmurResponden.findMany.data || [])
.filter(Boolean) // Hapus null, undefined, dll
.map((item) => ({
value: item.id,
label: item.name || 'Tanpa Nama',
}))
}
disabled={indeksKepuasanState.kelompokUmurResponden.findMany.loading}
/>
<Button
mt={10}
bg={colors['blue-button']}
onClick={handleSubmit}
>
Submit
</Button>
</Stack>
</Paper>
</Box>
);
}
export default RespondenCreate;

View File

@@ -0,0 +1,151 @@
'use client';
import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, 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 JudulList from '../../../_com/judulList';
import indeksKepuasanState from '../../../_state/landing-page/indeks-kepuasan';
function Responden() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Responden'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListResponden search={search} />
</Box>
);
}
interface ListRespondenProps {
search: string;
}
function ListResponden({ search }: ListRespondenProps) {
const state = useProxy(indeksKepuasanState.responden);
const router = useRouter();
const { data, page, totalPages, loading, load } = state.findMany;
useShallowEffect(() => {
load(page, 10)
}, [page]);
const filteredData = (data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword)
);
});
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={730} />
</Stack>
);
}
if (data.length === 0) {
return (
<Box py={10}>
<Paper p="md">
<Stack>
<JudulList
title='List Data Berdasarkan Jenis Kelamin Responden'
href='/admin/ppid/ikm-desa-darmasaba/responden/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Nama</TableTh>
<TableTh>Tanggal</TableTh>
<TableTh>Jenis Kelamin</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
</Table>
<Text ta="center">Tidak ada data berdasarkan jenis kelamin responden yang tersedia</Text>
</Stack>
</Paper>
</Box >
);
}
return (
<Box>
<Stack gap="xs">
<Paper bg={colors['white-1']} p="md" h={{ base: 730, md: 650 }}>
<JudulList
title='List Data Responden'
href='/admin/ppid/ikm-desa-darmasaba/responden/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh>
<TableTh style={{ width: '20%', textAlign: 'center' }}>Nama</TableTh>
<TableTh style={{ width: '20%', textAlign: 'center' }}>Tanggal</TableTh>
<TableTh style={{ width: '20%', textAlign: 'center' }}>Jenis Kelamin</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length === 0 ? (
<TableTr>
<TableTd colSpan={6}>
<Text ta='center' c='dimmed'>Belum ada data responden</Text>
</TableTd>
</TableTr>
) : (
filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd style={{ width: '5%', textAlign: 'center' }}>{index + 1}</TableTd>
<TableTd style={{ width: '20%', textAlign: 'center' }}>{item.name}</TableTd>
<TableTd style={{ width: '20%', textAlign: 'center' }}>{item.tanggal
? new Date(item.tanggal).toLocaleDateString('id-ID')
: '-'}</TableTd>
<TableTd style={{ width: '20%', textAlign: 'center' }}>{item.jenisKelamin.name}</TableTd>
<TableTd style={{ width: '15%', textAlign: 'center' }}>
<Button color='green' onClick={() => router.push(`/admin/ppid/ikm-desa-darmasaba/responden/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))
)}
</TableTbody>
</Table>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={totalPages}
mt="md"
mb="md"
/>
</Center>
</Stack>
</Box>
);
}
export default Responden;

View File

@@ -79,7 +79,7 @@ export const navBar = [
{
id: "PPID_8",
name: "IKM Desa Darmasaba",
path: "/admin/ppid/ikm-desa-darmasaba/grafik_hasil_kepuasan_masyarakat"
path: "/admin/ppid/ikm-desa-darmasaba/indeks-kepuasan-masyarakat"
},
]

View File

@@ -1,25 +1,57 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// /api/berita/findManyPaginated.ts
import prisma from "@/lib/prisma";
import { Context } from "elysia";
async function galleryFotoFindMany() {
try {
const data = await prisma.galleryFoto.findMany({
where: { isActive: true },
include: {
imageGalleryFoto: true,
},
});
async function galleryFotoFindMany(context: Context) {
// Ambil parameter dari query
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || '';
const skip = (page - 1) * limit;
return {
success: true,
message: "Success fetch gallery foto",
data,
};
} catch (e) {
console.error("Find many error:", e);
return {
success: false,
message: "Failed fetch gallery foto",
};
}
// Buat where clause
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ name: { contains: search, mode: 'insensitive' } },
{ deskripsi: { contains: search, mode: 'insensitive' } },
];
}
try {
// Ambil data dan total count secara paralel
const [data, total] = await Promise.all([
prisma.galleryFoto.findMany({
where,
include: {
imageGalleryFoto: true,
},
skip,
take: limit,
orderBy: { createdAt: 'desc' },
}),
prisma.galleryFoto.count({ where }),
]);
return {
success: true,
message: "Berhasil ambil foto dengan pagination",
data,
page,
limit,
total,
totalPages: Math.ceil(total / limit),
};
} catch (e) {
console.error("Error di findMany paginated:", e);
return {
success: false,
message: "Gagal mengambil data foto",
};
}
}
export default galleryFotoFindMany
export default galleryFotoFindMany;

View File

@@ -0,0 +1,18 @@
import prisma from "@/lib/prisma";
export default async function galleryFotoFindRecent() {
const result = await prisma.galleryFoto.findMany({
orderBy: {
createdAt: "desc",
},
take: 3, // ambil 4 data terbaru
include: {
imageGalleryFoto: true,
},
});
return {
success: true,
data: result,
};
}

View File

@@ -4,6 +4,7 @@ import galleryFotoDelete from "./del";
import galleryFotoFindMany from "./find-many";
import galleryFotoUpdate from "./updt";
import galleryFotoFindUnique from "./findUnique";
import galleryFotoFindRecent from "./findRecent";
const GalleryFoto = new Elysia({ prefix: "/gallery/foto", tags: ["Desa/Gallery/Foto"] })
.get("/find-many", galleryFotoFindMany)
@@ -18,6 +19,7 @@ const GalleryFoto = new Elysia({ prefix: "/gallery/foto", tags: ["Desa/Gallery/F
imagesId: t.String(),
}),
})
.get("/find-recent", galleryFotoFindRecent)
.delete("/del/:id", galleryFotoDelete)
.put("/:id", async (context) => {
const response = await galleryFotoUpdate(context);

View File

@@ -1,22 +1,53 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// /api/berita/findManyPaginated.ts
import prisma from "@/lib/prisma";
import { Context } from "elysia";
async function galleryVideoFindMany() {
try {
const data = await prisma.galleryVideo.findMany({
where: { isActive: true },
});
async function galleryVideoFindMany(context: Context) {
// Ambil parameter dari query
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || '';
const skip = (page - 1) * limit;
return {
success: true,
message: "Success fetch gallery video",
data,
};
} catch (e) {
console.error("Find many error:", e);
return {
success: false,
message: "Failed fetch gallery video",
};
}
// Buat where clause
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ name: { contains: search, mode: 'insensitive' } }
];
}
try {
// Ambil data dan total count secara paralel
const [data, total] = await Promise.all([
prisma.galleryVideo.findMany({
where,
skip,
take: limit,
orderBy: { createdAt: 'desc' },
}),
prisma.galleryVideo.count({ where }),
]);
return {
success: true,
message: "Berhasil ambil video dengan pagination",
data,
page,
limit,
total,
totalPages: Math.ceil(total / limit),
};
} catch (e) {
console.error("Error di findMany paginated:", e);
return {
success: false,
message: "Gagal mengambil data video",
};
}
}
export default galleryVideoFindMany;

View File

@@ -10,6 +10,7 @@ import Penghargaan from "./penghargaan";
import KategoriPotensi from "./potensi/kategori-potensi";
import KategoriBerita from "./berita/kategori-berita";
import KategoriPengumuman from "./pengumuman/kategori-pengumuman";
import MantanPerbekel from "./profile/profile-mantan-perbekel";
const Desa = new Elysia({ prefix: "/api/desa", tags: ["Desa"] })
@@ -19,10 +20,12 @@ const Desa = new Elysia({ prefix: "/api/desa", tags: ["Desa"] })
.use(PotensiDesa)
.use(GalleryFoto)
.use(GalleryVideo)
.use(MantanPerbekel)
.use(LayananDesa)
.use(Penghargaan)
.use(KategoriPotensi)
.use(KategoriBerita)
.use(KategoriPengumuman)
export default Desa;

View File

@@ -0,0 +1,29 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormCreate = {
nama: string;
periode: string;
imageId: string;
daerah: string;
}
export default async function profileMantanPerbekelCreate(context: Context) {
const body = context.body as FormCreate;
await prisma.perbekelDariMasaKeMasa.create({
data: {
nama: body.nama,
periode: body.periode,
imageId: body.imageId,
daerah: body.daerah,
},
});
return {
success: true,
message: "Success create profile mantan perbekel",
data: {
...body,
},
};
}

View File

@@ -0,0 +1,53 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
import fs from "fs/promises";
import path from "path";
const profileMantanPerbekelDelete = async (context: Context) => {
const id = context.params?.id as string;
if (!id) {
return {
status: 400,
body: "ID tidak diberikan",
};
}
const foto = await prisma.perbekelDariMasaKeMasa.findUnique({
where: { id },
include: {
image: true,
},
});
if (!foto) {
return {
status: 404,
body: "Foto tidak ditemukan",
};
}
// Hapus file gambar dari filesystem jika ada
if (foto.image) {
try {
const filePath = path.join(foto.image.path, foto.image.name);
await fs.unlink(filePath);
await prisma.fileStorage.delete({
where: { id: foto.image.id },
});
} catch (err) {
console.error("Gagal hapus gambar lama:", err);
}
}
await prisma.perbekelDariMasaKeMasa.delete({
where: { id },
});
return {
status: 200,
body: "Foto berhasil dihapus",
};
}
export default profileMantanPerbekelDelete

View File

@@ -0,0 +1,58 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// /api/berita/findManyPaginated.ts
import prisma from "@/lib/prisma";
import { Context } from "elysia";
async function profileMantanPerbekelFindMany(context: Context) {
// Ambil parameter dari query
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || '';
const skip = (page - 1) * limit;
// Buat where clause
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ nama: { contains: search, mode: 'insensitive' } },
{ periode: { contains: search, mode: 'insensitive' } },
{ daerah: { contains: search, mode: 'insensitive' } },
];
}
try {
// Ambil data dan total count secara paralel
const [data, total] = await Promise.all([
prisma.perbekelDariMasaKeMasa.findMany({
where,
include: {
image: true,
},
skip,
take: limit,
orderBy: { periode: 'asc' },
}),
prisma.perbekelDariMasaKeMasa.count({ where }),
]);
return {
success: true,
message: "Berhasil ambil foto dengan pagination",
data,
page,
limit,
total,
totalPages: Math.ceil(total / limit),
};
} catch (e) {
console.error("Error di findMany paginated:", e);
return {
success: false,
message: "Gagal mengambil data foto",
};
}
}
export default profileMantanPerbekelFindMany;

View File

@@ -0,0 +1,49 @@
import prisma from "@/lib/prisma";
export default async function profileMantanPerbekelFindUnique(request: Request) {
const url = new URL(request.url);
const pathSegments = url.pathname.split('/');
const id = pathSegments[pathSegments.length - 1];
if (!id) {
return Response.json({
success: false,
message: "ID tidak boleh kosong",
}, { status: 400 });
}
try {
if (typeof id !== 'string') {
return Response.json({
success: false,
message: "ID tidak valid",
}, { status: 400 });
}
const data = await prisma.perbekelDariMasaKeMasa.findUnique({
where: { id },
include: {
image: true,
},
});
if (!data) {
return Response.json({
success: false,
message: "Profile mantan perbekel tidak ditemukan",
}, { status: 404 });
}
return Response.json({
success: true,
message: "Success fetch profile mantan perbekel by ID",
data,
}, { status: 200 });
} catch (e) {
console.error("Find by ID error:", e);
return Response.json({
success: false,
message: "Gagal mengambil profile mantan perbekel: " + (e instanceof Error ? e.message : 'Unknown error'),
}, { status: 500 });
}
}

View File

@@ -0,0 +1,44 @@
import Elysia, { t } from "elysia";
import profileMantanPerbekelCreate from "./create";
import profileMantanPerbekelDelete from "./del";
import profileMantanPerbekelFindMany from "./findMany";
import profileMantanPerbekelUpdate from "./updt";
import profileMantanPerbekelFindUnique from "./findUnique";
const MantanPerbekel = new Elysia({
prefix: "mantanperbekel",
tags: ["Desa/Profile/MantanPerbekel"],
})
.get("/findMany", profileMantanPerbekelFindMany)
.get("/:id", async (context) => {
const response = await profileMantanPerbekelFindUnique(
new Request(context.request)
);
return response;
})
.post("/create", profileMantanPerbekelCreate, {
body: t.Object({
nama: t.String(),
daerah: t.String(),
periode: t.String(),
imageId: t.String(),
}),
})
.delete("/del/:id", profileMantanPerbekelDelete)
.put(
"/:id",
async (context) => {
const response = await profileMantanPerbekelUpdate(context);
return response;
},
{
body: t.Object({
nama: t.String(),
daerah: t.String(),
periode: t.String(),
imageId: t.String(),
}),
}
);
export default MantanPerbekel;

View File

@@ -0,0 +1,82 @@
import prisma from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import { Context } from "elysia";
import fs from "fs/promises";
import path from "path";
type FormUpdate = Prisma.PerbekelDariMasaKeMasaGetPayload<{
select: {
id: true;
nama: true;
periode: true;
imageId: true;
daerah: true;
};
}>;
export default async function profileMantanPerbekelUpdate(context: Context) {
try {
const id = context.params?.id;
const body = (await context.body) as Omit<FormUpdate, "id">;
const { nama, periode, imageId, daerah } = body;
if (!id) {
return {
success: false,
message: "ID tidak diberikan",
};
}
const existing = await prisma.perbekelDariMasaKeMasa.findUnique({
where: { id },
include: {
image: true,
},
});
if (!existing) {
return {
success: false,
message: "Profile mantan perbekel tidak ditemukan",
};
}
if (existing.imageId && existing.imageId !== imageId) {
const oldImage = existing.image;
if (oldImage) {
try {
const filePath = path.join(oldImage.path, oldImage.name);
await fs.unlink(filePath);
await prisma.fileStorage.delete({
where: { id: oldImage.id },
});
} catch (err) {
console.error("Gagal hapus gambar lama:", err);
}
}
}
const updated = await prisma.perbekelDariMasaKeMasa.update({
where: { id },
data: {
nama,
periode,
imageId,
daerah,
},
});
return {
success: true,
message: "Success update profile mantan perbekel",
data: updated,
};
} catch (error) {
console.error("Error updating profile mantan perbekel:", error);
return {
success: false,
message: "Terjadi kesalahan saat mengupdate profile mantan perbekel",
};
}
}

View File

@@ -1,12 +1,65 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export const fileStorageFindMany = async (context: Context) => {
const category = context.query?.category as string | undefined;
const data = await prisma.fileStorage.findMany({
where: category ? { category } : {},
});
return { data };
type WhereClause = {
category?: string;
isActive?: boolean;
OR?: Array<{
name?: { contains: string; mode: 'insensitive' };
realName?: { contains: string; mode: 'insensitive' };
}>;
};
export const fileStorageFindMany = async (context: Context) => {
try {
// Get query parameters with defaults
const page = Math.max(Number(context.query?.page) || 1, 1);
const limit = 10; // Fixed at 10 items per page
const category = context.query?.category as string | undefined;
const search = context.query?.search as string | undefined;
const skip = (page - 1) * limit;
// Build where clause with proper TypeScript types
const where: WhereClause = { isActive: true };
if (category) where.category = category;
if (search) {
where.OR = [
{ name: { contains: search, mode: 'insensitive' } },
{ realName: { contains: search, mode: 'insensitive' } },
];
}
// Get paginated data and total count
const [data, total] = await Promise.all([
prisma.fileStorage.findMany({
where,
skip,
take: limit,
orderBy: { createdAt: 'desc' },
}),
prisma.fileStorage.count({ where }),
]);
return {
data,
meta: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
};
} catch (error) {
console.error('Error in fileStorageFindMany:', error);
return {
data: [],
meta: {
page: 1,
limit: 10,
total: 0,
totalPages: 0,
},
};
}
};

View File

@@ -0,0 +1,26 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormCreate = {
name: string;
}
export default async function jenisKelaminRespondenCreate(context: Context) {
const body = (await context.body) as FormCreate;
try {
const result = await prisma.jenisKelaminResponden.create({
data: {
name: body.name,
},
});
return {
success: true,
message: "Berhasil membuat jenis kelamin responden",
data: result,
};
} catch (error) {
console.error("Error creating jenis kelamin responden:", error);
throw new Error("Gagal membuat jenis kelamin responden: " + (error as Error).message);
}
}

View File

@@ -0,0 +1,16 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function jenisKelaminRespondenDelete(context: Context) {
const id = context.params.id as string;
await prisma.jenisKelaminResponden.delete({
where: { id },
});
return {
status: 200,
success: true,
message: "Success delete jenis kelamin responden",
};
}

View File

@@ -0,0 +1,53 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// /api/berita/findManyPaginated.ts
import prisma from "@/lib/prisma";
import { Context } from "elysia";
async function jenisKelaminRespondenFindMany(context: Context) {
// Ambil parameter dari query
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || '';
const skip = (page - 1) * limit;
// Buat where clause
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ name: { contains: search, mode: 'insensitive' } },
];
}
try {
// Ambil data dan total count secara paralel
const [data, total] = await Promise.all([
prisma.jenisKelaminResponden.findMany({
where,
skip,
take: limit,
orderBy: { createdAt: 'desc' },
}),
prisma.jenisKelaminResponden.count({ where }),
]);
return {
success: true,
message: "Berhasil ambil jenis kelamin responden dengan pagination",
data,
page,
limit,
total,
totalPages: Math.ceil(total / limit),
};
} catch (e) {
console.error("Error di findMany paginated:", e);
return {
success: false,
message: "Gagal mengambil data jenis kelamin responden",
};
}
}
export default jenisKelaminRespondenFindMany;

View File

@@ -0,0 +1,46 @@
import prisma from "@/lib/prisma";
export default async function jenisKelaminRespondenFindUnique(request: Request) {
const url = new URL(request.url);
const pathSegments = url.pathname.split('/');
const id = pathSegments[pathSegments.length - 1];
if (!id) {
return {
success: false,
message: "ID is required",
}
}
try {
if (typeof id !== 'string') {
return {
success: false,
message: "ID is required",
}
}
const data = await prisma.jenisKelaminResponden.findUnique({
where: { id },
});
if (!data) {
return {
success: false,
message: "Data not found",
}
}
return {
success: true,
message: "Success get jenis kelamin responden",
data,
}
} catch (error) {
console.error("Find by ID error:", error);
return {
success: false,
message: "Gagal mengambil data: " + (error instanceof Error ? error.message : 'Unknown error'),
}
}
}

View File

@@ -0,0 +1,33 @@
import Elysia, { t } from "elysia";
import jenisKelaminRespondenCreate from "./create";
import jenisKelaminRespondenDelete from "./del";
import jenisKelaminRespondenFindMany from "./findMany";
import jenisKelaminRespondenFindUnique from "./findUnique";
import jenisKelaminRespondenUpdate from "./updt";
const JenisKelaminResponden = new Elysia({
prefix: "/jeniskelaminresponden",
tags: ["PPID / Indeks Kepuasan / Jenis Kelamin Responden"],
})
.post("/create", jenisKelaminRespondenCreate, {
body: t.Object({
name: t.String(),
}),
})
.get("/findMany", jenisKelaminRespondenFindMany)
.get("/:id", async (context) => {
const response = await jenisKelaminRespondenFindUnique(
new Request(context.request)
);
return response;
})
.put("/:id", jenisKelaminRespondenUpdate, {
body: t.Object({
name: t.String(),
}),
})
.delete("/del/:id", jenisKelaminRespondenDelete);
export default JenisKelaminResponden;

View File

@@ -0,0 +1,28 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormUpdate = {
name: string;
}
export default async function jenisKelaminRespondenUpdate(context: Context) {
const body = (await context.body) as FormUpdate;
const id = context.params.id as string;
try {
const result = await prisma.jenisKelaminResponden.update({
where: { id },
data: {
name: body.name,
},
});
return {
success: true,
message: "Berhasil mengupdate jenis kelamin responden",
data: result,
};
} catch (error) {
console.error("Error updating jenis kelamin responden:", error);
throw new Error("Gagal mengupdate jenis kelamin responden: " + (error as Error).message);
}
}

View File

@@ -0,0 +1,26 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormCreate = {
name: string;
}
export default async function pilihanRatingRespondenCreate(context: Context) {
const body = (await context.body) as FormCreate;
try {
const result = await prisma.pilihanRatingResponden.create({
data: {
name: body.name,
},
});
return {
success: true,
message: "Berhasil membuat pilihan rating responden",
data: result,
};
} catch (error) {
console.error("Error creating pilihan rating responden:", error);
throw new Error("Gagal membuat pilihan rating responden: " + (error as Error).message);
}
}

View File

@@ -0,0 +1,16 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function pilihanRatingRespondenDelete(context: Context) {
const id = context.params.id as string;
await prisma.pilihanRatingResponden.delete({
where: { id },
});
return {
status: 200,
success: true,
message: "Success delete pilihan rating responden",
};
}

View File

@@ -0,0 +1,53 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// /api/berita/findManyPaginated.ts
import prisma from "@/lib/prisma";
import { Context } from "elysia";
async function pilihanRatingRespondenFindMany(context: Context) {
// Ambil parameter dari query
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || '';
const skip = (page - 1) * limit;
// Buat where clause
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ name: { contains: search, mode: 'insensitive' } },
];
}
try {
// Ambil data dan total count secara paralel
const [data, total] = await Promise.all([
prisma.pilihanRatingResponden.findMany({
where,
skip,
take: limit,
orderBy: { createdAt: 'desc' },
}),
prisma.pilihanRatingResponden.count({ where }),
]);
return {
success: true,
message: "Berhasil ambil pilihan rating responden dengan pagination",
data,
page,
limit,
total,
totalPages: Math.ceil(total / limit),
};
} catch (e) {
console.error("Error di findMany paginated:", e);
return {
success: false,
message: "Gagal mengambil data pilihan rating responden",
};
}
}
export default pilihanRatingRespondenFindMany;

View File

@@ -0,0 +1,46 @@
import prisma from "@/lib/prisma";
export default async function pilihanRatingRespondenFindUnique(request: Request) {
const url = new URL(request.url);
const pathSegments = url.pathname.split('/');
const id = pathSegments[pathSegments.length - 1];
if (!id) {
return {
success: false,
message: "ID is required",
}
}
try {
if (typeof id !== 'string') {
return {
success: false,
message: "ID is required",
}
}
const data = await prisma.pilihanRatingResponden.findUnique({
where: { id },
});
if (!data) {
return {
success: false,
message: "Data not found",
}
}
return {
success: true,
message: "Success get pilihan rating responden",
data,
}
} catch (error) {
console.error("Find by ID error:", error);
return {
success: false,
message: "Gagal mengambil data: " + (error instanceof Error ? error.message : 'Unknown error'),
}
}
}

View File

@@ -0,0 +1,33 @@
import Elysia, { t } from "elysia";
import pilihanRatingRespondenCreate from "./create";
import pilihanRatingRespondenDelete from "./del";
import pilihanRatingRespondenFindMany from "./findMany";
import pilihanRatingRespondenFindUnique from "./findUnique";
import pilihanRatingRespondenUpdate from "./updt";
const PilihanRatingResponden = new Elysia({
prefix: "/pilihanratingresponden",
tags: ["PPID / Indeks Kepuasan / Pilihan Rating Responden"],
})
.post("/create", pilihanRatingRespondenCreate, {
body: t.Object({
name: t.String(),
}),
})
.get("/findMany", pilihanRatingRespondenFindMany)
.get("/:id", async (context) => {
const response = await pilihanRatingRespondenFindUnique(
new Request(context.request)
);
return response;
})
.put("/:id", pilihanRatingRespondenUpdate, {
body: t.Object({
name: t.String(),
}),
})
.delete("/del/:id", pilihanRatingRespondenDelete);
export default PilihanRatingResponden;

View File

@@ -0,0 +1,28 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormUpdate = {
name: string;
}
export default async function pilihanRatingRespondenUpdate(context: Context) {
const body = (await context.body) as FormUpdate;
const id = context.params.id as string;
try {
const result = await prisma.pilihanRatingResponden.update({
where: { id },
data: {
name: body.name,
},
});
return {
success: true,
message: "Berhasil mengupdate pilihan rating responden",
data: result,
};
} catch (error) {
console.error("Error updating pilihan rating responden:", error);
throw new Error("Gagal mengupdate pilihan rating responden: " + (error as Error).message);
}
}

View File

@@ -0,0 +1,43 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormCreate = {
name: string;
tanggal: string;
jenisKelaminId: string;
ratingId: string;
kelompokUmurId: string;
}
export default async function respondenCreate(context: Context) {
const body = (await context.body) as FormCreate;
try {
// Convert the date string to a Date object
const tanggal = new Date(body.tanggal);
// Validate the date
if (isNaN(tanggal.getTime())) {
throw new Error('Tanggal tidak valid');
}
const result = await prisma.responden.create({
data: {
name: body.name,
tanggal: tanggal, // Use the Date object
jenisKelaminId: body.jenisKelaminId,
ratingId: body.ratingId,
kelompokUmurId: body.kelompokUmurId,
},
});
return {
success: true,
message: "Berhasil membuat responden",
data: result,
};
} catch (error) {
console.error("Error creating responden:", error);
throw new Error("Gagal membuat responden: " + (error as Error).message);
}
}

View File

@@ -0,0 +1,33 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function respondenDelete(context: Context) {
const id = context.params?.id as string;
const responden = await prisma.responden.findUnique({
where: { id },
include: {
jenisKelamin: true,
rating: true,
kelompokUmur: true,
},
});
if (!responden) {
return {
status: 404,
success: false,
message: "Responden tidak ditemukan",
};
}
await prisma.responden.delete({
where: { id },
});
return {
status: 200,
success: true,
message: "Success delete responden",
};
}

View File

@@ -0,0 +1,61 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// /api/berita/findManyPaginated.ts
import prisma from "@/lib/prisma";
import { Context } from "elysia";
async function respondenFindMany(context: Context) {
// Ambil parameter dari query
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || '';
const skip = (page - 1) * limit;
// Buat where clause
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ tanggal: { contains: search, mode: 'insensitive' } },
{ jenisKelamin: { name: { contains: search, mode: 'insensitive' } } },
{ rating: { name: { contains: search, mode: 'insensitive' } } },
{ kelompokUmur: { name: { contains: search, mode: 'insensitive' } } }
];
}
try {
// Ambil data dan total count secara paralel
const [data, total] = await Promise.all([
prisma.responden.findMany({
where,
include: {
jenisKelamin: true,
rating: true,
kelompokUmur: true,
},
skip,
take: limit,
orderBy: { createdAt: 'desc' },
}),
prisma.responden.count({ where }),
]);
return {
success: true,
message: "Berhasil ambil responden dengan pagination",
data,
page,
limit,
total,
totalPages: Math.ceil(total / limit),
};
} catch (e) {
console.error("Error di findMany paginated:", e);
return {
success: false,
message: "Gagal mengambil data responden",
};
}
}
export default respondenFindMany;

View File

@@ -0,0 +1,51 @@
import prisma from "@/lib/prisma";
export default async function respondenFindUnique(request: Request) {
const url = new URL(request.url);
const pathSegments = url.pathname.split('/');
const id = pathSegments[pathSegments.length - 1];
if (!id) {
return {
success: false,
message: "ID is required",
}
}
try {
if (typeof id !== 'string') {
return {
success: false,
message: "ID is required",
}
}
const data = await prisma.responden.findUnique({
where: { id },
include: {
jenisKelamin: true,
rating: true,
kelompokUmur: true,
},
});
if (!data) {
return {
success: false,
message: "Data not found",
}
}
return {
success: true,
message: "Success fetch responden by ID",
data,
}
} catch (e) {
console.error("Find by ID error:", e);
return {
success: false,
message: "Gagal mengambil responden: " + (e instanceof Error ? e.message : 'Unknown error'),
}
}
}

View File

@@ -0,0 +1,42 @@
import Elysia from "elysia";
import { t } from "elysia";
import respondenFindMany from "./findMany";
import respondenFindUnique from "./findUnique";
import respondenCreate from "./create";
import respondenUpdate from "./updt";
import respondenDelete from "./del";
const Responden = new Elysia({ prefix: "/responden", tags: ["Desa/Responden"] })
.get("/findMany", respondenFindMany)
.get("/:id", async (context) => {
const response = await respondenFindUnique(new Request(context.request));
return response;
})
.post("/create", respondenCreate, {
body: t.Object({
name: t.String(),
tanggal: t.String(),
jenisKelaminId: t.String(),
ratingId: t.String(),
kelompokUmurId: t.String(),
}),
})
.delete("/del/:id", respondenDelete)
.put(
"/:id",
async (context) => {
const response = await respondenUpdate(context);
return response;
},
{
body: t.Object({
name: t.String(),
tanggal: t.String(),
jenisKelaminId: t.String(),
ratingId: t.String(),
kelompokUmurId: t.String(),
}),
}
);
export default Responden;

View File

@@ -0,0 +1,36 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormUpdate = {
name: string;
tanggal: string;
jenisKelaminId: string;
ratingId: string;
kelompokUmurId: string;
}
export default async function respondenUpdate(context: Context) {
const body = (await context.body) as FormUpdate;
const id = context.params.id as string;
try {
const result = await prisma.responden.update({
where: { id },
data: {
name: body.name,
tanggal: body.tanggal,
jenisKelaminId: body.jenisKelaminId,
ratingId: body.ratingId,
kelompokUmurId: body.kelompokUmurId,
},
});
return {
success: true,
message: "Berhasil mengupdate responden",
data: result,
};
} catch (error) {
console.error("Error updating responden:", error);
throw new Error("Gagal mengupdate responden: " + (error as Error).message);
}
}

View File

@@ -0,0 +1,26 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormCreate = {
name: string;
}
export default async function umurRespondenCreate(context: Context) {
const body = (await context.body) as FormCreate;
try {
const result = await prisma.umurResponden.create({
data: {
name: body.name,
},
});
return {
success: true,
message: "Berhasil membuat umur responden",
data: result,
};
} catch (error) {
console.error("Error creating umur responden:", error);
throw new Error("Gagal membuat umur responden: " + (error as Error).message);
}
}

View File

@@ -0,0 +1,16 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function umurRespondenDelete(context: Context) {
const id = context.params.id as string;
await prisma.umurResponden.delete({
where: { id },
});
return {
status: 200,
success: true,
message: "Success delete umur responden",
};
}

View File

@@ -0,0 +1,53 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// /api/berita/findManyPaginated.ts
import prisma from "@/lib/prisma";
import { Context } from "elysia";
async function umurRespondenFindMany(context: Context) {
// Ambil parameter dari query
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || '';
const skip = (page - 1) * limit;
// Buat where clause
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ name: { contains: search, mode: 'insensitive' } },
];
}
try {
// Ambil data dan total count secara paralel
const [data, total] = await Promise.all([
prisma.umurResponden.findMany({
where,
skip,
take: limit,
orderBy: { createdAt: 'desc' },
}),
prisma.umurResponden.count({ where }),
]);
return {
success: true,
message: "Berhasil ambil umur responden dengan pagination",
data,
page,
limit,
total,
totalPages: Math.ceil(total / limit),
};
} catch (e) {
console.error("Error di findMany paginated:", e);
return {
success: false,
message: "Gagal mengambil data umur responden",
};
}
}
export default umurRespondenFindMany;

View File

@@ -0,0 +1,46 @@
import prisma from "@/lib/prisma";
export default async function umurRespondenFindUnique(request: Request) {
const url = new URL(request.url);
const pathSegments = url.pathname.split('/');
const id = pathSegments[pathSegments.length - 1];
if (!id) {
return {
success: false,
message: "ID is required",
}
}
try {
if (typeof id !== 'string') {
return {
success: false,
message: "ID is required",
}
}
const data = await prisma.umurResponden.findUnique({
where: { id },
});
if (!data) {
return {
success: false,
message: "Data not found",
}
}
return {
success: true,
message: "Success get umur responden",
data,
}
} catch (error) {
console.error("Find by ID error:", error);
return {
success: false,
message: "Gagal mengambil data: " + (error instanceof Error ? error.message : 'Unknown error'),
}
}
}

View File

@@ -0,0 +1,33 @@
import Elysia, { t } from "elysia";
import umurRespondenCreate from "./create";
import umurRespondenDelete from "./del";
import umurRespondenFindMany from "./findMany";
import umurRespondenFindUnique from "./findUnique";
import umurRespondenUpdate from "./updt";
const UmurResponden = new Elysia({
prefix: "/umurresponden",
tags: ["PPID / Indeks Kepuasan / Umur Responden"],
})
.post("/create", umurRespondenCreate, {
body: t.Object({
name: t.String(),
}),
})
.get("/findMany", umurRespondenFindMany)
.get("/:id", async (context) => {
const response = await umurRespondenFindUnique(
new Request(context.request)
);
return response;
})
.put("/:id", umurRespondenUpdate, {
body: t.Object({
name: t.String(),
}),
})
.delete("/del/:id", umurRespondenDelete);
export default UmurResponden;

View File

@@ -0,0 +1,28 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormUpdate = {
name: string;
}
export default async function umurRespondenUpdate(context: Context) {
const body = (await context.body) as FormUpdate;
const id = context.params.id as string;
try {
const result = await prisma.umurResponden.update({
where: { id },
data: {
name: body.name,
},
});
return {
success: true,
message: "Berhasil mengupdate umur responden",
data: result,
};
} catch (error) {
console.error("Error updating umur responden:", error);
throw new Error("Gagal mengupdate umur responden: " + (error as Error).message);
}
}

View File

@@ -8,6 +8,10 @@ import SDGSDesa from "./sdgs-desa";
import APBDes from "./apbdes";
import PrestasiDesa from "./prestasi-desa";
import KategoriPrestasi from "./prestasi-desa/kategori-prestasi";
import JenisKelaminResponden from "./indeks_kepuasan/jenis-kelamin-responden";
import PilihanRatingResponden from "./indeks_kepuasan/pilihan-rating-responden";
import UmurResponden from "./indeks_kepuasan/umur-responden";
import Responden from "./indeks_kepuasan/responden";
const LandingPage = new Elysia({
prefix: "/api/landingpage",
@@ -23,5 +27,9 @@ const LandingPage = new Elysia({
.use(APBDes)
.use(PrestasiDesa)
.use(KategoriPrestasi)
.use(JenisKelaminResponden)
.use(PilihanRatingResponden)
.use(UmurResponden)
.use(Responden)
export default LandingPage

View File

@@ -124,7 +124,7 @@ export default function Content({ kategori }: { kategori: string }) {
p="lg"
radius="md"
withBorder
onClick={() => router.push(`/desa/berita/${item.id}`)}
onClick={() => router.push(`/darmasaba/desa/berita/${item.id}`)}
style={{ cursor: 'pointer' }}
>
<Card.Section>

View File

@@ -94,7 +94,7 @@ function Semua() {
<Button
variant="light"
rightSection={<IconArrowRight size={16} />}
onClick={() => router.push(`/desa/berita/${featuredData.id}`)}
onClick={() => router.push(`/darmasaba/desa/berita/${featuredData.kategoriBerita?.name}/${featuredData.id}`)}
>
Baca Selengkapnya
</Button>
@@ -155,7 +155,7 @@ function Semua() {
})}
</Text>
<Button p="xs" variant="light" rightSection={<IconArrowRight size={16} />} onClick={() => router.push(`/desa/berita/${item.id}`)}>Baca Selengkapnya</Button>
<Button p="xs" variant="light" rightSection={<IconArrowRight size={16} />} onClick={() => router.push(`/darmasaba/desa/berita/${item.kategoriBerita?.name}/${item.id}`)}>Baca Selengkapnya</Button>
</Flex>
</Card>
))}

View File

@@ -1,64 +0,0 @@
import colors from '@/con/colors';
import { SimpleGrid, Box, Paper, Center, Stack, Image, Text } from '@mantine/core';
import React from 'react';
const data = [
{
id: 1,
image: "/api/img/galeri-1.png",
title: "Pendapatan",
tanggal: "3 Mar 2025",
judul: "Pemasangan Wifi Gratis Di Publik Desa",
},
{
id: 2,
image: "/api/img/galeri-2.png",
title: "Belanja",
tanggal: "4 Mar 2025",
judul: "Panen raya Desa Darmasaba",
},
{
id: 3,
image: "/api/img/galeri-3.png",
title: "Pembiayaan",
tanggal: "5 Mar 2025",
judul: "Kegiatan Pembangunan Pelinggih",
}
]
function Foto() {
return (
<Box pt={20}>
<SimpleGrid
cols={{
base: 1,
md: 3,
}}>
{data.map((v, k) => {
return (
<Box key={k}>
<Paper mb={50} p={"md"} radius={26} bg={colors['white-trans-1']} w={{ base: "100%", md: "100%" }}>
<Box>
<Center>
<Image src={v.image} alt='' />
</Center>
</Box>
<Box>
<Stack gap={"sm"} py={10}>
<Text fz={{ base: "sm", md: "sm" }}>{v.tanggal}</Text>
<Text fw={"bold"} fz={{ base: "sm", md: "sm" }}>{v.judul}</Text>
<Text ta={"justify"} fz={{ base: "sm", md: "sm" }}>Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Fusce sagittis nec arcu ac ornare. Praesent a porttitor
felis. Proin varius ex nisl, in hendrerit odio tristique vel. </Text>
</Stack>
</Box>
</Paper>
</Box>
)
})}
</SimpleGrid>
</Box>
);
}
export default Foto;

View File

@@ -1,64 +0,0 @@
import colors from '@/con/colors';
import { Box, Center, Paper, SimpleGrid, Stack, Text } from '@mantine/core';
const data = [
{
id: 1,
video: "https://www.youtube.com/embed/J2uZcZlvL7g?si=3pWy0ho77dW0E2Gt",
tanggal: "3 Mar 2025",
judul: "MENERIMA KUNJUNGAN STUDI TIRU DARI PEMERINTAH DESA TUA SULAWESI SELATAN",
},
{
id: 2,
video: "https://www.youtube.com/embed/GX4sqS5zAzw?si=rulOAa2Ylbs4_R82",
tanggal: "4 Mar 2025",
judul: "Sosialisasi Pengelolaan Sampah di SD NO 3 Desa Darmasaba",
},
{
id: 3,
video: "https://www.youtube.com/embed/HCY4H6ODmeA?si=0epW8PAtd6Jum90k",
tanggal: "5 Mar 2025",
judul: "Posyandu dan Senam Lansia Banjar Gulingan",
}
]
function Video() {
return (
<Box pt={20}>
<SimpleGrid
cols={{
base: 1,
md: 3,
}}>
{data.map((v, k) => {
return (
<Box key={k}>
<Paper mb={50} p={"md"} radius={26} bg={colors['white-trans-1']} w={{ base: "100%", md: "100%" }}>
<Box>
<Center>
<Box style={{ maxWidth: "560px", width: "100%", aspectRatio: "16/9" }}>
<iframe style={{ borderRadius: "16px" }} width="100%"
height="100%"
src={v.video} title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" ></iframe>
</Box>
</Center>
</Box>
<Box>
<Stack gap={"sm"} py={10}>
<Text fz={{ base: "sm", md: "sm" }}>{v.tanggal}</Text>
<Text fw={"bold"} fz={{ base: "sm", md: "sm" }} lineClamp={1}>{v.judul}</Text>
<Text ta={"justify"} fz={{ base: "sm", md: "sm" }}>Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Fusce sagittis nec arcu ac ornare. Praesent a porttitor
felis. Proin varius ex nisl, in hendrerit odio tristique vel. </Text>
</Stack>
</Box>
</Paper>
</Box>
)
})}
</SimpleGrid>
</Box>
);
}
export default Video;

View File

@@ -0,0 +1,124 @@
'use client';
import { useEffect, useState } from 'react';
import { usePathname, useRouter } from 'next/navigation';
import { Box, Container, Grid, GridCol, Stack, Tabs, TabsList, TabsTab, Text } from '@mantine/core';
import BackButton from '../../layanan/_com/BackButto';
import dynamic from 'next/dynamic';
import type { SearchBarProps } from './searchBar';
import colors from '@/con/colors';
// Define tabs outside the component to ensure consistency between server and client
const TABS = [
{
label: "Foto",
value: "foto",
href: "/darmasaba/desa/galery/foto",
},
{
label: "Video",
value: "video",
href: "/darmasaba/desa/galery/video",
},
] as const;
const SearchBar = dynamic<SearchBarProps>(
() => import('./searchBar').then(mod => mod.SearchBar),
{ ssr: false }
);
type HeaderSearchProps = {
children?: React.ReactNode;
};
function LayoutTabsGalery({ children }: HeaderSearchProps) {
const router = useRouter();
const pathname = usePathname();
const [isClient, setIsClient] = useState(false);
// Set default active tab to empty string to prevent hydration mismatch
const [activeTab, setActiveTab] = useState('');
// Set client flag on mount
useEffect(() => {
setIsClient(true);
}, []);
// Update active tab based on current route - only on client side
useEffect(() => {
if (!isClient) return;
const currentTab = TABS.find(tab => pathname.includes(tab.value));
if (currentTab) {
setActiveTab(currentTab.value);
} else {
// Default to first tab if no match found
setActiveTab(TABS[0].value);
}
}, [pathname, isClient]);
const handleTabChange = (value: string | null) => {
if (!value) return;
const tab = TABS.find(tab => tab.value === value);
if (tab) {
// Only update if we're on the client
if (typeof window !== 'undefined') {
setActiveTab(value);
router.push(tab.href);
}
}
};
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
{/* 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">
Galeri Kegiatan Desa Darmasaba
</Text>
</Stack>
</Container>
<Tabs
value={isClient ? activeTab : undefined}
defaultValue={TABS[0].value}
onChange={handleTabChange}
color={colors['blue-button']}
variant="pills"
keepMounted={false}
>
<Box px={{ base: "md", md: 100 }} py="md" bg={colors['BG-trans']}>
<Grid>
<GridCol span={{ base: 12, md: 9, lg: 8, xl: 9 }}>
<TabsList>
{TABS.map((tab) => (
<TabsTab
key={tab.value}
value={tab.value}
component="button"
type="button"
>
{tab.label}
</TabsTab>
))}
</TabsList>
</GridCol>
<GridCol span={{ base: 12, md: 3, lg: 4, xl: 3 }}>
<SearchBar />
</GridCol>
</Grid>
</Box>
<Container size={'xl'}>
{children}
</Container>
</Tabs>
</Stack>
);
}
export default LayoutTabsGalery;

View File

@@ -0,0 +1,77 @@
// src/app/darmasaba/(pages)/desa/galery/SearchBar.tsx
'use client';
import { TextInput } from '@mantine/core';
import { IconSearch } from '@tabler/icons-react';
import { usePathname, useSearchParams } from 'next/navigation';
import { useEffect, useRef, useState } from 'react';
export type SearchBarProps = {
placeholder?: string;
searchIcon?: React.ReactNode;
};
export function SearchBar({
placeholder = "pencarian",
searchIcon = <IconSearch size={20} />,
}: SearchBarProps) {
const searchParams = useSearchParams();
// Get initial search value from URL
const [searchValue, setSearchValue] = useState(searchParams.get('search') || '');
const pathname = usePathname();
const typingTimeoutRef = useRef<number | null>(null);
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
setSearchValue(value);
// Clear previous timeout
if (typingTimeoutRef.current) {
window.clearTimeout(typingTimeoutRef.current);
}
// Set new timeout
typingTimeoutRef.current = window.setTimeout(() => {
const params = new URLSearchParams(searchParams.toString());
// Always reset to first page when search changes
params.set('page', '1');
if (value) {
params.set('search', value);
} else {
params.delete('search');
}
// Update URL without adding to history
const newUrl = `${pathname}?${params.toString()}`;
window.history.replaceState({ ...window.history.state, as: newUrl, url: newUrl }, '', newUrl);
// Dispatch a custom event that content components can listen to
window.dispatchEvent(new CustomEvent('searchUpdate', { detail: { search: value } }));
}, 500);
};
// Clean up timeout on unmount
useEffect(() => {
return () => {
if (typingTimeoutRef.current) {
window.clearTimeout(typingTimeoutRef.current);
}
};
}, []);
return (
<TextInput
radius="lg"
placeholder={placeholder}
leftSection={searchIcon}
w="100%"
value={searchValue}
onChange={handleSearchChange}
/>
);
}

View File

@@ -0,0 +1,183 @@
'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);
// Handle search and pagination changes
const loadData = useCallback((pageNum: number, searchTerm: string) => {
setLoading(true);
// Using the load function from the component's scope
const loadFn = async () => {
try {
const response = await ApiFetch.api.fileStorage.findMany.get({
query: {
category: 'image',
page: pageNum.toString(),
limit: '10',
...(searchTerm && { search: searchTerm })
}
});
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);
}
};
loadFn();
}, []);
// Initial load and URL change handler
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);
};
// Handle search updates from the search bar
const handleSearchUpdate = (e: Event) => {
const { search } = (e as CustomEvent).detail;
setSearch(search);
setPage(1); // Reset to first page on new search
loadData(1, search);
};
// Initial load
handleRouteChange();
// Set up event listeners
window.addEventListener('popstate', handleRouteChange);
window.addEventListener('searchUpdate', handleSearchUpdate as EventListener);
// Cleanup
return () => {
window.removeEventListener('popstate', handleRouteChange);
window.removeEventListener('searchUpdate', handleSearchUpdate as EventListener);
};
}, [loadData]);
// ✅ Fetch data
useEffect(() => {
const fetchFiles = async () => {
setLoading(true);
try {
const query: Record<string, string> = {
category: 'image',
page: page.toString(),
limit: '10',
};
if (search) query.search = search;
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('Fetch error:', err);
setFiles([]);
} finally {
setLoading(false);
}
};
if (page > 0) fetchFiles(); // jangan fetch jika page belum valid
}, [search, page]);
// ✅ Update URL
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%' }}
/>
</Box>
<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>
</Box>
</Paper>
))}
</SimpleGrid>
<Center mt="xl">
<Pagination total={totalPages} value={page} onChange={handlePageChange} />
</Center>
</Box>
);
}

View File

@@ -0,0 +1,25 @@
'use client'
import dynamic from 'next/dynamic';
import { Suspense } from 'react';
// ✅ Load komponen tanpa SSR
const FotoContent = dynamic(
() => import('./Content'),
{
ssr: false,
loading: () => <div>Memuat konten...</div>
}
);
function PageContent() {
return (
<Suspense fallback={<div>Memuat...</div>}>
<FotoContent />
</Suspense>
);
}
export default function Page() {
return <PageContent />;
}

View File

@@ -0,0 +1,9 @@
import LayoutTabsGalery from "./_lib/layoutTabs";
export default function LayoutGalery({ children }: { children: React.ReactNode }) {
return (
<LayoutTabsGalery>
{children}
</LayoutTabsGalery>
)
}

View File

@@ -1,63 +0,0 @@
import colors from '@/con/colors';
import { Box, Container, Grid, GridCol, Group, Stack, Tabs, TabsList, TabsPanel, TabsTab, Text, TextInput } from '@mantine/core';
import { IconPhoto, IconSearch, IconVideo } from '@tabler/icons-react';
import BackButton from '../layanan/_com/BackButto';
import Foto from './(tabs)/foto';
import Video from './(tabs)/video';
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="xs" mb="xl">
<Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" ta="center">
Galeri Kegiatan Desa Darmasaba
</Text>
</Stack>
</Container>
<Box px={{ base: "md", md: 100 }}>
<Tabs color={colors['blue-button']} defaultValue="foto">
<Grid align='center'>
<GridCol span={{ base: 12, md: 9, lg: 8, xl: 9 }}>
<Group>
<TabsList>
<TabsTab style={{ color: colors['blue-button'] }} fz={"xl"} value="foto" leftSection={<IconPhoto color={colors['blue-button']} size={45} />}>
Foto
</TabsTab>
<TabsTab style={{ color: colors['blue-button'] }} fz={"xl"} value="video" leftSection={<IconVideo color={colors['blue-button']} size={45} />}>
Video
</TabsTab>
</TabsList>
</Group>
</GridCol>
<GridCol span={{ base: 12, md: 3, lg: 4, xl: 3 }}>
<TextInput
w={{ base: "100%", md: "100%" }}
radius="lg"
placeholder="Cari Berita"
leftSection={<IconSearch size={18} />}
/>
</GridCol>
</Grid>
<TabsPanel value="foto">
<Foto />
</TabsPanel>
<TabsPanel value="video">
<Video />
</TabsPanel>
</Tabs>
</Box>
</Stack>
);
}
export default Page;

View File

@@ -0,0 +1,151 @@
'use client';
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
import colors from '@/con/colors';
import { Box, Center, Pagination, Paper, SimpleGrid, Spoiler, Stack, Text } from '@mantine/core';
import { useCallback, useEffect, useState } from 'react';
import { useSnapshot } from 'valtio';
export default function VideoContent() {
const [expanded, setExpanded] = useState(false);
const videoState = useSnapshot(stateGallery.video);
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());
}, []);
// Initial load and URL change handler
useEffect(() => {
const handleRouteChange = () => {
const urlParams = new URLSearchParams(window.location.search);
const urlSearch = urlParams.get('search') || '';
const urlPage = parseInt(urlParams.get('page') || '1');
loadData(urlPage, urlSearch);
};
// Handle search updates from the search bar
const handleSearchUpdate = (e: Event) => {
const { search } = (e as CustomEvent).detail;
loadData(1, search);
};
// Initial load
handleRouteChange();
// Set up event listeners
window.addEventListener('popstate', handleRouteChange);
window.addEventListener('searchUpdate', handleSearchUpdate as EventListener);
// Cleanup
return () => {
window.removeEventListener('popstate', handleRouteChange);
window.removeEventListener('searchUpdate', handleSearchUpdate as EventListener);
};
}, [loadData]);
const handlePageChange = (newPage: number) => {
const params = new URLSearchParams(window.location.search);
const search = params.get('search') || '';
loadData(newPage, search);
};
const dataVideo = data || [];
if (loading && !data) {
return (
<Box py={10}>
<Text>Memuat Video...</Text>
</Box>
);
}
return (
<Box pt={20} px={{ base: 'md', md: 100 }}>
<SimpleGrid cols={{ base: 1, md: 3 }}>
{dataVideo.map((v, k) => (
<Box key={k}>
<Paper mb={50} p="md" radius={26} bg={colors['white-trans-1']} w={{ base: '100%', md: '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={expanded}
onExpandedChange={setExpanded}
>
<Text
ta="justify"
fz="sm"
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
/>
</Spoiler>
</Stack>
</Box>
</Paper>
</Box>
))}
</SimpleGrid>
<Center>
<Pagination
value={page}
onChange={handlePageChange}
total={totalPages}
mt="md"
mb="md"
/>
</Center>
</Box>
);
}
// ✅ Fix: HAPUS SPASI BERLEBIH DI URL
function convertToEmbedUrl(youtubeUrl: string): string {
try {
const url = new URL(youtubeUrl);
const videoId = url.searchParams.get('v');
if (!videoId) return youtubeUrl;
return `https://www.youtube.com/embed/${videoId}`; // ✅ tanpa spasi!
} catch (err) {
console.error('Error converting YouTube URL to embed:', err);
return youtubeUrl;
}
}

View File

@@ -0,0 +1,12 @@
'use client'
import dynamic from 'next/dynamic';
// ✅ Load komponen tanpa SSR
const VideoContent = dynamic(
() => import('./Content'),
{ ssr: false, loading: () => <div>Memuat...</div> }
);
export default function Page() {
return <VideoContent />;
}

View File

@@ -7,7 +7,7 @@ import VisimisiDesa from './ui/visimisiDesa';
import LambangDesa from './ui/lambangDesa';
import MaskotDesa from './ui/maskotDesa';
import ProfilPerbekel from './ui/profilPerbekel';
import LembagaDesa from './ui/lembagaDesa';
// import LembagaDesa from './ui/lembagaDesa';
import MotoDesa from './ui/motoDesa';
import SemuaPerbekel from './ui/semuaPerbekel';
@@ -31,7 +31,6 @@ function Page() {
<LambangDesa />
<MaskotDesa />
<ProfilPerbekel />
<LembagaDesa />
<MotoDesa />
<SemuaPerbekel/>
</Box>

View File

@@ -1,8 +1,27 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Stack, Paper, Image, Text, Center } from '@mantine/core';
import React from 'react';
import { Box, Stack, Paper, Image, Text, Center, Skeleton } from '@mantine/core';
import React, { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
function LambangDesa() {
const state = useProxy(stateProfileDesa.lambangDesa)
useEffect(() => {
state.findUnique.load("edit")
}, [])
const { data, loading } = state.findUnique
if (loading || !data) {
return (
<Box py={10}>
<Skeleton h={500} />
</Box>
)
}
return (
<Box pb={70}>
<Stack align='center' gap={0}>
@@ -13,46 +32,8 @@ function LambangDesa() {
<Text c={colors['blue-button']} ta={"center"} fw={"bold"} fz={"2.5rem"}>Lambang Desa</Text>
</Box>
<Paper p={"xl"} bg={colors['white-trans-1']} w={{ base: "100%", md: "100%" }}>
<Text fz={{ base: "md", md: "h3"}} ta={"justify"}>
Simbul atau lambang bukanlah suatu gambar bentuk tanpa mengandung makna, namun segala yang terdapat di dalamnya mengandung makna filosofi yang mendalam dan kepribadian, setidaknya mengandung suatu pengharapan atau tujuan yang ingin dicapai oleh yang empunya lambang tersebut.
</Text>
<Text fz={{ base: "md", md: "h3"}} py={10} ta={"justify"}>
Lambang Desa Darmasaba berbentuk persegi lima sama sisi dengan dasar warna biru yang di dalamnya terdapat bangunan Padmasana dengan rong tiga, di dasarnya terdapat bunga Padma berwarna merah muda yang dikelilingi oleh padi dan kapas serta rantai sebagai pengikatnya, terdapat pula pita berwarna putih bertuliskan Dharma Temaja.
</Text>
<Box>
<Text fz={{ base: "md", md: "h3"}} ta={"justify"} fw={"bold"}>
Bentuk Dasar Persegi Lima
</Text>
<Text fz={{ base: "md", md: "h3"}} ta={"justify"}>
Melambangkan Pancasila yang menjadi dasar Negara Republik Indonesia yang harus dihayati dan diamalkan oleh masyarakat Indonesia termasuk masyarakat Desa Darmaaba merupakan bagian dari Bangsa Indonesia.
</Text>
</Box>
<Box>
<Text fz={{ base: "md", md: "h3"}} ta={"justify"} fw={"bold"}>
Dasar Persegi Lima Yang Berwarna Biru
</Text>
<Text fz={{ base: "md", md: "h3"}} ta={"justify"}>
Mengandung makna wilayah Desa Darmasaba yang luas, tanahnya yg subur dan masyarakatnya memiliki berbagai macam mata pencaharian.
</Text>
</Box>
<Box>
<Text fz={{ base: "md", md: "h3"}} ta={"justify"} fw={"bold"}>
Bangunan Pelinggih Padmasana Rong Tiga
</Text>
<Text fz={{ base: "md", md: "h3"}} ta={"justify"}>
Merupakan perlambang wilayah Desa Darmasaba terdiri dari tiga Desa Adat antara lain Desa Adat Aban, Desa Adat Tegal dan Desa Adat Darmasaba yang dengan rasa persatuan dan kesatuan dilandasi semangat segalak segilik seguluk sebayantaka membangun Desa Darmasaba.
</Text>
</Box>
<Box>
<Text fz={{ base: "md", md: "h3"}} ta={"justify"} fw={"bold"}>
Bunga Padma Merah Muda
</Text>
<Text fz={{ base: "md", md: "h3"}} ta={"justify"}>
Memiliki arti kemuliaan.
</Text>
</Box>
<Text fz={{ base: "md", md: "h3"}} ta={"justify"} dangerouslySetInnerHTML={{__html: data.deskripsi}} />
</Paper>
</Stack>
</Box >
);

View File

@@ -1,39 +0,0 @@
import colors from '@/con/colors';
import { Box, Center, Image, Paper, Stack, Text } from '@mantine/core';
function LembagaDesa() {
return (
<Box pb={70}>
<Stack align='center' gap={0}>
<Box pb={30}>
<Text c={colors['blue-button']} ta={"center"} fw={"bold"} fz={{ base: "h1", md: "2.5rem" }}>Lembaga Desa</Text>
<Text c={colors['blue-button']} ta={"center"} fw={"bold"} fz={{ base: "h4", md: "h2"}}>Badan Permusyawaratan Desa (BPD)</Text>
</Box>
<Paper mb={50} p={"xl"} bg={colors['white-trans-1']} w={{ base: "100%", md: "100%" }}>
<Center pb={10}>
<Image src={"/api/img/bpddarmasaba.png"} alt='' w={{ base: 390, md: 1000 }} />
</Center>
</Paper>
<Box pb={30}>
<Text c={colors['blue-button']} ta={"center"} fw={"bold"} fz={{ base: "h1", md: "2.5rem" }}>Lembaga pemberdayaan Masyarakat (LPM)</Text>
</Box>
<Paper mb={50} p={"xl"} bg={colors['white-trans-1']} w={{ base: "100%", md: "100%" }}>
<Center pb={10}>
<Image src={"/api/img/bpddarmasaba.png"} alt='' w={{ base: 390, md: 1000 }} />
</Center>
</Paper>
<Box pb={30}>
<Text c={colors['blue-button']} ta={"center"} fw={"bold"} fz={{ base: "h1", md: "2.5rem" }}>Perangkat Desa</Text>
<Text c={colors['blue-button']} ta={"center"} fw={"bold"} fz={{ base: "h4", md: "h2"}}>Struktur Organisasi Tata Kerja Pemerintahan Desa Darmasaba</Text>
</Box>
<Paper p={"xl"} bg={colors['white-trans-1']} w={{ base: "100%", md: "100%" }}>
<Center pb={10}>
<Image src={"/api/img/pernagkatdesa.png"} alt='' w={{ base: 390, md: 1000 }} />
</Center>
</Paper>
</Stack>
</Box >
);
}
export default LembagaDesa;

View File

@@ -1,7 +1,27 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors';
import { Box, Center, Image, Paper, SimpleGrid, Stack, Text } from '@mantine/core';
import { Box, Card, Center, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
function MaskotDesa() {
const state = useProxy(stateProfileDesa.maskotDesa)
useEffect(() => {
state.findUnique.load("edit")
}, [])
const { data, loading } = state.findUnique
if (loading || !data) {
return (
<Box py={10}>
<Skeleton h={500} />
</Box>
)
}
return (
<Box pb={70}>
<Stack align='center' gap={0}>
@@ -11,83 +31,24 @@ function MaskotDesa() {
</Center>
<Text c={colors['blue-button']} ta={"center"} fw={"bold"} fz={"2.5rem"}>Maskot Desa</Text>
</Box>
<Paper p={"xl"} bg={colors['white-trans-1']} w={{ base: "100%", md: "100%" }}>
<Text fz={{ base: "md", md: "h3" }} ta={"justify"}>
Pudak adalah bunga dari tanaman sejenis pandan <Text span fz={{ base: "md", md: "h3" }} fs={"italic"}>(Pandanaceae)</Text>. Bentuk bunga ini tersusun dalam beberapa lapisan, terbungkus oleh kelopak warna putih <Text span fz={{ base: "md", md: "h3" }} fs={"italic"}>(semacam daun lonjong)</Text> yang ujungnya meruncing.
</Text>
<Text fz={{ base: "md", md: "h3" }} py={10} ta={"justify"}>
Bunga Pudak berwarna kuning dan akan terlihat jika kelopak atau pelepahnya telah mekar. Kekhasan dari bunga pudak, yaitu mempunyai aroma wangi yang semerbak nan lembut <Text span fz={{ base: "md", md: "h3" }} fs={"italic"}>(tidak menyengat)</Text>, dan dapat menebar keharuman sepanjang pagi atau pun sore hari. Tanaman ini dapat tumbuh di sepanjang pantai, aliran sungai, di atas batu-batu karang, dan juga di tanah ladang.
</Text>
{/* Pohon dan Bunga Pudak */}
<Box py={20}>
<SimpleGrid
cols={{
base: 1,
md: 2,
}}
>
<Center>
<Box>
<Paper p={"lg"}>
<Image src={"/pohonpudak.png"} alt="" w={{ base: 150, md: 250 }} />
<Text ta={"center"} fw={"bold"} c={colors["blue-button"]} fz={{ base: "md", md: "h3" }}>Pohon Pudak</Text>
</Paper>
</Box>
</Center>
<Center>
<Box>
<Paper p={"lg"}>
<Image src={"/bungapudak.png"} alt="" w={{ base: 150, md: 250 }} />
<Text ta={"center"} fw={"bold"} c={colors["blue-button"]} fz={{ base: "md", md: "h3" }}>Bunga Pudak</Text>
</Paper>
</Box>
</Center>
</SimpleGrid>
</Box>
<Box>
<Text fz={{ base: "md", md: "h3" }} py={10} ta={"justify"}>
Dalam Kamus Jawa Kuna- Indonesia kata Pudak berarti bunga pandan atau Pandanus Moschatus (Mardiwarsito: 1981: 442). Selain itu bunga pudak juga dapat disebut ketaka atau ketaki (Mardiwarsito, 1981: 276). Sedangkan kata Sategal berasal dari kata dasar Tegal yang berarti ladang (Mardiwarsito, 1981: 593). Jadi Pudak Sategal dapat diartikan sebagai satu ladang luas yang dipenuhi bunga pudak dan menabar keharuman.
</Text>
<Text fz={{ base: "md", md: "h3" }} py={10} ta={"justify"}>
Pada sebuah kesempatan, Ida Pedanda Putu Pemaron menjelaskan mengenai makna dari istilah Pudak Sategal dengan sebuah analogi bahwa, sekuntum bunga pudak memiliki aroma wangi atau keharuman yang sangat kuat, apalagi jika satu ladang penuh bunga pudak, maka dapat dipastikan aroma keharumannya akan membumbung menyebar ke segala penjuru (Wawancara, 18 Mei 2019 di Geria Putra Mandara Kenderan, Tegallalang).
Pudak ialah sebuah bunga yang memiliki aroma wangi atau keharuman yang semerbak, lembut, dan khas.
</Text>
</Box>
{/* Tari dan Pose Klimaks Sekar Pudak */}
<Box py={20}>
<SimpleGrid
cols={{
base: 1,
md: 2,
}}
>
<Box>
<Center>
<Paper p={"lg"}>
<Image src={"/tarisekar.png"} alt="" w={{ base: 150, md: 250 }} />
<Text ta={"center"} fw={"bold"} c={colors["blue-button"]} fz={{ base: "md", md: "h3" }}>Tari Sekar Pudak</Text>
</Paper>
</Center>
</Box>
<Box>
<Center>
<Paper p={"lg"}>
<Image src={"/klimakstari.png"} alt="" w={{ base: 150, md: 250 }} />
<Text ta={"center"} fw={"bold"} c={colors["blue-button"]} fz={{ base: "md", md: "h3" }}>Pose Klimaks Tari </Text>
<Text ta={"center"} fw={"bold"} c={colors["blue-button"]} fz={{ base: "md", md: "h3" }}>Sekar Pudak </Text>
</Paper>
</Center>
</Box>
</SimpleGrid>
</Box>
<Box>
<Text fz={{ base: "md", md: "h3" }} py={10} ta={"justify"}>
Garapan Tari Maskot Desa Darmasaba Sekar Pudak diwujudkan ke dalam bentuk tari kreasi yang ditarikan secara berkelompok dengan jumlah lima orang penari perempuan (putri).
</Text>
<Text fz={{ base: "md", md: "h3" }} py={10} ta={"justify"}>
Pemilihan penari perempuan dimaksudkan untuk mempresentasikan keindahan, keluwesan, dan keharuman dari bunga pudak. Sedangkan penetapan jumlah penari lima orang didasarkan atas pertimbangan kebutuhan koreografi agar dapat membentuk desain-desain komposisi lantai yang menarik dan dinamis, baik ketika ditarikan di area panggung yang luas atau pun area panggung yang kecil. Penyajian tari maskot ini dirancang dengan durasi waktu 9 menit.
</Text>
</Box>
<Paper p={"xl"} bg={colors['white-trans-1']}>
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: data.deskripsi }} />
<Group wrap="wrap" gap="md">
{data.images.map((img, index) => (
<Card key={index} p="xs" w={220}>
<Image
src={img.image.link}
alt={img.label}
w={200}
h={200}
fit="cover"
radius="md"
style={{ border: '1px solid #ccc', objectFit: 'cover' }}
/>
<Text ta="center" mt="xs" fw="bold">{img.label}</Text>
</Card>
))}
</Group>
</Paper>
</Stack>
</Box >

View File

@@ -1,7 +1,27 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors';
import { Box, Image, List, ListItem, Paper, SimpleGrid, Stack, Text } from '@mantine/core';
import { Box, Image, Paper, SimpleGrid, Skeleton, Stack, Text } from '@mantine/core';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
function ProfilPerbekel() {
const state = useProxy(stateProfileDesa.profilPerbekel)
useEffect(() => {
state.findUnique.load("edit")
}, [])
const { data, loading } = state.findUnique
if (loading || !data) {
return (
<Box py={10}>
<Skeleton h={500} />
</Box>
)
}
return (
<Box pb={70}>
<Stack align='center' gap={0}>
@@ -19,7 +39,16 @@ function ProfilPerbekel() {
<Box>
<Paper bg={colors['white-trans-1']} w={{ base: "100%", md: "100%" }}>
<Stack gap={0}>
<Image pt={{ base: 0, md: 120 }} px={"lg"} src={"/api/img/perbekel.png"} sizes='100%' alt='' />
<Image
pt={{ base: 0, md: 120 }}
px={"lg"}
src={data.image?.link || "/perbekel.png"}
sizes='100%'
alt=''
onError={(e) => {
e.currentTarget.src = "/perbekel.png";
}}
/>
<Paper
bg={colors['blue-button']}
px={"lg"}
@@ -35,101 +64,30 @@ function ProfilPerbekel() {
</Stack>
</Paper>
</Box>
<Box>
<Paper p={"xl"} bg={colors['white-trans-1']} w={{ base: "100%", md: "100%" }}>
<Text fz={"h2"} fw={"bold"}>
Biodata
</Text>
<Text fz={{ base: "md", md: "h3" }} ta={"justify"}>
I.B Surya Prabhawa Manuaba, S.H., M.H., adalah Perbekel Darmasaba periode 2021-2027, seorang advokat, pendiri Mantra Legal Consultants & Advocates, serta aktif di bidang musik dan akademis. Dia menempuh pendidikan hukum di Universitas Udayana dan Universitas Mahasaraswati Denpasar serta memiliki pengalaman luas di berbagai organisasi dan kepemimpinan.
</Text>
<Text py={10} fz={"h2"} fw={"bold"}>
Pengalaman
</Text>
<List>
<Box px={20}>
<ListItem fz={{ base: "md", md: "h3" }} ta={"justify"}>2021 - 2027: Perbekel Desa Darmasaba</ListItem>
<ListItem fz={{ base: "md", md: "h3" }} ta={"justify"}>2015 - Sekarang: Founder & Managing Director Mantra
Legal Consultants & Advocates</ListItem>
<ListItem fz={{ base: "md", md: "h3" }} ta={"justify"}>2020 - Sekarang: Founder Ugawa Record Music Studio</ListItem>
<ListItem fz={{ base: "md", md: "h3" }} >2010 - 2016: Dosen Fakultas Hukum Universitas
Mahasaraswati Denpasar</ListItem>
</Box>
</List>
<Text py={10} fz={"h2"} fw={"bold"}>
Pengalaman Organisasi
</Text>
<List>
<Box px={20}>
<ListItem fz={{ base: "md", md: "h3" }} ta={"justify"}>1996 - 1997: Ketua OSIS SMP Negeri 1 Abiansemal</ListItem>
<ListItem fz={{ base: "md", md: "h3" }} ta={"justify"}>1999 - 2000: Ketua OSIS SMA Negeri 1 Mengwi</ListItem>
<ListItem fz={{ base: "md", md: "h3" }} ta={"justify"}>2008 - 2009: Ketua BEM Universitas Mahasaraswati Denpasar</ListItem>
<ListItem fz={{ base: "md", md: "h3" }} >2008 - 2010: Ketua Sekaa Taruna Sila Dharma, Banjar Tengah, Desa Adat Tegal, Darmasaba</ListItem>
<ListItem fz={{ base: "md", md: "h3" }} >2020 - Sekarang: Pengurus Young Lawyer Committee Peradi Denpasar</ListItem>
<ListItem fz={{ base: "md", md: "h3" }} >2021 - Sekarang: Dewan Kehormatan Himpunan Pengusaha Muda Indonesia (HIPMI) Badung</ListItem>
<ListItem fz={{ base: "md", md: "h3" }} >2023 - 2028: Komite Tetap Advokasi - Bidang Hukum dan Regulasi Kamar Dagang dan Industri Badung</ListItem>
</Box>
</List>
</Paper>
</Box>
</SimpleGrid>
<Paper p={"xl"} bg={colors['white-trans-1']} w={{ base: "100%", md: "100%" }}>
<Box>
<Text fz={{ base: "1.125rem", md: "1.6rem" }} fw={'bold'}>Biodata</Text>
<Text fz={{ base: "1rem", md: "1.5rem" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: data.biodata }} />
</Box>
<Box>
<Text fz={{ base: "1.125rem", md: "1.6rem" }} fw={'bold'}>Pengalaman</Text>
<Text fz={{ base: "1rem", md: "1.5rem" }} dangerouslySetInnerHTML={{ __html: data.pengalaman }} />
</Box>
</Paper>
</SimpleGrid >
<Paper p={"xl"} bg={colors['white-trans-1']} w={{ base: "100%", md: "100%" }}>
<Text py={10} fz={{ base: "md", md: "h1" }} fw={"bold"} >
Program Kerja Unggulan
</Text>
<Stack>
<Box>
<Text fz={{ base: "md", md: "h3" }} fw={"bold"} ta={"justify"}>
Pemberdayaan Ekonomi dan UMKM
</Text>
<List>
<Box px={10}>
<ListItem fz={{ base: "md", md: "h3" }} ta={"justify"}>Pelatihan dan pendampingan UMKM lokal</ListItem>
<ListItem fz={{ base: "md", md: "h3" }} ta={"justify"}>Program bantuan modal usaha bagi pelaku usaha kecil</ListItem>
<ListItem fz={{ base: "md", md: "h3" }} ta={"justify"}>Digitalisasi UMKM untuk meningkatkan pemasaran produk lokal</ListItem>
</Box>
</List>
<Box>
<Text fz={{ base: "1.125rem", md: "1.6rem" }} fw={'bold'}>Pengalaman Organisasi</Text>
<Text fz={{ base: "1rem", md: "1.5rem" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: data.pengalamanOrganisasi }} />
</Box>
<Box pb={20}>
<Text fz={{ base: "1.125rem", md: "1.6rem" }} fw={'bold'}>Program Kerja Unggulan</Text>
<Box px={20}>
<Text fz={{ base: "1rem", md: "1.5rem" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: data.programUnggulan }} />
</Box>
<Box>
<Text fz={{ base: "md", md: "h3" }} fw={"bold"} ta={"justify"}>
Peningkatan Infrastruktur Desa
</Text>
<List>
<Box px={10}>
<ListItem fz={{ base: "md", md: "h3" }} ta={"justify"}>Pembangunan dan perbaikan jalan desa</ListItem>
<ListItem fz={{ base: "md", md: "h3" }} ta={"justify"}>Penyediaan fasilitas umum dan ruang terbuka hijau</ListItem>
<ListItem fz={{ base: "md", md: "h3" }} ta={"justify"}>Optimalisasi layanan publik berbasis digital</ListItem>
</Box>
</List>
</Box>
<Box>
<Text fz={{ base: "md", md: "h3" }} fw={"bold"} ta={"justify"}>
Pendidikan dan Pengembangan SDM
</Text>
<List>
<Box px={10}>
<ListItem fz={{ base: "md", md: "h3" }} ta={"justify"}>Beasiswa pendidikan bagi siswa berprestasi dari keluarga kurang mampu</ListItem>
<ListItem fz={{ base: "md", md: "h3" }} ta={"justify"}>Program kursus dan pelatihan kerja bagi pemuda desa</ListItem>
<ListItem fz={{ base: "md", md: "h3" }} ta={"justify"}>Peningkatan kualitas pendidikan melalui kerja sama dengan perguruan tinggi</ListItem>
</Box>
</List>
</Box>
<Box>
<Text fz={{ base: "md", md: "h3" }} fw={"bold"} ta={"justify"}>
Pelestarian Budaya dan Pariwisata
</Text>
<List>
<Box px={10}>
<ListItem fz={{ base: "md", md: "h3" }} ta={"justify"}>Revitalisasi pura dan tempat bersejarah di Darmasaba</ListItem>
<ListItem fz={{ base: "md", md: "h3" }} ta={"justify"}>Pengembangan desa wisata berbasis budaya dan seni lokal</ListItem>
<ListItem fz={{ base: "md", md: "h3" }} ta={"justify"}>Festival budaya dan seni sebagai daya tarik wisata</ListItem>
</Box>
</List>
</Box>
</Stack>
</Box>
</Paper>
</Box>
</Box >
);
}

View File

@@ -1,7 +1,28 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors';
import { Box, Center, Image, Paper, Stack, Text } from '@mantine/core';
import { Box, Center, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
function SejarahDesa() {
const state = useProxy(stateProfileDesa.sejarahDesa)
useEffect(() => {
state.findUnique.load("edit")
}, [])
const { data, loading } = state.findUnique
if (loading || !data) {
return (
<Box py={10}>
<Skeleton h={500} />
</Box>
)
}
return (
<>
<Box pb={70}>
@@ -12,16 +33,8 @@ function SejarahDesa() {
</Center>
<Text c={colors['blue-button']} ta={"center"} fw={"bold"} fz={"2.5rem"}>Sejarah Desa</Text>
</Box>
<Paper p={"xl"} bg={colors['white-trans-1']} w={{ base: "100%", md: "100%" }}>
<Text fz={{ base: "md", md: "h3" }} ta={"justify"}>
Asal usul nama Darmasaba tertuang dalam lontar Usada Bali. Seperti di tulis dalam monografi Desa Darmasaba tahun 1980 silam, nama Darmasaba berkaitan dengan keturunan Danghyang Nirarta diceritakan, Sang kawi-wiku asal Daha (Jawa Timur) itu memiliki cucu bernama Ida Pedanda Sakti Manuaba yang tigggal di Desa Kendran Tegalalang Gianyar.
</Text>
<Text fz={{ base: "md", md: "h3" }} py={10} ta={"justify"}>
Merasa tidak disenangi sang ayah, Ida Pedanda Sakti Manuaba pergi mengembara bersama dua orang pengiringnya. Pengembaraan sang pendeta sampai di pura Sarin Buana di Jimbaran. Saat mengadakan semedi di tempat ini sang pendeta melihat sinar api. Yang sangat jauh di utara. Timbul keinginan Ida Pedanda Manuaba untuk mengunjungi tempat itu.
</Text>
<Text fz={{ base: "md", md: "h3" }} ta={"justify"}>
Sampailah sang Pedanda di pura Batan Bila Peguyangan. Disini Ida Pedanda Manuaba singgah menghadap Ida Pedanda Budha yang tinggal disana. Selanjutnya, kedua pendeta bersama-sama menuju arah utara dan singgah di Taman Cang Ana, sebuah taman milik Arya Lanang Blusung. Di tempat ini kedua pendeta bersama-sama melaksanakan semedi dan menetap untuk sementara waktu.
</Text>
<Paper p={"xl"} bg={colors['white-trans-1']}>
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: data.deskripsi }} />
</Paper>
</Stack>

View File

@@ -1,73 +1,25 @@
'use client'
import colors from '@/con/colors';
import { Box, Center, Image, Paper, SimpleGrid, Stack, Text } from '@mantine/core';
const data = [
{
id: 1,
nama: "SI GEDE KANIA",
wilayah: "PERBEKEL TEGAL",
periode: "Tahun 1943 - 1946",
foto: "/api/img/perbekel-1.png"
},
{
id: 2,
nama: "SI GEDE GANDEM",
wilayah: "PERBEKEL TEGAL",
periode: "Tahun 1946 - 1950",
foto: "/api/img/perbekel-2.png"
},
{
id: 3,
nama: "I WAYAN SAMA",
wilayah: "PERBEKEL TEGAL",
periode: "Tahun 1950 - 1953",
foto: "/api/img/perbekel-3.png"
},
{
id: 4,
nama: "I WAYAN NAMBREG",
wilayah: "PERBEKEL DARMASABA",
periode: "Tahun 1950 - 1960",
foto: "/api/img/perbekel-4.png"
},
{
id: 5,
nama: "IDA BAGUS PUTU OKA",
wilayah: "PERBEKEL TEGAL/DARMASABA",
periode: "Tahun 1953 - 1974",
foto: "/api/img/perbekel-5.png"
},
{
id: 6,
nama: "I NYOMAN PATRA",
wilayah: "PERBEKEL DARMASABA",
periode: "Tahun 1974 - 1991",
foto: "/api/img/perbekel-6.png"
},
{
id: 7,
nama: "I MADE RUDJA",
wilayah: "PERBEKEL DARMASABA",
periode: "Tahun 1991 - 2007",
foto: "/api/img/perbekel-7.png"
},
{
id: 8,
nama: "I WAYAN KALER, SH.MH.",
wilayah: "PERBEKEL DARMASABA",
periode: "Tahun 2007 - 2013",
foto: "/api/img/perbekel-8.png"
},
{
id: 9,
nama: "I MADE TARAM, SH.",
wilayah: "PERBEKEL DARMASABA",
periode: "Tahun 2013 - 2019",
foto: "/api/img/perbekel-9.png"
},
]
import { Box, Center, Image, Paper, SimpleGrid, Skeleton, Stack, Text } from '@mantine/core';
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import { useProxy } from 'valtio/utils';
import { useShallowEffect } from '@mantine/hooks';
function SemuaPerbekel() {
const state = useProxy(stateProfileDesa.mantanPerbekel)
useShallowEffect(() => {
state.findMany.load()
}, [])
const {data, loading} = state.findMany
if (loading || !data) {
return (
<Box py={10}>
<Skeleton h={500} />
</Box>
)
}
return (
<Box pb={50}>
@@ -84,16 +36,16 @@ function SemuaPerbekel() {
{data.map((v, k) => {
return (
<Box key={k}>
<Paper mb={50} radius={26} bg={colors['white-trans-1']} w={{ base: "100%", md: "100%" }}>
<Paper h={620} mb={50} radius={26} bg={colors['white-trans-1']} w={{ base: "100%", md: "100%" }}>
<Box>
<Center>
<Image src={v.foto} alt='' />
<Image src={v.image?.link} alt='' />
</Center>
</Box>
<Box>
<Stack gap={"sm"} py={10}>
<Text ta={"center"} fw={"bold"} fz={{ base: "h3", md: "h3" }}>{v.nama}</Text>
<Text ta={"center"} fz={{ base: "h5", md: "h4" }}>{v.wilayah}</Text>
<Text ta={"center"} fz={{ base: "h5", md: "h4" }}>{v.daerah}</Text>
<Text ta={"center"} fz={{ base: "h5", md: "h4" }}>{v.periode}</Text>
</Stack>
</Box>

View File

@@ -1,8 +1,27 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors';
import { Box, Stack, Paper, Image, Text, ListItem, List } from '@mantine/core';
import React from 'react';
import { Box, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
function VisimisiDesa() {
const state = useProxy(stateProfileDesa.visiMisiDesa)
useEffect(() => {
state.findUnique.load("edit")
}, [])
const { data, loading } = state.findUnique
if (loading || !data) {
return (
<Box py={10}>
<Skeleton h={500} />
</Box>
)
}
return (
<>
<Box pb={30}>
@@ -12,24 +31,12 @@ function VisimisiDesa() {
</Box>
<Paper p={"xl"} bg={colors['white-trans-1']} w={{ base: "100%", md: "100%" }}>
<Text c={colors['blue-button']} ta={"center"} fw={"lighter"} fz={"2.5rem"}>Visi Desa</Text>
<Text fz={"1.5rem"} ta={"center"} fw={"bold"}>
Mewujudkan Desa Darmasaba yang sejahtera, unggul, religius, berbudaya, dan aman dengan berlandaskan Tri Hita Karana
</Text>
<Text fz={"1.5rem"} ta={"center"} fw={"bold"} dangerouslySetInnerHTML={{ __html: data.visi }} />
</Paper>
<Paper my={20} p={"xl"} bg={colors['white-trans-1']} w={{ base: "100%", md: "100%" }}>
<Text c={colors['blue-button']} ta={"center"} fw={"lighter"} fz={"2.5rem"}>Misi Desa</Text>
<Box >
<List type='ordered'>
<ListItem fz={{ base: "md", md: "h3"}} ta={"justify"}>Memperkokoh kerukunan hidup masyarakat dalam jalinan adat, budaya, olahraga, dan agama.</ListItem>
<ListItem fz={{ base: "md", md: "h3"}} ta={"justify"}>Meningkatkan kualitas pelayanan publik dengan menerapkan teknologi informasi dan komunikasi terintegrasi.</ListItem>
<ListItem fz={{ base: "md", md: "h3"}} ta={"justify"}>Meningkatkan tata kelola pemerintah desa dengan menerapkan prinsip good governance dan good clean goverment.</ListItem>
<ListItem fz={{ base: "md", md: "h3"}} ta={"justify"}>Meningkatkan kualitas pendidikan, kesehatan, Keluarga Berencana serta pengelolaan kependudukan.</ListItem>
<ListItem fz={{ base: "md", md: "h3"}} ta={"justify"}>Memperkuat usaha mikro kecil dan menengah (UMKM) dan BUMDesa sebagai pilar ekonomi masyarakat.</ListItem>
<ListItem fz={{ base: "md", md: "h3"}} ta={"justify"}>Mewujudkan tatanan kehidupan bermasyarakat yang menjunjung tinggi penegakan hukum dan HAM. </ListItem>
<ListItem fz={{ base: "md", md: "h3"}} ta={"justify"}>Meningkatkan perlindungan dan pengelolaan terhadap sumber daya alam dan lingkungan hidup.</ListItem>
<ListItem fz={{ base: "md", md: "h3"}} ta={"justify"}>Memperkuat daya saing desa melalui peningkatan mutu sumber daya manusia dan infrastruktur desa berbasis potensi desa. </ListItem>
<ListItem fz={{ base: "md", md: "h3"}} ta={"justify"}>Meningkatkan sinergisitas potensi budaya, pertanian dalam arti luas dan pariwisata.</ListItem>
</List>
<Box>
<Text fz={"1.5rem"} fw={"bold"} dangerouslySetInnerHTML={{ __html: data.misi }} />
</Box>
</Paper>
</Stack>

View File

@@ -97,7 +97,7 @@ function PengaduanMasyarakat() {
>
<Paper p="md" withBorder>
<Stack gap="xs">
<Title order={3}>Ajukan Administrasi Online</Title>
<Title order={3}>Ajukan Pengaduan Masyarakat</Title>
<TextInput
label={<Text fz="sm" fw="bold">Nama</Text>}
placeholder="masukkan nama"

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