Fix QC Kak Inno & Kak Ayu Tanggal 15 Oct

This commit is contained in:
2025-10-17 10:03:03 +08:00
parent 0b574406e2
commit 75bf0652b1
25 changed files with 1420 additions and 356 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -82,6 +82,7 @@
"react-simple-toasts": "^6.1.0",
"react-toastify": "^11.0.5",
"react-transition-group": "^4.4.5",
"react-zoom-pan-pinch": "^3.7.0",
"readdirp": "^4.1.1",
"recharts": "^2.15.3",
"sharp": "^0.34.3",

View File

@@ -1606,7 +1606,7 @@ model Pembiayaan {
ApbDesa ApbDesa[] @relation("ApbDesaPembiayaan")
}
// ========================================= INOVASI ========================================= //
// ========================================= MENU INOVASI ========================================= //
// ========================================= DESA DIGITAL / SMART VILLAGE ========================================= //
model DesaDigital {
id String @id @default(cuid())

View File

@@ -75,7 +75,8 @@ const berita = proxy({
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "", kategori = "") => {
berita.findMany.loading = true; // ✅ Akses langsung via nama path
const startTime = Date.now();
berita.findMany.loading = true;
berita.findMany.page = page;
berita.findMany.search = search;
@@ -98,7 +99,14 @@ const berita = proxy({
berita.findMany.data = [];
berita.findMany.totalPages = 1;
} finally {
// pastikan minimal 300ms sebelum loading = false (biar UX smooth)
const elapsed = Date.now() - startTime;
const minDelay = 300;
const delay = elapsed < minDelay ? minDelay - elapsed : 0;
setTimeout(() => {
berita.findMany.loading = false;
}, delay);
}
},
},

View File

@@ -68,6 +68,7 @@ const dataPerpustakaan = proxy({
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "", kategori = "") => {
const startTime = Date.now();
dataPerpustakaan.findMany.loading = true; // ✅ Akses langsung via nama path
dataPerpustakaan.findMany.page = page;
dataPerpustakaan.findMany.search = search;
@@ -77,7 +78,10 @@ const dataPerpustakaan = proxy({
if (search) query.search = search;
if (kategori) query.kategori = kategori;
const res = await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan["findMany"].get({ query });
const res =
await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan[
"findMany"
].get({ query });
if (res.status === 200 && res.data?.success) {
dataPerpustakaan.findMany.data = res.data.data ?? [];
@@ -91,7 +95,14 @@ const dataPerpustakaan = proxy({
dataPerpustakaan.findMany.data = [];
dataPerpustakaan.findMany.totalPages = 1;
} finally {
// pastikan minimal 300ms sebelum loading = false (biar UX smooth)
const elapsed = Date.now() - startTime;
const minDelay = 300;
const delay = elapsed < minDelay ? minDelay - elapsed : 0;
setTimeout(() => {
dataPerpustakaan.findMany.loading = false;
}, delay);
}
},
},
@@ -115,7 +126,10 @@ const dataPerpustakaan = proxy({
if (search) query.search = search;
if (kategori) query.kategori = kategori;
const res = await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan["findManyAll"].get({ query });
const res =
await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan[
"findManyAll"
].get({ query });
if (res.status === 200 && res.data?.success) {
dataPerpustakaan.findManyAll.data = res.data.data ?? [];
@@ -365,7 +379,10 @@ const kategoriBuku = proxy({
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.pendidikan.perpustakaandigital.kategoribuku["findMany"].get({ query });
const res =
await ApiFetch.api.pendidikan.perpustakaandigital.kategoribuku[
"findMany"
].get({ query });
if (res.status === 200 && res.data?.success) {
kategoriBuku.findMany.data = res.data.data ?? [];
@@ -557,7 +574,7 @@ const templatePeminjamanBuku = z.object({
tanggalPinjam: z.string().min(1, "Tanggal Pinjam harus diisi"),
batasKembali: z.string().min(1, "Batas Kembali harus diisi"),
tanggalKembali: z.string().min(1, "Tanggal Kembali harus diisi"),
catatan: z.string().min(1, "Catatan harus diisi")
catatan: z.string().min(1, "Catatan harus diisi"),
});
const defaultPeminjamanBuku = {
@@ -568,7 +585,7 @@ const defaultPeminjamanBuku = {
tanggalPinjam: "",
batasKembali: "",
tanggalKembali: "",
catatan: ""
catatan: "",
};
interface FormEditData {
@@ -584,7 +601,7 @@ interface FormEditData {
batasKembali: string;
tanggalKembali: string;
catatan: string;
status: 'Dipinjam' | 'Dikembalikan' | 'Terlambat' | 'Dibatalkan';
status: "Dipinjam" | "Dikembalikan" | "Terlambat" | "Dibatalkan";
}
const editForm: FormEditData = {
@@ -596,8 +613,8 @@ const editForm: FormEditData = {
batasKembali: "",
tanggalKembali: "",
catatan: "",
status: "Dipinjam"
}
status: "Dipinjam",
};
const peminjamanBuku = proxy({
create: {
@@ -651,7 +668,10 @@ const peminjamanBuku = proxy({
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.pendidikan.perpustakaandigital.peminjamanbuku["findMany"].get({ query });
const res =
await ApiFetch.api.pendidikan.perpustakaandigital.peminjamanbuku[
"findMany"
].get({ query });
if (res.status === 200 && res.data?.success) {
peminjamanBuku.findMany.data = res.data.data ?? [];
@@ -720,7 +740,9 @@ const peminjamanBuku = proxy({
);
await peminjamanBuku.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus Data Peminjaman Buku");
toast.error(
result?.message || "Gagal menghapus Data Peminjaman Buku"
);
}
} catch (error) {
console.error("Gagal delete:", error);
@@ -768,7 +790,7 @@ const peminjamanBuku = proxy({
batasKembali: data.batasKembali,
tanggalKembali: data.tanggalKembali,
catatan: data.catatan,
status: data.status
status: data.status,
};
return data; // Return the loaded data
} else {
@@ -811,7 +833,7 @@ const peminjamanBuku = proxy({
batasKembali: this.form.batasKembali,
tanggalKembali: this.form.tanggalKembali,
catatan: this.form.catatan,
status: this.form.status
status: this.form.status,
}),
}
);
@@ -830,7 +852,9 @@ const peminjamanBuku = proxy({
await peminjamanBuku.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal update data peminjaman buku");
throw new Error(
result.message || "Gagal update data peminjaman buku"
);
}
} catch (error) {
console.error("Error updating data peminjaman buku:", error);
@@ -849,7 +873,7 @@ const peminjamanBuku = proxy({
peminjamanBuku.update.form = { ...editForm };
},
},
})
});
const perpustakaanDigitalState = proxy({
dataPerpustakaan,

View File

@@ -701,6 +701,457 @@ export default async function searchFindMany(context: Context) {
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
// ========================================= MENU INOVASI ========================================= //
// ========================================= DESA DIGITAL / SMART VILLAGE ========================================= //
if (type === "desaDigital") {
const data = await prisma.desaDigital.findMany({
where: {
OR: [
{ name: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } }
],
},
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
// ========================================= PROGRAM KREATIF ========================================= //
if (type === "programKreatif") {
const data = await prisma.programKreatif.findMany({
where: {
OR: [
{ name: { contains: query, mode: "insensitive" } },
{ slug: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } }
],
},
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
// ========================================= KOLABORASI INOVASI ========================================= //
if (type === "kolaborasiInovasi") {
const data = await prisma.kolaborasiInovasi.findMany({
where: {
OR: [
{ name: { contains: query, mode: "insensitive" } },
{ slug: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } },
{ kolaborator: { contains: query, mode: "insensitive" } }
],
},
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
if (type === "mitraKolaborasi") {
const data = await prisma.mitraKolaborasi.findMany({
where: {
OR: [
{ name: { contains: query, mode: "insensitive" } }
],
},
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
// ========================================= INFO TEKHNOLOGI TEPAT GUNA ========================================= //
if (type === "infoTekno") {
const data = await prisma.infoTekno.findMany({
where: {
OR: [
{ name: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } }
],
},
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
// ========================================= LINGKUNGAN ========================================= //
// ========================================= PENGELOLAAN SAMPAH ========================================= //
if (type === "pengelolaanSampah") {
const data = await prisma.pengelolaanSampah.findMany({
where: {
OR: [
{ name: { contains: query, mode: "insensitive" } }
],
},
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
if (type === "keteranganBankSampahTerdekat") {
const data = await prisma.keteranganBankSampahTerdekat.findMany({
where: {
OR: [
{ name: { contains: query, mode: "insensitive" } },
{ alamat: { contains: query, mode: "insensitive" } },
{ namaTempatMaps: { contains: query, mode: "insensitive" } },
{ linkPetunjukArah: { contains: query, mode: "insensitive" } }
],
},
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
// ========================================= PORGRAM PENGHIJAUAN ========================================= //
if (type === "programPenghijauan") {
const data = await prisma.programPenghijauan.findMany({
where: {
OR: [
{ name: { contains: query, mode: "insensitive" } },
{ judul: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } }
],
},
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
// ========================================= DATA LINGKUNGAN DESA ========================================= //
if (type === "dataLingkunganDesa") {
const data = await prisma.dataLingkunganDesa.findMany({
where: {
OR: [
{ name: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } }
],
},
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
// ========================================= GOTONG ROYONG ========================================= //
if (type === "gotongRoyong") {
const data = await prisma.kegiatanDesa.findMany({
where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
{ deskripsiSingkat: { contains: query, mode: "insensitive" } },
{ deskripsiLengkap: { contains: query, mode: "insensitive" } }
],
},
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
// ========================================= EDUKASI LINGKUNGAN ========================================= //
if (type === "tujuanEdukasiLingkungan") {
const data = await prisma.tujuanEdukasiLingkungan.findMany({
where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } }
],
},
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
if (type === "materiEdukasiLingkungan") {
const data = await prisma.materiEdukasiLingkungan.findMany({
where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } }
],
},
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
if (type === "contohEdukasiLingkungan") {
const data = await prisma.contohEdukasiLingkungan.findMany({
where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } }
],
},
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
// ========================================= KONSERVASI ADAT BALI ========================================= //
if (type === "filosofiTriHita") {
const data = await prisma.filosofiTriHita.findMany({
where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } }
],
},
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
if (type === "bentukKonservasiBerdasarkanAdat") {
const data = await prisma.bentukKonservasiBerdasarkanAdat.findMany({
where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } }
],
},
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
if (type === "nilaiKonservasiAdat") {
const data = await prisma.nilaiKonservasiAdat.findMany({
where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } }
],
},
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
// ========================================= MENU PENDIDIKAN ========================================= //
// ========================================= INFO SEKOLAH & PAUD ========================================= //
if (type === "jenjangPendidikan") {
const data = await prisma.jenjangPendidikan.findMany({
where: {
OR: [
{ nama: { contains: query, mode: "insensitive" } }
],
},
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
if (type === "lembaga") {
const data = await prisma.lembaga.findMany({
where: {
OR: [
{ nama: { contains: query, mode: "insensitive" } }
],
},
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
if (type === "siswa") {
const data = await prisma.siswa.findMany({
where: {
OR: [
{ nama: { contains: query, mode: "insensitive" } }
],
},
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
if (type === "pengajar") {
const data = await prisma.pengajar.findMany({
where: {
OR: [
{ nama: { contains: query, mode: "insensitive" } }
],
},
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
// ========================================= BEASISWA DESA ========================================= //
if (type === "keunggulanProgram") {
const data = await prisma.keunggulanProgram.findMany({
where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } }
],
},
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
// ========================================= PROGRAM PENDIDIKAN ANAK ========================================= //
if (type === "tujuanProgram") {
const data = await prisma.tujuanProgram.findMany({
where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } }
],
},
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
if (type === "programUnggulan") {
const data = await prisma.programUnggulan.findMany({
where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } }
],
},
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
if (type === "lokasiJadwalBimbinganBelajarDesa") {
const data = await prisma.lokasiJadwalBimbinganBelajarDesa.findMany({
where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } }
],
},
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
if (type === "fasilitasBimbinganBelajarDesa") {
const data = await prisma.fasilitasBimbinganBelajarDesa.findMany({
where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } }
],
},
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
// ========================================= PENDIDIKAN NON FORMAL ========================================= //
if (type === "tujuanPendidikanNonFormal") {
const data = await prisma.tujuanPendidikanNonFormal.findMany({
where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } }
],
},
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
if (type === "tempatKegiatan") {
const data = await prisma.tempatKegiatan.findMany({
where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } }
],
},
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
if (type === "jenisProgramYangDiselenggarakan") {
const data = await prisma.jenisProgramYangDiselenggarakan.findMany({
where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } }
],
},
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
// ========================================= PERPUSTAKAAN ========================================= //
if (type === "dataPerpustakaan") {
const data = await prisma.dataPerpustakaan.findMany({
where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } }
],
},
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
// ========================================= DATA PENDIDIKAN ========================================= //
if (type === "dataPendidikan") {
const data = await prisma.dataPendidikan.findMany({
where: {
OR: [
{ name: { contains: query, mode: "insensitive" } },
{ jumlah: { contains: query, mode: "insensitive" } }
],
},
skip,
take: limitNum,
});
return { data, nextPage: data.length < limitNum ? null : pageNum + 1 };
}
// 🌍 GLOBAL SEARCH — cari di beberapa modul sekaligus
const [
pejabatdesa,
@@ -760,6 +1211,37 @@ export default async function searchFindMany(context: Context) {
programKemiskinan,
sektorUnggulanDesa,
demografiPekerjaan,
desaDigital,
programKreatif,
kolaborasiInovasi,
mitraKolaborasi,
infoTekno,
pengelolaanSampah,
keteranganBankSampahTerdekat,
programPenghijauan,
dataLingkunganDesa,
gotongRoyong,
tujuanEdukasiLingkungan,
materiEdukasiLingkungan,
contohEdukasiLingkungan,
filosofiTriHita,
bentukKonservasiBerdasarkanAdat,
nilaiKonservasiAdat,
jenjangPendidikan,
lembaga,
siswa,
pengajar,
keunggulanProgram,
tujuanProgram,
programUnggulan,
lokasiJadwalBimbinganBelajarDesa,
fasilitasBimbinganBelajarDesa,
tujuanPendidikanNonFormal,
tempatKegiatan,
jenisProgramYangDiselenggarakan,
dataPerpustakaan,
dataPendidikan
] = await Promise.all([
prisma.pejabatDesa.findMany({
where: { name: { contains: query, mode: "insensitive" } },
@@ -1097,6 +1579,277 @@ export default async function searchFindMany(context: Context) {
pekerjaan: { contains: query, mode: "insensitive" }
},
take: limitNum,
}),
prisma.desaDigital.findMany({
where: {
OR: [
{ name: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } }
],
},
take: limitNum,
}),
prisma.programKreatif.findMany({
where: {
OR: [
{ name: { contains: query, mode: "insensitive" } },
{ slug: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } }
],
},
take: limitNum,
}),
prisma.kolaborasiInovasi.findMany({
where: {
OR: [
{ name: { contains: query, mode: "insensitive" } },
{ slug: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } },
{ kolaborator: { contains: query, mode: "insensitive" } }
],
},
take: limitNum,
}),
prisma.mitraKolaborasi.findMany({
where: {
OR: [
{ name: { contains: query, mode: "insensitive" } }
],
},
take: limitNum,
}),
prisma.infoTekno.findMany({
where: {
OR: [
{ name: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } }
],
},
take: limitNum,
}),
prisma.pengelolaanSampah.findMany({
where: {
OR: [
{ name: { contains: query, mode: "insensitive" } }
],
},
take: limitNum,
}),
prisma.keteranganBankSampahTerdekat.findMany({
where: {
OR: [
{ name: { contains: query, mode: "insensitive" } },
{ alamat: { contains: query, mode: "insensitive" } },
{ namaTempatMaps: { contains: query, mode: "insensitive" } },
{ linkPetunjukArah: { contains: query, mode: "insensitive" } }
],
},
take: limitNum,
}),
prisma.programPenghijauan.findMany({
where: {
OR: [
{ name: { contains: query, mode: "insensitive" } },
{ judul: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } }
],
},
take: limitNum,
}),
prisma.dataLingkunganDesa.findMany({
where: {
OR: [
{ name: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } }
],
},
take: limitNum,
}),
prisma.kegiatanDesa.findMany({
where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
{ deskripsiSingkat: { contains: query, mode: "insensitive" } },
{ deskripsiLengkap: { contains: query, mode: "insensitive" } }
],
},
take: limitNum,
}),
prisma.tujuanEdukasiLingkungan.findMany({
where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } }
],
},
take: limitNum,
}),
prisma.materiEdukasiLingkungan.findMany({
where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } }
],
},
take: limitNum,
}),
prisma.contohEdukasiLingkungan.findMany({
where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } }
],
},
take: limitNum,
}),
prisma.filosofiTriHita.findMany({
where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } }
],
},
take: limitNum,
}),
prisma.bentukKonservasiBerdasarkanAdat.findMany({
where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } }
],
},
take: limitNum,
}),
prisma.nilaiKonservasiAdat.findMany({
where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } }
],
},
take: limitNum,
}),
prisma.jenjangPendidikan.findMany({
where: {
OR: [
{ nama: { contains: query, mode: "insensitive" } }
],
},
take: limitNum,
}),
prisma.lembaga.findMany({
where: {
OR: [
{ nama: { contains: query, mode: "insensitive" } }
],
},
take: limitNum,
}),
prisma.siswa.findMany({
where: {
OR: [
{ nama: { contains: query, mode: "insensitive" } }
],
},
take: limitNum,
}),
prisma.pengajar.findMany({
where: {
OR: [
{ nama: { contains: query, mode: "insensitive" } }
],
},
take: limitNum,
}),
prisma.keunggulanProgram.findMany({
where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } }
],
},
take: limitNum,
}),
prisma.tujuanProgram.findMany({
where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } }
],
},
take: limitNum,
}),
prisma.programUnggulan.findMany({
where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } }
],
},
take: limitNum,
}),
prisma.lokasiJadwalBimbinganBelajarDesa.findMany({
where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } }
],
},
take: limitNum,
}),
prisma.fasilitasBimbinganBelajarDesa.findMany({
where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } }
],
},
take: limitNum,
}),
prisma.tujuanPendidikanNonFormal.findMany({
where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } }
],
},
take: limitNum,
}),
prisma.tempatKegiatan.findMany({
where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } }
],
},
take: limitNum,
}),
prisma.jenisProgramYangDiselenggarakan.findMany({
where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } }
],
},
take: limitNum,
}),
prisma.dataPerpustakaan.findMany({
where: {
OR: [
{ judul: { contains: query, mode: "insensitive" } },
{ deskripsi: { contains: query, mode: "insensitive" } }
],
},
take: limitNum,
}),
prisma.dataPendidikan.findMany({
where: {
OR: [
{ name: { contains: query, mode: "insensitive" } },
{ jumlah: { contains: query, mode: "insensitive" } }
],
},
take: limitNum,
})
]);
@@ -1180,6 +1933,36 @@ export default async function searchFindMany(context: Context) {
...programKemiskinan.map((b) => ({ type: "programKemiskinan", ...b })),
...sektorUnggulanDesa.map((b) => ({ type: "sektorUnggulanDesa", ...b })),
...demografiPekerjaan.map((b) => ({ type: "demografiPekerjaan", ...b })),
...desaDigital.map((b) => ({ type: "desaDigital", ...b })),
...programKreatif.map((b) => ({ type: "programKreatif", ...b })),
...kolaborasiInovasi.map((b) => ({ type: "kolaborasiInovasi", ...b })),
...mitraKolaborasi.map((b) => ({ type: "mitraKolaborasi", ...b })),
...infoTekno.map((b) => ({ type: "infoTekno", ...b })),
...pengelolaanSampah.map((b) => ({ type: "pengelolaanSampah", ...b })),
...keteranganBankSampahTerdekat.map((b) => ({ type: "keteranganBankSampahTerdekat", ...b })),
...programPenghijauan.map((b) => ({ type: "programPenghijauan", ...b })),
...dataLingkunganDesa.map((b) => ({ type: "dataLingkunganDesa", ...b })),
...gotongRoyong.map((b) => ({ type: "gotongRoyong", ...b })),
...tujuanEdukasiLingkungan.map((b) => ({ type: "tujuanEdukasiLingkungan", ...b })),
...materiEdukasiLingkungan.map((b) => ({ type: "materiEdukasiLingkungan", ...b })),
...contohEdukasiLingkungan.map((b) => ({ type: "contohEdukasiLingkungan", ...b })),
...filosofiTriHita.map((b) => ({ type: "filosofiTriHita", ...b })),
...bentukKonservasiBerdasarkanAdat.map((b) => ({ type: "bentukKonservasiBerdasarkanAdat", ...b })),
...nilaiKonservasiAdat.map((b) => ({ type: "nilaiKonservasiAdat", ...b })),
...jenjangPendidikan.map((b) => ({ type: "jenjangPendidikan", ...b })),
...lembaga.map((b) => ({ type: "lembaga", ...b })),
...siswa.map((b) => ({ type: "siswa", ...b })),
...pengajar.map((b) => ({ type: "pengajar", ...b })),
...keunggulanProgram.map((b) => ({ type: "keunggulanProgram", ...b })),
...tujuanProgram.map((b) => ({ type: "tujuanProgram", ...b })),
...programUnggulan.map((b) => ({ type: "programUnggulan", ...b })),
...tujuanPendidikanNonFormal.map((b) => ({ type: "tujuanPendidikanNonFormal", ...b })),
...fasilitasBimbinganBelajarDesa.map((b) => ({ type: "fasilitasBimbinganBelajarDesa", ...b })),
...lokasiJadwalBimbinganBelajarDesa.map((b) => ({ type: "lokasiJadwalBimbinganBelajarDesa", ...b })),
...tempatKegiatan.map((b) => ({ type: "tempatKegiatan", ...b })),
...jenisProgramYangDiselenggarakan.map((b) => ({ type: "jenisProgramYangDiselenggarakan", ...b })),
...dataPerpustakaan.map((b) => ({ type: "dataPerpustakaan", ...b })),
...dataPendidikan.map((b) => ({ type: "dataPendidikan", ...b })),
],
nextPage: null, // bisa dibuat lebih kompleks kalau perlu

View File

@@ -28,6 +28,32 @@ function Page() {
)
}
// Add this check before the return statement
if (data.length === 0) {
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap={22}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
<Text fz={{ base: 'h1', md: '2.5rem' }} c={colors['blue-button']} fw="bold">
Sektor Unggulan Desa Darmasaba
</Text>
<Text c="dimmed" mt="md">
Data sektor unggulan belum tersedia
</Text>
</Box>
</Stack>
);
}
const chartData = data
.filter(item => item?.name && typeof item.value === 'number')
.map((item) => ({
id: item.id,
sektor: item.name,
Ton: item.value,
}));
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}>
@@ -54,11 +80,7 @@ function Page() {
<BarChart
p={10}
h={300}
data={data.map((item) => ({
id: item.id,
sektor: item.name,
Ton: item.value,
}))}
data={chartData}
dataKey="sektor"
series={[
{ name: 'Ton', color: colors['blue-button'] },

View File

@@ -41,6 +41,37 @@ function Page() {
)
}
if (data.length === 0) {
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap={22}>
<Box px={{ base: 'md', md: 100 }}>
<Text fz={{ base: 'h1', md: '2.5rem' }} c={colors['blue-button']} fw="bold">
Pencegahan Kriminalitas
</Text>
<Text c={colors['blue-button']} fz={{ base: 'h4', md: 'h3' }}>
Keamanan Komunitas & Pencegahan Kriminal
</Text>
</Box>
<SimpleGrid
px={{ base: 20, md: 100 }}
cols={{ base: 1, md: 2 }}
spacing="xl"
>
<Paper p="xl" radius="xl" shadow="lg" >
<Text fz={{ base: 'h3', md: 'h2' }} c={colors['blue-button']} fw="bold">
Program Keamanan Berjalan
</Text>
<Stack pt={30} gap="lg">
<Text c="dimmed">
Tidak ada data pencegahan kriminalitas yang cocok
</Text>
</Stack>
</Paper>
</SimpleGrid>
</Stack>
)
}
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap={22}>
<Box px={{ base: 'md', md: 100 }}>

View File

@@ -101,15 +101,30 @@ function Page() {
}}
>
<Stack align="center" gap="sm">
<Box
style={{
width: '100%',
aspectRatio: '16/9',
borderRadius: '12px',
overflow: 'hidden',
position: 'relative',
}}
>
<Image
src={v.image.link}
alt={v.name}
w={140}
h={140}
fit="contain"
radius="md"
fit="cover"
loading="lazy"
style={{
width: '100%',
height: '100%',
transition: 'transform 0.4s ease',
}}
onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.05)')}
onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')}
/>
</Box>
<Text ta="center" fw={700} fz="lg" c={colors['blue-button']}>
{v.name}
</Text>

View File

@@ -2,7 +2,6 @@
import penangananDarurat from '@/app/admin/(dashboard)/_state/kesehatan/penanganan-darurat/penangananDarurat'
import colors from '@/con/colors'
import {
Badge,
Box,
Center,
Grid,
@@ -106,15 +105,30 @@ function Page() {
>
<Stack align="center" gap="md">
<Center>
<Box
style={{
width: '100%',
aspectRatio: '16/9',
borderRadius: '12px',
overflow: 'hidden',
position: 'relative',
}}
>
<Image
src={v.image.link}
alt={v.name}
h={180}
w="100%"
radius="md"
fit="cover"
style={{ aspectRatio: '4/3' }}
loading="lazy"
style={{
width: '100%',
height: '100%',
transition: 'transform 0.4s ease',
}}
onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.05)')}
onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')}
/>
</Box>
</Center>
<Stack gap={4} w="100%">
<Text
@@ -136,9 +150,6 @@ function Page() {
/>
</Box>
</Stack>
<Badge radius="md" color="blue" variant="light" mt="sm">
Darurat
</Badge>
</Stack>
</Paper>
))}

View File

@@ -7,16 +7,18 @@ import { IconCalendar, IconInfoCircle, IconPhone, IconSearch } from "@tabler/ico
import { useState } from "react";
import { useProxy } from "valtio/utils";
import BackButton from "../../desa/layanan/_com/BackButto";
import { useDebouncedValue } from "@mantine/hooks";
export default function Page() {
const state = useProxy(posyandustate);
const [search, setSearch] = useState("");
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const { data, page, totalPages, loading, load } = state.findMany;
useShallowEffect(() => {
load(page, 6, search);
}, [page, search]);
load(page, 6, debouncedSearch);
}, [page, debouncedSearch]);
if (loading || !data) {
return (

View File

@@ -78,9 +78,7 @@ function Page() {
<Tooltip label={materi.data?.judul} position="top" withArrow>
<Stack gap={4} align="center">
<IconRecycle size={28} color={colors['blue-button']} />
<Text fz="h3" fw="bold" c={colors['blue-button']} ta="center">
{materi.data?.judul}
</Text>
<Text fz="h3" fw="bold" c={colors['blue-button']} ta="center" dangerouslySetInnerHTML={{ __html: materi.data?.judul || '' }} />
</Stack>
</Tooltip>
</Box>

View File

@@ -1,6 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID'
import colors from '@/con/colors'
import {
@@ -15,17 +16,27 @@ import {
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
Transition,
} from '@mantine/core'
import { IconRefresh, IconSearch, IconUsers } from '@tabler/icons-react'
import {
IconRefresh,
IconSearch,
IconUsers,
IconZoomIn,
IconZoomOut,
IconArrowsMaximize,
IconArrowsMinimize,
} from '@tabler/icons-react'
import { OrganizationChart } from 'primereact/organizationchart'
import { useEffect } from 'react'
import { useEffect, useRef, useState } from 'react'
import { useProxy } from 'valtio/utils'
import BackButton from '../../desa/layanan/_com/BackButto'
import { useTransitionRouter } from 'next-view-transitions'
import ScrollToTopButton from '@/app/darmasaba/_com/scrollToTopButton'
import { debounce } from 'lodash'
export default function Page() {
return (
@@ -47,7 +58,6 @@ export default function Page() {
ta="center"
c={colors['blue-button']}
fz={{ base: 28, md: 36, lg: 44 }}
>
Struktur Organisasi PPID
</Title>
@@ -70,14 +80,24 @@ export default function Page() {
function StrukturOrganisasiPPID() {
const stateOrganisasi: any = useProxy(stateStrukturPPID.pegawai)
const router = useTransitionRouter()
const chartContainerRef = useRef<HTMLDivElement>(null)
const [scale, setScale] = useState(1)
const [isFullscreen, setFullscreen] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
// debounce untuk pencarian
const debouncedSearch = useRef(
debounce((value: string) => {
setSearchQuery(value)
}, 400)
).current
useEffect(() => {
void stateOrganisasi.findMany.load()
}, [])
const isLoading =
!stateOrganisasi.findMany.data &&
stateOrganisasi.findMany.loading !== false
!stateOrganisasi.findMany.data && stateOrganisasi.findMany.loading !== false
if (isLoading) {
return (
@@ -93,10 +113,7 @@ function StrukturOrganisasiPPID() {
)
}
if (
!stateOrganisasi.findMany.data ||
stateOrganisasi.findMany.data.length === 0
) {
if (!stateOrganisasi.findMany.data || stateOrganisasi.findMany.data.length === 0) {
return (
<Center py={40}>
<Stack align="center" gap="md">
@@ -117,8 +134,7 @@ function StrukturOrganisasiPPID() {
Data pegawai belum tersedia
</Title>
<Text c="dimmed" mt="xs">
Belum ada data pegawai yang tercatat untuk PPID. Silakan coba
muat ulang atau periksa sumber data.
Belum ada data pegawai yang tercatat untuk PPID.
</Text>
<Group justify="center" mt="lg">
<Button
@@ -129,15 +145,6 @@ function StrukturOrganisasiPPID() {
>
Muat Ulang
</Button>
<Button
leftSection={<IconSearch size={16} />}
variant="subtle"
onClick={() =>
stateOrganisasi.findMany.load({ query: { q: '' } })
}
>
Cari Pegawai
</Button>
</Group>
</Paper>
</Stack>
@@ -145,44 +152,39 @@ function StrukturOrganisasiPPID() {
)
}
// Buat struktur organisasi
const posisiMap = new Map<string, any>()
const aktifPegawai = stateOrganisasi.findMany.data.filter((p: any) => p.isActive);
const aktifPegawai = stateOrganisasi.findMany.data.filter((p: any) => p.isActive)
for (const pegawai of aktifPegawai) {
const posisiId = pegawai.posisi.id;
const posisiId = pegawai.posisi.id
if (!posisiMap.has(posisiId)) {
posisiMap.set(posisiId, {
...pegawai.posisi,
pegawaiList: [],
children: [],
});
})
}
posisiMap.get(posisiId)!.pegawaiList.push(pegawai);
posisiMap.get(posisiId)!.pegawaiList.push(pegawai)
}
const root: any[] = []
posisiMap.forEach((posisi) => {
if (posisi.parentId) {
const parent = posisiMap.get(posisi.parentId)
if (parent) {
parent.children.push(posisi)
} else {
root.push(posisi)
}
} else {
root.push(posisi)
}
if (parent) parent.children.push(posisi)
else root.push(posisi)
} else root.push(posisi)
})
function toOrgChartFormat(node: any): any {
const pegawai = node.pegawaiList?.[0];
const pegawai = node.pegawaiList?.[0]
return {
expanded: true,
type: 'person',
styleClass: 'p-person',
data: {
id: pegawai?.id || null, // tambahin ini bro
id: pegawai?.id || null,
name: pegawai?.namaLengkap || 'Belum ditugaskan',
title: node.nama || 'Tanpa jabatan',
image: pegawai?.image?.link || '/img/default.png',
@@ -190,28 +192,90 @@ function StrukturOrganisasiPPID() {
positionId: node.id || null,
},
children: node.children?.map(toOrgChartFormat) || [],
};
}
}
const chartData = root.map(toOrgChartFormat)
let chartData = root.map(toOrgChartFormat)
// 🔍 filter by search
if (searchQuery) {
const filterNodes = (nodes: any[]): any[] =>
nodes
.map((n) => ({
...n,
children: filterNodes(n.children || []),
}))
.filter(
(n) =>
n.data.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
n.data.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
n.children.length > 0
)
chartData = filterNodes(chartData)
}
// 🧭 fungsi fullscreen
const toggleFullscreen = () => {
if (!document.fullscreenElement) {
chartContainerRef.current?.requestFullscreen()
setFullscreen(true)
} else {
document.exitFullscreen()
setFullscreen(false)
}
}
// 🧭 fungsi zoom
const handleZoomIn = () => setScale((prev) => Math.min(prev + 0.1, 2))
const handleZoomOut = () => setScale((prev) => Math.max(prev - 0.1, 0.5))
const resetZoom = () => setScale(1)
return (
<Box py={16} >
<Paper
radius="md"
p="md"
<Stack align="center" mt="xl">
{/* 🔍 Search + Zoom + Fullscreen controls */}
<Group mb="md" justify="center" gap="sm">
<TextInput
placeholder="Cari nama atau jabatan..."
leftSection={<IconSearch size={16} />}
onChange={(e) => debouncedSearch(e.target.value)}
/>
<Button variant="light" size="sm" onClick={handleZoomOut}>
<IconZoomOut size={16} />
</Button>
<Button variant="light" size="sm" onClick={resetZoom}>
100%
</Button>
<Button variant="light" size="sm" onClick={handleZoomIn}>
<IconZoomIn size={16} />
</Button>
<Button
variant="light"
size="sm"
onClick={toggleFullscreen}
leftSection={
isFullscreen ? <IconArrowsMinimize size={16} /> : <IconArrowsMaximize size={16} />
}
>
{isFullscreen ? 'Keluar' : 'Fullscreen'}
</Button>
</Group>
{/* Chart Container */}
<Box
ref={chartContainerRef}
style={{
background: 'rgba(28,110,164,0.2)',
border: `1px solid rgba(255,255,255,0.1)`,
overflowX: 'auto',
overflow: 'auto',
transform: `scale(${scale})`,
transformOrigin: 'center top',
transition: 'transform 0.25s ease',
}}
>
<OrganizationChart
value={chartData}
nodeTemplate={(node) => nodeTemplate(node, router)}
/>
</Paper>
</Box>
</Stack>
)
}
@@ -221,7 +285,6 @@ function nodeTemplate(node: any, router: ReturnType<typeof useTransitionRouter>)
const title = node?.data?.title || 'Tanpa Jabatan'
const description = node?.data?.description || ''
return (
<Transition mounted transition="pop" duration={240}>
{(styles) => (
@@ -244,15 +307,15 @@ function nodeTemplate(node: any, router: ReturnType<typeof useTransitionRouter>)
src={imageSrc}
alt={name}
radius="md"
width={120}
height={120}
width={60}
height={60}
fit="cover"
style={{
objectFit: 'cover',
border: '2px solid rgba(255,255,255,0.2)',
marginBottom: 12,
}}
loading='lazy'
loading="lazy"
/>
<Text fw={700}>{name}</Text>
<Text size="sm" c="dimmed" mt={4}>

View File

@@ -86,10 +86,15 @@ function NavbarMobile({ listNavbar }: { listNavbar: MenuItem[] }) {
align="center"
p="xs"
onClick={() => {
if (item.href) {
router.push(item.href);
stateNav.mobileOpen = false;
}
}}
style={{
cursor: item.href ? "pointer" : "default",
opacity: item.href ? 1 : 0.8
}}
style={{ cursor: "pointer" }}
>
<Text c="dark.9" fw={600} fz="md">
{item.name}

View File

@@ -2,15 +2,14 @@
import colors from "@/con/colors"
import stateNav from "@/state/state-nav"
import { ActionIcon, Button, Container, Flex, Image, Stack, Tooltip } from "@mantine/core"
import { useHover } from "@mantine/hooks"
import { ActionIcon, Button, Container, Flex, Image, Menu, MenuTarget, Stack, Tooltip } from "@mantine/core"
import { IconSearch, IconUser } from "@tabler/icons-react"
import { useTransitionRouter } from 'next-view-transitions'
import { usePathname, useRouter } from "next/navigation"
import { useSnapshot } from "valtio"
import { MenuItem } from "../../../../types/menu-item"
import { NavbarSearch } from "./NavBarSearch"
import { NavbarSubMenu } from "./NavbarSubMenu"
import { useRouter } from "next/navigation"
// contoh state auth (dummy aja dulu, bisa diganti sesuai sistem auth kamu)
const stateAuth = {
@@ -21,6 +20,7 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
const { item, isSearch } = useSnapshot(stateNav)
const router = useTransitionRouter()
const next = useRouter()
const pathname = usePathname();
return (
<Stack gap={0} visibleFrom="sm" bg={colors["white-trans-1"]}>
@@ -47,7 +47,12 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
</Tooltip>
{listNavbar.map((item, k) => (
<MenuItemCom key={k} item={item} />
<MenuItemCom
key={k}
item={item}
isActive={pathname === item.href ||
(item.children?.some(child => child.href === pathname))}
/>
))}
<Tooltip label="Search content" position="bottom" withArrow>
@@ -88,27 +93,45 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
)
}
function MenuItemCom({ item }: { item: MenuItem }) {
const { ref, hovered } = useHover()
function MenuItemCom({ item, isActive = false }: { item: MenuItem, isActive?: boolean }) {
const router = useTransitionRouter()
return (
<Menu
trigger="hover"
position="bottom-start"
offset={20}
width={300}
shadow="md"
withinPortal
onOpen={() => {
stateNav.item = item.children || null;
stateNav.isSearch = false;
}}
>
<MenuTarget>
<Button
ref={ref}
color={hovered ? "gray" : colors["blue-button"]}
onMouseEnter={() => {
stateNav.item = item.children || null
stateNav.isSearch = false
}}
variant="subtle"
radius="xl"
color={isActive ? 'blue' : 'gray'}
onClick={() => {
router.push(item.href)
stateNav.clear()
if (item.href) {
router.push(item.href);
stateNav.clear();
}
}}
styles={{
root: {
fontWeight: isActive ? 600 : 400,
borderBottom: isActive ? `2px solid ${colors['blue-button']}` : 'none',
'&:hover': {
backgroundColor: 'transparent',
}
}
}}
fw={500}
>
{item.name}
</Button>
</MenuTarget>
</Menu>
)
}

View File

@@ -7,10 +7,11 @@ import { IconArrowRight } from "@tabler/icons-react";
import { MenuItem } from "../../../../types/menu-item";
import { useTransitionRouter } from "next-view-transitions";
import colors from "@/con/colors";
import { usePathname } from "next/navigation";
export function NavbarSubMenu({ item }: { item: MenuItem[] | null }) {
const router = useTransitionRouter();
const pathname = usePathname();
return (
<motion.div
key={Math.random().toString(36).slice(2)}
@@ -37,23 +38,24 @@ export function NavbarSubMenu({ item }: { item: MenuItem[] | null }) {
justify="space-between"
size="lg"
radius="md"
color="gray.0"
color={pathname === link.href ? 'blue' : 'gray'}
onClick={() => {
if (link.href) {
router.push(link.href);
stateNav.item = null;
stateNav.isSearch = false;
}
}}
rightSection={<IconArrowRight size={18} />}
styles={(theme) => ({
root: {
background: "transparent",
color: colors['blue-button'],
fontWeight: 500,
background: pathname === link.href ? theme.colors.blue[0] : 'transparent',
color: pathname === link.href ? theme.colors.blue[7] : colors['blue-button'],
fontWeight: pathname === link.href ? 600 : 500,
transition: "all 0.2s ease",
"&:hover": {
background: theme.colors.gray[8],
boxShadow: `0 0 12px ${theme.colors.blue[6]}55`,
},
background: pathname === link.href ? theme.colors.blue[1] : theme.colors.gray[0],
}
},
})}
>

View File

@@ -1,21 +1,24 @@
'use client';
import searchState, { debouncedFetch } from '@/app/api/[[...slugs]]/_lib/search/searchState';
import { Box, Center, Loader, Stack, Text, TextInput } from '@mantine/core';
import { Box, Center, Loader, Modal, Text, TextInput } from '@mantine/core';
import { IconX } from '@tabler/icons-react';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { useSnapshot } from 'valtio';
import getDetailUrl from './searchUrl';
export default function GlobalSearch() {
const snap = useSnapshot(searchState);
const [isOpen, setIsOpen] = useState(false);
// Toggle modal when there's a query
useEffect(() => {
setIsOpen(!!snap.query);
}, [snap.query]);
// Infinite scroll
useEffect(() => {
const handleScroll = () => {
const bottom =
window.innerHeight + window.scrollY >= document.body.offsetHeight - 200;
const bottom = window.innerHeight + window.scrollY >= document.body.offsetHeight - 200;
if (bottom && !snap.loading) searchState.next();
};
window.addEventListener('scroll', handleScroll);
@@ -23,15 +26,14 @@ export default function GlobalSearch() {
}, [snap.loading]);
return (
<Stack maw={800} mx="auto">
{/* 🔍 Search input */}
<Box style={{ position: 'relative', width: '100%' }}>
<TextInput
placeholder="Cari apapun..."
value={snap.query}
onChange={(e) => (
searchState.query = e.currentTarget.value,
debouncedFetch()
)}
onChange={(e) => {
searchState.query = e.currentTarget.value;
debouncedFetch();
}}
radius="xl"
rightSection={
snap.query ? (
@@ -47,8 +49,29 @@ export default function GlobalSearch() {
}
/>
{/* 📄 Hasil pencarian */}
<div style={{ maxHeight: '400px', overflowY: 'auto' }}>
{/* Modal for search results */}
<Modal
opened={isOpen && !!snap.query}
onClose={() => {
searchState.query = '';
searchState.results = [];
}}
withCloseButton={false}
size="lg"
padding={0}
radius="md"
style={{ position: 'absolute', top: '100%', left: 0, right: 0, zIndex: 1000 }}
styles={{
content: { // Changed from 'modal' to 'content'
backgroundColor: 'white',
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
borderRadius: '0.5rem',
maxHeight: '400px',
overflow: 'hidden',
},
}}
>
<Box style={{ maxHeight: '400px', overflowY: 'auto' }}>
{snap.results.map((item, i) => (
<Box
key={i}
@@ -60,7 +83,7 @@ export default function GlobalSearch() {
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
maxWidth: '100%'
maxWidth: '100%',
}}
onMouseEnter={(e) => (e.currentTarget.style.background = '#f5f5f5')}
onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')}
@@ -77,15 +100,13 @@ export default function GlobalSearch() {
</Text>
</Box>
))}
</div>
{/* ⏳ Loader di bawah hasil */}
{snap.loading && (
<Center py="md">
<Loader size="sm" />
</Center>
)}
</Stack>
</Box>
</Modal>
</Box>
);
}

View File

@@ -332,7 +332,7 @@ function Kepuasan() {
<TextInput
label="Nama"
type='text'
placeholder="masukkan nama"
placeholder="Masukkan nama"
defaultValue={state.create.form.name}
onChange={(val) => {
state.create.form.name = val.currentTarget.value;

View File

@@ -6,20 +6,19 @@ import {
Center,
Image,
Paper,
ScrollArea,
SimpleGrid,
Skeleton,
Stack,
Text,
Tooltip,
Skeleton,
useMantineColorScheme,
ScrollArea,
useMantineColorScheme
} from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks";
import { Prisma } from "@prisma/client";
import { IconPhotoOff } from "@tabler/icons-react";
import { motion } from "framer-motion";
import { useTransitionRouter } from "next-view-transitions";
import { useProxy } from "valtio/utils";
import { Prisma } from "@prisma/client";
import { IconPhotoOff } from "@tabler/icons-react";
type ProgramInovasiItem = Prisma.ProgramInovasiGetPayload<{ include: { image: true } }>;
@@ -30,7 +29,6 @@ function ModuleItem({ data }: { data: ProgramInovasiItem }) {
return (
<motion.div whileHover={{ scale: 1.03 }}>
<Tooltip label={`Lihat ${data.name}`} withArrow>
<Paper
onClick={() => router.push(`/darmasaba/program-inovasi/${data.id}`)}
p="lg"
@@ -67,7 +65,6 @@ function ModuleItem({ data }: { data: ProgramInovasiItem }) {
</Text>
</Box>
</Paper>
</Tooltip>
</motion.div>
);
}

View File

@@ -37,9 +37,13 @@ export default function ProfileView({ data }: ProfileViewProps) {
<Image
src={data.image.link}
alt={data.name || 'Foto profil'}
fit="cover"
fit="contain"
radius="lg"
loading="lazy"
style={{
objectPosition: 'bottom center',
transform: 'translateY(10px)', // sedikit turun biar natural
}}
/>
) : (
<Stack align="center" gap="xs" w="100%" py="xl">
@@ -49,13 +53,26 @@ export default function ProfileView({ data }: ProfileViewProps) {
</Text>
</Stack>
)}
<Box pos="absolute" bottom={0} w="100%" p={{ base: 'xs', md: 'md' }}>
{/* Box nama dan jabatan - sedikit overlap dengan gambar */}
<Box
pos="absolute"
bottom={-20} // bikin naik sedikit ke gambar
right={0}
w="100%"
p={{ base: 'xs', md: 'md' }}
style={{ pointerEvents: 'none' }} // biar ga ganggu klik di gambar
>
<Card
px="lg"
radius="2xl"
py="sm"
radius="lg"
withBorder
className="glass3"
style={{ border: '1px solid rgba(255,255,255,0.15)' }}
style={{
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
backdropFilter: 'blur(6px)',
pointerEvents: 'auto',
}}
>
<Tooltip label="Jabatan Resmi" withArrow>
<Text fz="sm" c="dimmed">

View File

@@ -51,7 +51,7 @@ export default function SDGS() {
</Title>
</Center>
<Text fz={{ base: "1rem", md: "1.2rem" }} ta="center" c="dimmed" mt="md" maw={820} mx="auto">
SDGs Desa merupakan langkah nyata untuk mewujudkan desa yang maju, inklusif, dan berkelanjutan melalui 17 tujuan pembangunan: dari pengentasan kemiskinan, pendidikan, kesehatan, kesetaraan gender, hingga pelestarian lingkungan.
SDGs Desa merupakan langkah nyata untuk mewujudkan desa yang maju, inklusif, dan berkelanjutan melalui 17 tujuan pembangunan dari pengentasan kemiskinan, pendidikan, kesehatan, kesetaraan gender, hingga pelestarian lingkungan.
</Text>
<Box py={50}>
@@ -78,6 +78,9 @@ export default function SDGS() {
background: "linear-gradient(180deg, #FFFFFF, #F6F8FA)",
border: "1px solid rgba(0,0,0,0.05)",
transition: "all 0.3s ease",
height: "100%", // biar tinggi antar card konsisten
display: "flex",
flexDirection: "column",
}}
>
<Center mb="lg">
@@ -105,11 +108,21 @@ export default function SDGS() {
/>
</Box>
</Center>
{/* Stack isi teks & angka */}
<Stack justify="space-between" align="center" gap="xs" h="100%">
<Tooltip label="Nama tujuan SDGs Desa" position="top" withArrow>
<Text ta="center" fz={{ base: "lg", md: "xl" }} fw={700} mb="xs">
<Text
ta="center"
fz={{ base: "lg", md: "xl" }}
fw={700}
mb="xs"
style={{ minHeight: mobile ? 60 : 70 }} // biar judulnya punya tinggi tetap
>
{item.name}
</Text>
</Tooltip>
<Title
order={2}
ta="center"
@@ -122,9 +135,11 @@ export default function SDGS() {
>
{item.jumlah}
</Title>
</Stack>
</Paper>
))}
</SimpleGrid>
) : (
<Center mih={200} style={{ flexDirection: "column" }}>
<IconMoodSad size={48} stroke={1.5} style={{ marginBottom: "1rem" }} />

View File

@@ -52,6 +52,37 @@ const getDetailUrl = (item: { type?: string; id: string | number; [key: string]:
programKemiskinan: '/darmasaba/ekonomi/program-kemiskinan',
sektorUnggulanDesa: '/darmasaba/ekonomi/sektor-unggulan-desa',
demografiPekerjaan: '/darmasaba/ekonomi/demografi-pekerjaan',
desaDigital: '/darmasaba/inovasi/desa-digital-smart-village',
programKreatif: '/darmasaba/inovasi/program-kreatif-desa',
kolaborasiInovasi: '/darmasaba/inovasi/kolaborasi-inovasi',
mitraKolaborasi: '/darmasaba/inovasi/kolaborasi-inovasi',
infoTekno: '/darmasaba/inovasi/info-teknologi-tepat-guna',
pengelolaanSampah: '/darmasaba/lingkungan/pengelolaan-sampah-bank-sampah',
keteranganBankSampahTerdekat: '/darmasaba/lingkungan/pengelolaan-sampah-bank-sampah',
programPenghijauan: '/darmasaba/lingkungan/program-penghijauan',
dataLingkunganDesa: '/darmasaba/lingkungan/data-lingkungan-desa',
gotongRoyong: '/darmasaba/lingkungan/gotong-royong',
tujuanEdukasiLingkungan: '/darmasaba/lingkungan/tujuan-edukasi-lingkungan',
materiEdukasiLingkungan: '/darmasaba/lingkungan/materi-edukasi-lingkungan',
contohEdukasiLingkungan: '/darmasaba/lingkungan/contoh-edukasi-lingkungan',
filosofiTriHita: '/darmasaba/lingkungan/filosofi-tri-hita',
bentukKonservasiBerdasarkanAdat: '/darmasaba/lingkungan/bentuk-konservasi-berdasarkan-adat',
nilaiKonservasiAdat: '/darmasaba/lingkungan/nilai-konservasi-adat',
jenjangPendidikan: '/darmasaba/inovasi/jenjang-pendidikan',
lembaga: '/darmasaba/inovasi/lembaga',
siswa: '/darmasaba/inovasi/siswa',
pengajar: '/darmasaba/inovasi/pengajar',
keunggulanProgram: '/darmasaba/inovasi/keunggulan-program',
tujuanProgram: '/darmasaba/inovasi/tujuan-program',
programUnggulan: '/darmasaba/inovasi/program-unggulan',
lokasiJadwalBimbinganBelajarDesa: '/darmasaba/inovasi/lokasi-jadwal-bimbingan-belajar-desa',
fasilitasBimbinganBelajarDesa: '/darmasaba/inovasi/fasilitas-bimbingan-belajar-desa',
tujuanPendidikanNonFormal: '/darmasaba/inovasi/tujuan-pendidikan-non-formal',
tempatKegiatan: '/darmasaba/inovasi/tempat-kegiatan',
jenisProgramYangDiselenggarakan: '/darmasaba/inovasi/jenis-program-yang-diselenggarakan',
dataPerpustakaan: '/darmasaba/inovasi/data-perpustakaan',
dataPendidikan: '/darmasaba/inovasi/data-pendidikan',
};
return typeUrlMap[type || ''] || '/darmasaba';

View File

@@ -2,7 +2,6 @@ const navbarListMenu = [
{
id: "1",
name: "PPID",
href: "/darmasaba/ppid/profile-ppid",
children: [
{
id: "1.1",
@@ -51,7 +50,6 @@ const navbarListMenu = [
{
id: "2",
name: "Desa",
href: "/darmasaba/desa/profile",
children: [
{
id: "2.1",
@@ -94,7 +92,6 @@ const navbarListMenu = [
{
id: "3",
name: "Kesehatan",
href: "/darmasaba/kesehatan/posyandu",
children: [
{
id: "3.1",
@@ -136,7 +133,6 @@ const navbarListMenu = [
{
id: "4",
name: "Keamanan",
href: "/darmasaba/keamanan/keamanan-lingkungan-pecalang-patwal",
children: [
{
id: "4.1",
@@ -173,7 +169,6 @@ const navbarListMenu = [
{
id: "5",
name: "Ekonomi",
href: "/darmasaba/ekonomi/pasar-desa",
children: [
{
id: "5.1",
@@ -229,7 +224,6 @@ const navbarListMenu = [
}, {
id: "6",
name: "Inovasi",
href: "/darmasaba/inovasi/desa-digital-smart-village",
children: [
{
id: "6.1",
@@ -266,7 +260,6 @@ const navbarListMenu = [
}, {
id: "7",
name: "Lingkungan",
href: "/darmasaba/lingkungan/pengelolaan-sampah-bank-sampah",
children: [
{
id: "7.1",
@@ -302,7 +295,6 @@ const navbarListMenu = [
}, {
id: "8",
name: "Pendidikan",
href: "/darmasaba/pendidikan/info-sekolah",
children: [
{
id: "8.1",

View File

@@ -1,6 +1,9 @@
export type MenuItem = {
id: string,
name: string,
href: string,
children?: MenuItem[]
}
id: string;
name: string;
href?: string;
children?: MenuItem[];
} & (
{ href: string; children?: MenuItem[] } |
{ children: MenuItem[] }
)