Compare commits
2 Commits
nico/15-ok
...
nico/17-ok
| Author | SHA1 | Date | |
|---|---|---|---|
| bbf13c1cf7 | |||
| 75bf0652b1 |
@@ -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",
|
||||
|
||||
@@ -1,16 +1,4 @@
|
||||
[
|
||||
{
|
||||
"id": "cmds8w2q60002vnbe6i8qhkuo",
|
||||
"name": "Telephone Desa Darmasaba",
|
||||
"iconUrl": "081239580000",
|
||||
"imageId": "cmff3nv180003vn6h5jvedidq"
|
||||
},
|
||||
{
|
||||
"id": "cmds8z7u20005vnbegyyvnbk0",
|
||||
"name": "Email Desa Darmasaba",
|
||||
"iconUrl": "desadarmasaba@badungkab.go.id",
|
||||
"imageId": "cmff3ll130001vn6hkhls3f5y"
|
||||
},
|
||||
{
|
||||
"id": "cmds9023u0008vnbe3oxmhwyf",
|
||||
"name": "Desa Darmasaba",
|
||||
|
||||
@@ -1606,7 +1606,7 @@ model Pembiayaan {
|
||||
ApbDesa ApbDesa[] @relation("ApbDesaPembiayaan")
|
||||
}
|
||||
|
||||
// ========================================= INOVASI ========================================= //
|
||||
// ========================================= MENU INOVASI ========================================= //
|
||||
// ========================================= DESA DIGITAL / SMART VILLAGE ========================================= //
|
||||
model DesaDigital {
|
||||
id String @id @default(cuid())
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 266 KiB After Width: | Height: | Size: 378 KiB |
@@ -75,17 +75,18 @@ 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;
|
||||
|
||||
|
||||
try {
|
||||
const query: any = { page, limit };
|
||||
if (search) query.search = search;
|
||||
if (kategori) query.kategori = kategori;
|
||||
|
||||
|
||||
const res = await ApiFetch.api.desa.berita["find-many"].get({ query });
|
||||
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
berita.findMany.data = res.data.data ?? [];
|
||||
berita.findMany.totalPages = res.data.totalPages ?? 1;
|
||||
@@ -98,9 +99,16 @@ const berita = proxy({
|
||||
berita.findMany.data = [];
|
||||
berita.findMany.totalPages = 1;
|
||||
} finally {
|
||||
berita.findMany.loading = false;
|
||||
// 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);
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
findUnique: {
|
||||
|
||||
@@ -55,81 +55,95 @@ const dataPerpustakaan = proxy({
|
||||
},
|
||||
},
|
||||
findMany: {
|
||||
data: null as
|
||||
| Prisma.DataPerpustakaanGetPayload<{
|
||||
include: {
|
||||
image: true;
|
||||
kategori: true;
|
||||
};
|
||||
}>[]
|
||||
| null,
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
loading: false,
|
||||
search: "",
|
||||
load: async (page = 1, limit = 10, search = "", kategori = "") => {
|
||||
dataPerpustakaan.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||
dataPerpustakaan.findMany.page = page;
|
||||
dataPerpustakaan.findMany.search = search;
|
||||
|
||||
try {
|
||||
const query: any = { page, limit };
|
||||
if (search) query.search = search;
|
||||
if (kategori) query.kategori = kategori;
|
||||
|
||||
const res = await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan["findMany"].get({ query });
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
dataPerpustakaan.findMany.data = res.data.data ?? [];
|
||||
dataPerpustakaan.findMany.totalPages = res.data.totalPages ?? 1;
|
||||
} else {
|
||||
dataPerpustakaan.findMany.data = [];
|
||||
dataPerpustakaan.findMany.totalPages = 1;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Gagal fetch data perpustakaan paginated:", err);
|
||||
data: null as
|
||||
| Prisma.DataPerpustakaanGetPayload<{
|
||||
include: {
|
||||
image: true;
|
||||
kategori: true;
|
||||
};
|
||||
}>[]
|
||||
| null,
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
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;
|
||||
|
||||
try {
|
||||
const query: any = { page, limit };
|
||||
if (search) query.search = search;
|
||||
if (kategori) query.kategori = kategori;
|
||||
|
||||
const res =
|
||||
await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan[
|
||||
"findMany"
|
||||
].get({ query });
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
dataPerpustakaan.findMany.data = res.data.data ?? [];
|
||||
dataPerpustakaan.findMany.totalPages = res.data.totalPages ?? 1;
|
||||
} else {
|
||||
dataPerpustakaan.findMany.data = [];
|
||||
dataPerpustakaan.findMany.totalPages = 1;
|
||||
} finally {
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Gagal fetch data perpustakaan paginated:", err);
|
||||
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);
|
||||
}
|
||||
},
|
||||
findManyAll: {
|
||||
data: null as
|
||||
| Prisma.DataPerpustakaanGetPayload<{
|
||||
include: {
|
||||
image: true;
|
||||
kategori: true;
|
||||
};
|
||||
}>[]
|
||||
| null,
|
||||
loading: false,
|
||||
search: "",
|
||||
load: async (search = "", kategori = "") => {
|
||||
dataPerpustakaan.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||
dataPerpustakaan.findMany.search = search;
|
||||
|
||||
try {
|
||||
const query: any = {};
|
||||
if (search) query.search = search;
|
||||
if (kategori) query.kategori = kategori;
|
||||
|
||||
const res = await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan["findManyAll"].get({ query });
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
dataPerpustakaan.findManyAll.data = res.data.data ?? [];
|
||||
} else {
|
||||
dataPerpustakaan.findManyAll.data = [];
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Gagal fetch data perpustakaan paginated:", err);
|
||||
},
|
||||
findManyAll: {
|
||||
data: null as
|
||||
| Prisma.DataPerpustakaanGetPayload<{
|
||||
include: {
|
||||
image: true;
|
||||
kategori: true;
|
||||
};
|
||||
}>[]
|
||||
| null,
|
||||
loading: false,
|
||||
search: "",
|
||||
load: async (search = "", kategori = "") => {
|
||||
dataPerpustakaan.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||
dataPerpustakaan.findMany.search = search;
|
||||
|
||||
try {
|
||||
const query: any = {};
|
||||
if (search) query.search = search;
|
||||
if (kategori) query.kategori = kategori;
|
||||
|
||||
const res =
|
||||
await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan[
|
||||
"findManyAll"
|
||||
].get({ query });
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
dataPerpustakaan.findManyAll.data = res.data.data ?? [];
|
||||
} else {
|
||||
dataPerpustakaan.findManyAll.data = [];
|
||||
} finally {
|
||||
dataPerpustakaan.findManyAll.loading = false;
|
||||
}
|
||||
},
|
||||
} catch (err) {
|
||||
console.error("Gagal fetch data perpustakaan paginated:", err);
|
||||
dataPerpustakaan.findManyAll.data = [];
|
||||
} finally {
|
||||
dataPerpustakaan.findManyAll.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
findUnique: {
|
||||
data: null as Prisma.DataPerpustakaanGetPayload<{
|
||||
include: {
|
||||
@@ -356,17 +370,20 @@ const kategoriBuku = proxy({
|
||||
totalPages: 1,
|
||||
loading: false,
|
||||
search: "",
|
||||
load: async (page = 1, limit = 10, search = "") => {
|
||||
load: async (page = 1, limit = 10, search = "") => {
|
||||
kategoriBuku.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||
kategoriBuku.findMany.page = page;
|
||||
kategoriBuku.findMany.search = search;
|
||||
|
||||
|
||||
try {
|
||||
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 ?? [];
|
||||
kategoriBuku.findMany.totalPages = res.data.totalPages ?? 1;
|
||||
@@ -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: {
|
||||
@@ -646,13 +663,16 @@ const peminjamanBuku = proxy({
|
||||
peminjamanBuku.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||
peminjamanBuku.findMany.page = page;
|
||||
peminjamanBuku.findMany.search = search;
|
||||
|
||||
|
||||
try {
|
||||
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 ?? [];
|
||||
peminjamanBuku.findMany.totalPages = res.data.totalPages ?? 1;
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,122 +6,83 @@ 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) => {
|
||||
id: string;
|
||||
name: string;
|
||||
link: string;
|
||||
realName: string;
|
||||
createdAt: string | Date;
|
||||
category: string;
|
||||
path: string;
|
||||
mimeType: string;
|
||||
}
|
||||
|
||||
export default function FotoContent() {
|
||||
const [files, setFiles] = useState<FileItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [search, setSearch] = useState('');
|
||||
const [page, setPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const limit = 9; // ✅ ambil 12 data per page
|
||||
|
||||
const loadData = useCallback(async (pageNum: number, searchTerm: string) => {
|
||||
setLoading(true);
|
||||
// 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 })
|
||||
}
|
||||
});
|
||||
try {
|
||||
const query: Record<string, string> = {
|
||||
category: 'image',
|
||||
page: pageNum.toString(),
|
||||
limit: limit.toString(),
|
||||
};
|
||||
if (searchTerm) query.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);
|
||||
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([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadFn();
|
||||
} catch (err) {
|
||||
console.error('Load error:', err);
|
||||
setFiles([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Initial load and URL change handler
|
||||
// ✅ Initial load + update when URL/search changes
|
||||
useEffect(() => {
|
||||
const handleRouteChange = () => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const urlSearch = urlParams.get('search') || '';
|
||||
const urlPage = parseInt(urlParams.get('page') || '1');
|
||||
|
||||
setSearch(urlSearch);
|
||||
setPage(urlPage);
|
||||
loadData(urlPage, urlSearch);
|
||||
};
|
||||
|
||||
// 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
|
||||
setPage(1);
|
||||
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
|
||||
// ✅ Update when page/search changes
|
||||
useEffect(() => {
|
||||
const fetchFiles = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const query: Record<string, string> = {
|
||||
category: 'image',
|
||||
page: page.toString(),
|
||||
limit: '10',
|
||||
};
|
||||
if (search) query.search = search;
|
||||
loadData(page, search);
|
||||
}, [page, search, loadData]);
|
||||
|
||||
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);
|
||||
@@ -148,7 +109,14 @@ interface FileItem {
|
||||
<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%' }}>
|
||||
<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}
|
||||
@@ -159,20 +127,18 @@ interface FileItem {
|
||||
loading="lazy"
|
||||
/>
|
||||
</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>
|
||||
<Stack gap="sm" py={10}>
|
||||
<Text fw="bold" fz={{ base: 'h4', md: 'h3' }}>
|
||||
{file.realName || file.name}
|
||||
</Text>
|
||||
<Text fz="sm" c="dimmed">
|
||||
{new Date(file.createdAt).toLocaleDateString('id-ID', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
@@ -181,4 +147,4 @@ interface FileItem {
|
||||
</Center>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,24 +146,24 @@ function Page() {
|
||||
<Title order={3}>Ajukan Permohonan</Title>
|
||||
<TextInput
|
||||
label={<Text fz="sm" fw="bold">Nama</Text>}
|
||||
placeholder="masukkan nama"
|
||||
placeholder="Masukkan nama"
|
||||
onChange={(val) => (stateCreate.create.form.nama = val.target.value)}
|
||||
/>
|
||||
<TextInput
|
||||
type="number"
|
||||
label={<Text fz="sm" fw="bold">NIK</Text>}
|
||||
placeholder="masukkan NIK"
|
||||
placeholder="Masukkan NIK"
|
||||
onChange={(val) => (stateCreate.create.form.nik = val.target.value)}
|
||||
/>
|
||||
<TextInput
|
||||
label={<Text fz="sm" fw="bold">Alamat</Text>}
|
||||
placeholder="masukkan alamat"
|
||||
placeholder="Masukkan alamat"
|
||||
onChange={(val) => (stateCreate.create.form.alamat = val.target.value)}
|
||||
/>
|
||||
<TextInput
|
||||
type="number"
|
||||
label={<Text fz="sm" fw="bold">Nomor KK</Text>}
|
||||
placeholder="masukkan Nomor KK"
|
||||
placeholder="Masukkan Nomor KK"
|
||||
onChange={(val) => (stateCreate.create.form.nomorKk = val.target.value)}
|
||||
/>
|
||||
<Select
|
||||
|
||||
@@ -72,21 +72,21 @@ function Page() {
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22" style={{ overflow: 'auto' }}>
|
||||
<Box px={{ base: 'md', md: 50, lg: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
<Box px={{ base: 'md', md: 100 }} >
|
||||
<Box px={{ base: 'md', md: 50, lg: 100 }} >
|
||||
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
|
||||
Jumlah Penduduk Usia Kerja Yang Menganggur
|
||||
</Text>
|
||||
</Box>
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Box px={{ base: "md", md: 50, lg: 100 }}>
|
||||
<Stack gap={'lg'} justify='center'>
|
||||
<Paper p={'lg'}>
|
||||
<Text fw={'bold'} fz={'h3'}>Pengangguran Berdasarkan Usia</Text>
|
||||
{mounted && donutGrafikNganggurData.length > 0 ? (<Box style={{ width: '100%', height: 'auto', minHeight: 200 }}>
|
||||
<Box w="100%" style={{ maxWidth: 400, margin: "0 auto" }}>
|
||||
<Box w="100%" maw={{ base: '100%', md: 400 }} mx="auto">
|
||||
<PieChart
|
||||
w="100%"
|
||||
h={250} // lebih kecil biar aman di mobile
|
||||
@@ -133,7 +133,7 @@ function Page() {
|
||||
<Box w="100%" style={{ maxWidth: 400, margin: "0 auto" }}>
|
||||
<PieChart
|
||||
w="100%"
|
||||
h={250} // lebih kecil biar aman di mobile
|
||||
h="min(250px, 50vh)" // lebih kecil biar aman di mobile
|
||||
withLabelsLine
|
||||
labelsPosition="outside"
|
||||
labelsType="percent"
|
||||
|
||||
@@ -199,7 +199,7 @@ function Page() {
|
||||
<TableTd ta={'center'}>{item.totalUnemployment}</TableTd>
|
||||
<TableTd ta={'center'}>{item.educatedUnemployment}</TableTd>
|
||||
<TableTd ta={'center'}>{item.uneducatedUnemployment}</TableTd>
|
||||
<TableTd ta={'center'}>{item.percentageChange}</TableTd>
|
||||
<TableTd ta={'center'}>{item.percentageChange}%</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
</TableTbody>
|
||||
|
||||
@@ -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 }}>
|
||||
@@ -45,27 +71,34 @@ function Page() {
|
||||
return (
|
||||
<Paper p={'xl'} key={k}>
|
||||
<Text fw={'bold'} fz={'h4'}>{v.name}</Text>
|
||||
<Text fz={'h4'} ta={'justify'} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: v.description || '' }} />
|
||||
<Text fz={'h4'} ta={'justify'} style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: v.description || '' }} />
|
||||
</Paper>
|
||||
)
|
||||
})}
|
||||
<Paper p={'xl'}>
|
||||
<Text pb={10} fw={'bold'} fz={'h4'}>Statistik Sektor Unggulan Darmasaba</Text>
|
||||
<BarChart
|
||||
p={10}
|
||||
h={300}
|
||||
data={data.map((item) => ({
|
||||
id: item.id,
|
||||
sektor: item.name,
|
||||
Ton: item.value,
|
||||
}))}
|
||||
dataKey="sektor"
|
||||
series={[
|
||||
{ name: 'Ton', color: colors['blue-button'] },
|
||||
]}
|
||||
tickLine="y"
|
||||
/>
|
||||
</Paper>
|
||||
<Box style={{ width: '100%', overflowX: 'auto' }}>
|
||||
<Paper p="xl">
|
||||
<Text pb={10} fw="bold" fz="h4">Statistik Sektor Unggulan Darmasaba</Text>
|
||||
<Box style={{ width: '100%', minWidth: '600px' }}>
|
||||
<BarChart
|
||||
p={10}
|
||||
h={300}
|
||||
data={chartData}
|
||||
dataKey="sektor"
|
||||
series={[
|
||||
{ name: 'Ton', color: colors['blue-button'] },
|
||||
]}
|
||||
tickLine="y"
|
||||
tooltipAnimationDuration={200}
|
||||
withTooltip
|
||||
style={{
|
||||
fontFamily: 'inherit',
|
||||
}}
|
||||
xAxisLabel="Sektor"
|
||||
yAxisLabel="Ton"
|
||||
/>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
@@ -50,10 +50,12 @@ function Page() {
|
||||
</Text>
|
||||
</Box>
|
||||
<TextInput
|
||||
placeholder='Cari kontak darurat, nama, atau nomor...'
|
||||
leftSection={<IconSearch size={20} />}
|
||||
radius={"lg"}
|
||||
placeholder='Cari Kontak Darurat'
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftSection={<IconSearch size={20} />}
|
||||
w={{ base: "100%", md: "25%" }}
|
||||
/>
|
||||
</Group>
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
@@ -95,10 +97,12 @@ function Page() {
|
||||
</Text>
|
||||
</Box>
|
||||
<TextInput
|
||||
placeholder='Cari kontak darurat, nama, atau nomor...'
|
||||
leftSection={<IconSearch size={20} />}
|
||||
radius={"lg"}
|
||||
placeholder='Cari Kontak Darurat'
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftSection={<IconSearch size={20} />}
|
||||
w={{ base: "50%", md: "100%" }}
|
||||
/>
|
||||
</Group>
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
|
||||
@@ -4,7 +4,7 @@ import colors from '@/con/colors';
|
||||
import { Box, Button, Center, ColorSwatch, Flex, Group, Modal, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import { DateTimePicker } from '@mantine/dates';
|
||||
import { useDebouncedValue, useDisclosure, useShallowEffect } from '@mantine/hooks';
|
||||
import { IconArrowRight, IconPlus } from '@tabler/icons-react';
|
||||
import { IconArrowRight, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||
@@ -56,10 +56,13 @@ function Page() {
|
||||
<Flex justify="space-between" align="center">
|
||||
<BackButton />
|
||||
<TextInput
|
||||
placeholder="Cari laporan"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
radius={"lg"}
|
||||
placeholder='Cari Laporan Publik'
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftSection={<IconSearch size={20} />}
|
||||
w={{ base: "50%", md: "100%" }}
|
||||
/>
|
||||
</Flex>
|
||||
</Box>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -141,7 +141,7 @@ function Page() {
|
||||
<Box>
|
||||
<Flex gap={{ base: 0, md: 5 }} align={'center'}>
|
||||
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Angka Kematian</Text>
|
||||
<ColorSwatch color="#EF3E3E" size={30} />
|
||||
<ColorSwatch color="#EF3E3E" size={30} />
|
||||
</Flex>
|
||||
</Box>
|
||||
<Box>
|
||||
|
||||
@@ -101,27 +101,42 @@ function Page() {
|
||||
}}
|
||||
>
|
||||
<Stack align="center" gap="sm">
|
||||
<Image
|
||||
src={v.image.link}
|
||||
alt={v.name}
|
||||
w={140}
|
||||
h={140}
|
||||
fit="contain"
|
||||
radius="md"
|
||||
loading="lazy"
|
||||
/>
|
||||
<Box
|
||||
style={{
|
||||
width: '100%',
|
||||
aspectRatio: '16/9',
|
||||
borderRadius: '12px',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={v.image.link}
|
||||
alt={v.name}
|
||||
fit="cover"
|
||||
loading="lazy"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
transition: 'transform 0.4s ease',
|
||||
}}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.05)')}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Text ta="center" fw={700} fz="lg" c={colors['blue-button']}>
|
||||
{v.name}
|
||||
</Text>
|
||||
<Text fz="sm" c="dimmed" ta="center" lineClamp={3}>
|
||||
<span style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: v.deskripsi }} />
|
||||
</Text>
|
||||
<Button
|
||||
variant="light"
|
||||
leftSection={<IconBrandWhatsapp size={18} />}
|
||||
component="a"
|
||||
href={`https://wa.me/${v.whatsapp.replace(/\D/g, '')}`}
|
||||
target="_blank"
|
||||
<Button
|
||||
variant="light"
|
||||
leftSection={<IconBrandWhatsapp size={18} />}
|
||||
component="a"
|
||||
href={`https://wa.me/${v.whatsapp.replace(/\D/g, '')}`}
|
||||
target="_blank"
|
||||
aria-label="Hubungi WhatsApp"
|
||||
>WhatsApp</Button>
|
||||
</Stack>
|
||||
|
||||
@@ -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>
|
||||
<Image
|
||||
src={v.image.link}
|
||||
alt={v.name}
|
||||
h={180}
|
||||
w="100%"
|
||||
radius="md"
|
||||
fit="cover"
|
||||
style={{ aspectRatio: '4/3' }}
|
||||
/>
|
||||
<Box
|
||||
style={{
|
||||
width: '100%',
|
||||
aspectRatio: '16/9',
|
||||
borderRadius: '12px',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={v.image.link}
|
||||
alt={v.name}
|
||||
fit="cover"
|
||||
loading="lazy"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
transition: 'transform 0.4s ease',
|
||||
}}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.05)')}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
</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>
|
||||
))}
|
||||
@@ -160,7 +171,7 @@ function Page() {
|
||||
'&:hover': { backgroundColor: colors['blue-button'], color: 'white' },
|
||||
},
|
||||
}}
|
||||
|
||||
|
||||
/>
|
||||
</Center>
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Transition } from '@mantine/core';
|
||||
import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong';
|
||||
import {
|
||||
Badge,
|
||||
@@ -23,12 +27,11 @@ import {
|
||||
} from '@mantine/core';
|
||||
import { IconArrowRight, IconCalendar } from '@tabler/icons-react';
|
||||
import { useTransitionRouter } from 'next-view-transitions';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
export default function Content({ kategori }: { kategori: string }) {
|
||||
const router = useTransitionRouter();
|
||||
const [page, setPage] = useState(1);
|
||||
const [animateKey, setAnimateKey] = useState(0);
|
||||
|
||||
const state = useProxy(gotongRoyongState.kegiatanDesa);
|
||||
const featuredState = useProxy(gotongRoyongState.kegiatanDesa.findFirst);
|
||||
@@ -37,119 +40,178 @@ export default function Content({ kategori }: { kategori: string }) {
|
||||
const paginatedNews = state.findMany.data || [];
|
||||
const totalPages = state.findMany.totalPages || 1;
|
||||
|
||||
// Load data
|
||||
// Load data awal
|
||||
useEffect(() => {
|
||||
gotongRoyongState.kegiatanDesa.findFirst.load(kategori);
|
||||
}, [kategori]);
|
||||
|
||||
// Load daftar berita
|
||||
useEffect(() => {
|
||||
state.findMany.load(page, 3, '', kategori);
|
||||
setAnimateKey((prev) => prev + 1); // trigger animasi halus saat page berubah
|
||||
}, [page, kategori]);
|
||||
|
||||
// Tampilan kosong
|
||||
if (!featuredState.loading && !featured) {
|
||||
return (
|
||||
<Center py={100}>
|
||||
<Stack align="center" gap="sm">
|
||||
<Title order={3}>Belum Ada Data Gotong Royong</Title>
|
||||
<Text c="dimmed">Tidak ada data gotong royong yang tersedia saat ini.</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={20}>
|
||||
<Container size="xl" px={{ base: 'md', md: 'xl' }}>
|
||||
{/* === Gotong Royong Utama === */}
|
||||
{featuredState.loading ? (
|
||||
<Center><Skeleton h={400} /></Center>
|
||||
) : featured ? (
|
||||
<Box mb={50}>
|
||||
<Text fz="h2" fw={700} mb="md">Gotong Royong Utama</Text>
|
||||
<Paper shadow="md" radius="md" withBorder>
|
||||
<Grid gutter={0}>
|
||||
<GridCol span={{ base: 12, md: 6 }}>
|
||||
<Image
|
||||
src={featured.image?.link}
|
||||
alt={featured.judul || 'Berita Utama'}
|
||||
height={400}
|
||||
fit="cover"
|
||||
radius="md"
|
||||
style={{ borderBottomRightRadius: 0, borderTopRightRadius: 0 }}
|
||||
loading="lazy"
|
||||
/>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 6 }} p="xl">
|
||||
<Stack h="100%" justify="space-between">
|
||||
<div>
|
||||
<Badge color="blue" variant="light" mb="md">
|
||||
{featured.kategoriKegiatan?.nama || kategori}
|
||||
</Badge>
|
||||
<Title order={2} mb="md">{featured.judul}</Title>
|
||||
<Text c="dimmed" lineClamp={3} mb="md" dangerouslySetInnerHTML={{ __html: featured.deskripsiLengkap }} />
|
||||
</div>
|
||||
<Group justify="apart" mt="auto">
|
||||
<Group gap="xs">
|
||||
<IconCalendar size={18} />
|
||||
<Text size="sm">
|
||||
{new Date(featured.createdAt).toLocaleDateString('id-ID', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</Text>
|
||||
</Group>
|
||||
<Button
|
||||
variant="light"
|
||||
rightSection={<IconArrowRight size={16} />}
|
||||
onClick={() => router.push(`/darmasaba/lingkungan/gotong-royong/${kategori}/${featured.id}`)}
|
||||
>
|
||||
Baca Selengkapnya
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
</Paper>
|
||||
</Box>
|
||||
) : null}
|
||||
<Transition mounted={!featuredState.loading} transition="fade" duration={250} timingFunction="ease">
|
||||
{(styles) => (
|
||||
<div style={styles}>
|
||||
{featured ? (
|
||||
<Box mb={50}>
|
||||
<Text fz="h2" fw={700} mb="md">Gotong Royong Utama</Text>
|
||||
<Paper shadow="md" radius="md" withBorder>
|
||||
<Grid gutter={0}>
|
||||
<GridCol span={{ base: 12, md: 6 }}>
|
||||
<Image
|
||||
src={featured.image?.link || '/images/placeholder.jpg'}
|
||||
alt={featured.judul || 'Berita Utama'}
|
||||
height={400}
|
||||
fit="cover"
|
||||
radius="md"
|
||||
style={{ borderBottomRightRadius: 0, borderTopRightRadius: 0 }}
|
||||
loading="lazy"
|
||||
/>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 6 }} p="xl">
|
||||
<Stack h="100%" justify="space-between">
|
||||
<div>
|
||||
<Badge color="blue" variant="light" mb="md">
|
||||
{featured.kategoriKegiatan?.nama || kategori}
|
||||
</Badge>
|
||||
<Title order={2} mb="md">{featured.judul}</Title>
|
||||
<Text
|
||||
c="dimmed"
|
||||
lineClamp={3}
|
||||
mb="md"
|
||||
dangerouslySetInnerHTML={{ __html: featured.deskripsiLengkap }}
|
||||
/>
|
||||
</div>
|
||||
<Group justify="apart" mt="auto">
|
||||
<Group gap="xs">
|
||||
<IconCalendar size={18} />
|
||||
<Text size="sm">
|
||||
{new Date(featured.createdAt).toLocaleDateString('id-ID', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</Text>
|
||||
</Group>
|
||||
<Button
|
||||
variant="light"
|
||||
rightSection={<IconArrowRight size={16} />}
|
||||
onClick={() =>
|
||||
router.push(`/darmasaba/lingkungan/gotong-royong/${kategori}/${featured.id}`)
|
||||
}
|
||||
>
|
||||
Baca Selengkapnya
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
</Paper>
|
||||
</Box>
|
||||
) : (
|
||||
<Skeleton h={400} radius="md" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Transition>
|
||||
|
||||
{/* === Daftar Gotong Royong === */}
|
||||
{/* === Daftar Gotong Royong (Pagination + Fade-in Halus) === */}
|
||||
<Box mt={50}>
|
||||
<Title order={2} mb="md">Daftar Gotong Royong</Title>
|
||||
<Divider mb="xl" />
|
||||
|
||||
{state.findMany.loading ? (
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl">
|
||||
{Array(3).fill(0).map((_, i) => (
|
||||
<Skeleton key={i} h={300} radius="md" />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
) : paginatedNews.length === 0 ? (
|
||||
<Text c="dimmed" ta="center">Belum ada gotong royong di kategori "{kategori}".</Text>
|
||||
) : (
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl" verticalSpacing="xl">
|
||||
{paginatedNews.map((item) => (
|
||||
<Card
|
||||
key={item.id}
|
||||
shadow="sm"
|
||||
p="lg"
|
||||
radius="md"
|
||||
withBorder
|
||||
onClick={() => router.push(`/darmasaba/lingkungan/gotong-royong/${kategori}/${item.id}`)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<Card.Section>
|
||||
<Image src={item.image?.link} height={200} alt={item.judul} fit="cover" loading="lazy"/>
|
||||
</Card.Section>
|
||||
<Badge color="blue" variant="light" mt="md">
|
||||
{item.kategoriKegiatan?.nama || kategori}
|
||||
</Badge>
|
||||
<Text fw={600} size="lg" mt="sm" lineClamp={2}>{item.judul}</Text>
|
||||
<Text size="sm" c="dimmed" lineClamp={3} style={{wordBreak: "break-word", whiteSpace: "normal"}} mt="xs" dangerouslySetInnerHTML={{ __html: item.deskripsiLengkap }} />
|
||||
<Group justify="apart" mt="md" gap="xs">
|
||||
<Text size="xs" c="dimmed">
|
||||
{new Date(item.createdAt).toLocaleDateString('id-ID', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</Text>
|
||||
<Badge color="gray" variant="outline">Baca Selengkapnya</Badge>
|
||||
</Group>
|
||||
</Card>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
)}
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={animateKey}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
transition={{ duration: 0.25, ease: 'easeInOut' }}
|
||||
>
|
||||
{state.findMany.loading ? (
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl">
|
||||
{Array(3)
|
||||
.fill(0)
|
||||
.map((_, i) => (
|
||||
<Skeleton key={i} h={300} radius="md" />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
) : paginatedNews.length === 0 ? (
|
||||
<Center py={50}>
|
||||
<Stack align="center" gap="sm">
|
||||
<Title order={3}>Tidak Ada Data</Title>
|
||||
<Text c="dimmed">Belum ada data gotong royong yang tersedia.</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
) : (
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl" verticalSpacing="xl">
|
||||
{paginatedNews.map((item) => (
|
||||
<Card
|
||||
key={item.id}
|
||||
shadow="sm"
|
||||
p="lg"
|
||||
radius="md"
|
||||
withBorder
|
||||
onClick={() => router.push(`/darmasaba/lingkungan/gotong-royong/${kategori}/${item.id}`)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<Card.Section>
|
||||
<Image
|
||||
src={item.image?.link || '/images/placeholder-small.jpg'}
|
||||
height={200}
|
||||
alt={item.judul}
|
||||
fit="cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
</Card.Section>
|
||||
<Badge color="blue" variant="light" mt="md">
|
||||
{item.kategoriKegiatan?.nama || kategori}
|
||||
</Badge>
|
||||
<Text fw={600} size="lg" mt="sm" lineClamp={2}>
|
||||
{item.judul}
|
||||
</Text>
|
||||
<Text
|
||||
size="sm"
|
||||
c="dimmed"
|
||||
lineClamp={3}
|
||||
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
|
||||
mt="xs"
|
||||
dangerouslySetInnerHTML={{ __html: item.deskripsiLengkap }}
|
||||
/>
|
||||
<Group justify="apart" mt="md" gap="xs">
|
||||
<Text size="xs" c="dimmed">
|
||||
{new Date(item.createdAt).toLocaleDateString('id-ID', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</Text>
|
||||
<Badge color="gray" variant="outline">Baca Selengkapnya</Badge>
|
||||
</Group>
|
||||
</Card>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
)}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Pagination */}
|
||||
<Center mt="xl">
|
||||
@@ -166,4 +228,4 @@ export default function Content({ kategori }: { kategori: string }) {
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +1,64 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
'use client';
|
||||
|
||||
import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong';
|
||||
import { Badge, Box, Button, Card, Center, Container, Divider, Flex, Grid, GridCol, Group, Image, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Center,
|
||||
Container,
|
||||
Divider,
|
||||
Flex,
|
||||
Grid,
|
||||
GridCol,
|
||||
Group,
|
||||
Image,
|
||||
Pagination,
|
||||
Paper,
|
||||
SimpleGrid,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
Transition,
|
||||
} from '@mantine/core';
|
||||
import { IconArrowRight, IconCalendar } from '@tabler/icons-react';
|
||||
import { useTransitionRouter } from 'next-view-transitions';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
function Page() {
|
||||
export default function Page() {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useTransitionRouter();
|
||||
const router = useRouter();
|
||||
|
||||
// Parameter URL
|
||||
const search = searchParams.get('search') || '';
|
||||
const page = parseInt(searchParams.get('page') || '1');
|
||||
|
||||
// Gunakan proxy untuk state
|
||||
const state = useProxy(gotongRoyongState.kegiatanDesa);
|
||||
const featured = useProxy(gotongRoyongState.kegiatanDesa.findFirst); // ✅ Berita utama
|
||||
const featured = useProxy(gotongRoyongState.kegiatanDesa.findFirst);
|
||||
const loadingGrid = state.findMany.loading;
|
||||
const loadingFeatured = featured.loading;
|
||||
|
||||
// Load berita utama (hanya sekali)
|
||||
useEffect(() => {
|
||||
if (!featured.data && !loadingFeatured) {
|
||||
gotongRoyongState.kegiatanDesa.findFirst.load();
|
||||
}
|
||||
}, [featured.data, loadingFeatured]);
|
||||
|
||||
// Load berita terbaru (untuk grid) saat page/search berubah
|
||||
useEffect(() => {
|
||||
const limit = 3; // Sesuaikan dengan tampilan grid
|
||||
const limit = 3;
|
||||
state.findMany.load(page, limit, search);
|
||||
}, [page, search]);
|
||||
|
||||
// Update URL saat page berubah
|
||||
const handlePageChange = (newPage: number) => {
|
||||
const url = new URLSearchParams(searchParams.toString());
|
||||
if (search) url.set('search', search);
|
||||
if (newPage > 1) url.set('page', newPage.toString());
|
||||
else url.delete('page'); // biar page=1 ga muncul di URL
|
||||
|
||||
else url.delete('page');
|
||||
router.replace(`?${url.toString()}`);
|
||||
};
|
||||
|
||||
@@ -49,121 +66,177 @@ function Page() {
|
||||
const paginatedNews = state.findMany.data || [];
|
||||
const totalPages = state.findMany.totalPages || 1;
|
||||
|
||||
return (
|
||||
<Box py={20}>
|
||||
<Container size="xl" px={{ base: "md", md: "xl" }}>
|
||||
{/* === Gotong royong Utama (Tetap) === */}
|
||||
{loadingFeatured ? (
|
||||
<Center><Skeleton h={400} /></Center>
|
||||
) : featuredData ? (
|
||||
<Box mb={50}>
|
||||
<Text fz="h2" fw={700} mb="md">Gotong royong Utama</Text>
|
||||
<Paper shadow="md" radius="md" withBorder>
|
||||
<Grid gutter={0}>
|
||||
<GridCol span={{ base: 12, md: 6 }}>
|
||||
<Image
|
||||
src={featuredData.image?.link || '/images/placeholder.jpg'}
|
||||
alt={featuredData.judul || 'Gotong royong Utama'}
|
||||
height={400}
|
||||
fit="cover"
|
||||
radius="md"
|
||||
style={{ borderBottomRightRadius: 0, borderTopRightRadius: 0 }}
|
||||
loading="lazy"
|
||||
/>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 6 }} p="xl">
|
||||
<Stack h="100%" justify="space-between">
|
||||
<div>
|
||||
<Badge color="blue" variant="light" mb="md">
|
||||
{featuredData.kategoriKegiatan?.nama || 'Gotong royong'}
|
||||
</Badge>
|
||||
<Title order={2} mb="md">{featuredData.judul}</Title>
|
||||
<Text c="dimmed" lineClamp={3} mb="md" dangerouslySetInnerHTML={{ __html: featuredData.deskripsiSingkat }} />
|
||||
</div>
|
||||
<Group justify="apart" mt="auto">
|
||||
<Group gap="xs">
|
||||
<IconCalendar size={18} />
|
||||
<Text size="sm">
|
||||
{new Date(featuredData.createdAt).toLocaleDateString('id-ID', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
})}
|
||||
</Text>
|
||||
</Group>
|
||||
<Button
|
||||
variant="light"
|
||||
rightSection={<IconArrowRight size={16} />}
|
||||
onClick={() => router.push(`/darmasaba/lingkungan/gotong-royong/${featuredData.kategoriKegiatan?.nama}/${featuredData.id}`)}
|
||||
>
|
||||
Baca Selengkapnya
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
</Paper>
|
||||
</Box>
|
||||
) : null}
|
||||
// Animasi transisi halus tapi tetap instant load
|
||||
const MotionBox = motion(Box as any);
|
||||
|
||||
{/* === Gotong royong Terbaru (Berubah Saat Pagination) === */}
|
||||
// fallback kosong
|
||||
if (!loadingGrid && !loadingFeatured && paginatedNews.length === 0) {
|
||||
return (
|
||||
<Container size="xl" py={80} ta="center">
|
||||
<Title order={2} mb="md">Belum Ada Data Gotong Royong</Title>
|
||||
<Text c="dimmed">Tidak ada data gotong royong yang tersedia saat ini.</Text>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MotionBox
|
||||
key={`${page}-${search}`}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
transition={{ duration: 0.2, ease: 'easeOut' }}
|
||||
py={20}
|
||||
>
|
||||
<Container size="xl" px={{ base: 'md', md: 'xl' }}>
|
||||
{/* === Gotong Royong Utama === */}
|
||||
<Transition mounted={!loadingFeatured} transition="fade" duration={200} timingFunction="ease-out">
|
||||
{(styles) =>
|
||||
featuredData ? (
|
||||
<Box mb={50} style={styles}>
|
||||
<Text fz="h2" fw={700} mb="md">Gotong royong Utama</Text>
|
||||
<Paper shadow="md" radius="md" withBorder>
|
||||
<Grid gutter={0}>
|
||||
<GridCol span={{ base: 12, md: 6 }}>
|
||||
<Image
|
||||
src={featuredData.image?.link || '/images/placeholder.jpg'}
|
||||
alt={featuredData.judul || 'Gotong royong Utama'}
|
||||
height={400}
|
||||
fit="cover"
|
||||
radius="md"
|
||||
style={{ borderBottomRightRadius: 0, borderTopRightRadius: 0 }}
|
||||
loading="lazy"
|
||||
/>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 6 }} p="xl">
|
||||
<Stack h="100%" justify="space-between">
|
||||
<div>
|
||||
<Badge color="blue" variant="light" mb="md">
|
||||
{featuredData.kategoriKegiatan?.nama || 'Gotong royong'}
|
||||
</Badge>
|
||||
<Title order={2} mb="md">{featuredData.judul}</Title>
|
||||
<Text
|
||||
c="dimmed"
|
||||
lineClamp={3}
|
||||
mb="md"
|
||||
dangerouslySetInnerHTML={{ __html: featuredData.deskripsiSingkat }}
|
||||
/>
|
||||
</div>
|
||||
<Group justify="apart" mt="auto">
|
||||
<Group gap="xs">
|
||||
<IconCalendar size={18} />
|
||||
<Text size="sm">
|
||||
{new Date(featuredData.createdAt).toLocaleDateString('id-ID', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</Text>
|
||||
</Group>
|
||||
<Button
|
||||
variant="light"
|
||||
rightSection={<IconArrowRight size={16} />}
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/darmasaba/lingkungan/gotong-royong/${featuredData.kategoriKegiatan?.nama}/${featuredData.id}`
|
||||
)
|
||||
}
|
||||
>
|
||||
Baca Selengkapnya
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
</Paper>
|
||||
</Box>
|
||||
) : (
|
||||
<Skeleton h={400} radius="md" mb="xl" />
|
||||
)
|
||||
}
|
||||
</Transition>
|
||||
|
||||
{/* === Gotong royong Terbaru === */}
|
||||
<Box mt={50}>
|
||||
<Title order={2} mb="md">Gotong royong Terbaru</Title>
|
||||
<Divider mb="xl" />
|
||||
|
||||
{loadingGrid ? (
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl">
|
||||
{Array(3).fill(0).map((_, i) => (
|
||||
<Skeleton key={i} h={300} radius="md" />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
) : paginatedNews.length === 0 ? (
|
||||
<Text c="dimmed" ta="center">Tidak ada gotong royong ditemukan.</Text>
|
||||
) : (
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl" verticalSpacing="xl">
|
||||
{paginatedNews.map((item) => (
|
||||
<Card
|
||||
key={item.id}
|
||||
shadow="sm"
|
||||
p="lg"
|
||||
radius="md"
|
||||
withBorder
|
||||
>
|
||||
<Card.Section>
|
||||
<Image
|
||||
src={item.image?.link || '/images/placeholder-small.jpg'}
|
||||
height={200}
|
||||
alt={item.judul}
|
||||
fit="cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
</Card.Section>
|
||||
<Transition mounted={!loadingGrid} transition="fade" duration={200} timingFunction="ease-out">
|
||||
{(styles) =>
|
||||
loadingGrid ? (
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl">
|
||||
{Array(3)
|
||||
.fill(0)
|
||||
.map((_, i) => (
|
||||
<Skeleton key={i} h={300} radius="md" />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
) : paginatedNews.length === 0 ? (
|
||||
<Text c="dimmed" ta="center">
|
||||
Tidak ada gotong royong ditemukan.
|
||||
</Text>
|
||||
) : (
|
||||
<Box style={styles}>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl" verticalSpacing="xl">
|
||||
{paginatedNews.map((item) => (
|
||||
<Card key={item.id} shadow="sm" p="lg" radius="md" withBorder>
|
||||
<Card.Section>
|
||||
<Image
|
||||
src={item.image?.link || '/images/placeholder-small.jpg'}
|
||||
height={200}
|
||||
alt={item.judul}
|
||||
fit="cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
</Card.Section>
|
||||
|
||||
<Badge color="blue" variant="light" mt="md">
|
||||
{item.kategoriKegiatan?.nama || 'Gotong royong'}
|
||||
</Badge>
|
||||
<Badge color="blue" variant="light" mt="md">
|
||||
{item.kategoriKegiatan?.nama || 'Gotong royong'}
|
||||
</Badge>
|
||||
|
||||
<Text fw={600} size="lg" mt="sm" lineClamp={2}>{item.judul}</Text>
|
||||
<Text fw={600} size="lg" mt="sm" lineClamp={2}>
|
||||
{item.judul}
|
||||
</Text>
|
||||
|
||||
<Text size="sm" c="dimmed" lineClamp={3} mt="xs" dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat }} />
|
||||
<Text
|
||||
size="sm"
|
||||
c="dimmed"
|
||||
lineClamp={3}
|
||||
mt="xs"
|
||||
dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat }}
|
||||
/>
|
||||
|
||||
<Flex align="center" justify="apart" mt="md" gap="xs">
|
||||
<Text size="xs" c="dimmed">
|
||||
{new Date(item.createdAt).toLocaleDateString('id-ID', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric'
|
||||
})}
|
||||
</Text>
|
||||
|
||||
<Button p="xs" variant="light" rightSection={<IconArrowRight size={16} />} onClick={() => router.push(`/darmasaba/lingkungan/gotong-royong/${item.kategoriKegiatan?.nama}/${item.id}`)}>Baca Selengkapnya</Button>
|
||||
</Flex>
|
||||
</Card>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
)}
|
||||
<Flex align="center" justify="apart" mt="md" gap="xs">
|
||||
<Text size="xs" c="dimmed">
|
||||
{new Date(item.createdAt).toLocaleDateString('id-ID', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</Text>
|
||||
|
||||
{/* Pagination hanya untuk berita terbaru */}
|
||||
<Button
|
||||
p="xs"
|
||||
variant="light"
|
||||
rightSection={<IconArrowRight size={16} />}
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/darmasaba/lingkungan/gotong-royong/${item.kategoriKegiatan?.nama}/${item.id}`
|
||||
)
|
||||
}
|
||||
>
|
||||
Baca Selengkapnya
|
||||
</Button>
|
||||
</Flex>
|
||||
</Card>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
</Transition>
|
||||
|
||||
{/* Pagination */}
|
||||
<Center mt="xl">
|
||||
<Pagination
|
||||
total={totalPages}
|
||||
@@ -176,9 +249,6 @@ function Page() {
|
||||
</Center>
|
||||
</Box>
|
||||
</Container>
|
||||
</Box>
|
||||
</MotionBox>
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
import stateBimbinganBelajarDesa from '@/app/admin/(dashboard)/_state/pendidikan/bimbingan-belajar-desa';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Paper, SimpleGrid, Skeleton, Stack, Text, Title, Tooltip, Divider, Badge } from '@mantine/core';
|
||||
import { Box, Paper, SimpleGrid, Skeleton, Stack, Text, Title, Tooltip, Divider, Badge, Group } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import { IconMapPin, IconCalendarTime, IconBook2 } from '@tabler/icons-react';
|
||||
@@ -49,46 +49,46 @@ function Page() {
|
||||
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="xl">
|
||||
<Paper p="xl" radius="lg" shadow="md" withBorder bg={colors['white-trans-1']}>
|
||||
<Stack gap="sm">
|
||||
<Box>
|
||||
<Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
|
||||
{stateTujuanProgram.findById.data?.judul}
|
||||
</Badge>
|
||||
<Group>
|
||||
<Tooltip label="Gambaran manfaat utama program" position="top-start" withArrow>
|
||||
<Box>
|
||||
<IconBook2 size={36} stroke={1.5} color={colors['blue-button']} />
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
|
||||
{stateTujuanProgram.findById.data?.judul}
|
||||
</Badge>
|
||||
</Group>
|
||||
<Text fz="md" style={{wordBreak: "break-word", whiteSpace: "normal"}} lh={1.6} dangerouslySetInnerHTML={{ __html: stateTujuanProgram.findById.data?.deskripsi }} />
|
||||
</Stack>
|
||||
</Paper>
|
||||
<Paper p="xl" radius="lg" shadow="md" withBorder bg={colors['white-trans-1']}>
|
||||
<Stack gap="sm">
|
||||
<Box>
|
||||
<Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
|
||||
{stateLokasiDanJadwal.findById.data?.judul}
|
||||
</Badge>
|
||||
<Group>
|
||||
<Tooltip label="Tempat dan waktu pelaksanaan" position="top-start" withArrow>
|
||||
<Box>
|
||||
<IconMapPin size={36} stroke={1.5} color={colors['blue-button']} />
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
|
||||
{stateLokasiDanJadwal.findById.data?.judul}
|
||||
</Badge>
|
||||
</Group>
|
||||
<Text fz="md" lh={1.6} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: stateLokasiDanJadwal.findById.data?.deskripsi }} />
|
||||
</Stack>
|
||||
</Paper>
|
||||
<Paper p="xl" radius="lg" shadow="md" withBorder bg={colors['white-trans-1']}>
|
||||
<Stack gap="sm">
|
||||
<Box>
|
||||
<Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
|
||||
{stateFasilitas.findById.data?.judul}
|
||||
</Badge>
|
||||
<Group>
|
||||
<Tooltip label="Sarana yang disediakan untuk peserta" position="top-start" withArrow>
|
||||
<Box>
|
||||
<IconCalendarTime size={36} stroke={1.5} color={colors['blue-button']} />
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
|
||||
{stateFasilitas.findById.data?.judul}
|
||||
</Badge>
|
||||
</Group>
|
||||
<Text fz="md" lh={1.6} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: stateFasilitas.findById.data?.deskripsi }} />
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
@@ -56,7 +56,7 @@ export default function Content() {
|
||||
try {
|
||||
await state.dataPerpustakaan.findMany.load(
|
||||
currentPage,
|
||||
10,
|
||||
3,
|
||||
searchQuery,
|
||||
''
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
@@ -61,8 +71,8 @@ export default function Page() {
|
||||
</Box>
|
||||
</Container>
|
||||
|
||||
{/* Tombol Scroll ke Atas */}
|
||||
<ScrollToTopButton />
|
||||
{/* Tombol Scroll ke Atas */}
|
||||
<ScrollToTopButton />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
</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}>
|
||||
|
||||
@@ -1,6 +1,63 @@
|
||||
'use client'
|
||||
import { ActionIcon, Anchor, Box, Button, Center, Container, Divider, Flex, Group, Image, Paper, SimpleGrid, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import { IconAt, IconBrandFacebook, IconBrandInstagram, IconBrandTwitter, IconBrandWhatsapp } from '@tabler/icons-react';
|
||||
import { IconAt, IconBrandFacebook, IconBrandInstagram, IconBrandTiktok, IconBrandYoutube } from '@tabler/icons-react';
|
||||
|
||||
const sosialMedia = [
|
||||
{
|
||||
title: "Facebook",
|
||||
link: "https://www.facebook.com/DarmasabaDesaku",
|
||||
icon: IconBrandFacebook,
|
||||
},
|
||||
{
|
||||
title: "Instagram",
|
||||
link: "https://www.instagram.com/ddarmasaba/",
|
||||
icon: IconBrandInstagram,
|
||||
},
|
||||
{
|
||||
title: "Youtube",
|
||||
link: "https://www.youtube.com/channel/UCtPw9WOQO7d2HIKzKgel4Xg",
|
||||
icon: IconBrandYoutube,
|
||||
},
|
||||
{
|
||||
title: "Tiktok",
|
||||
link: "https://www.tiktok.com/@desa.darmasaba?is_from_webapp=1&sender_device=pc",
|
||||
icon: IconBrandTiktok,
|
||||
},
|
||||
]
|
||||
|
||||
const layanandesa = [
|
||||
{
|
||||
title: "Administrasi Kependudukan",
|
||||
link: "/darmasaba/desa/layanan/",
|
||||
},
|
||||
{
|
||||
title: "Layanan Sosial",
|
||||
link: "/darmasaba/ekonomi/program-kemiskinan",
|
||||
},
|
||||
{
|
||||
title: "Pengaduan Masyarakat",
|
||||
link: "/darmasaba/keamanan/laporan-publik",
|
||||
},
|
||||
{
|
||||
title: "Informasi Publik",
|
||||
link: "/darmasaba/ppid/daftar-informasi-publik-desa-darmasaba",
|
||||
},
|
||||
]
|
||||
|
||||
const tautanPenting = [
|
||||
{
|
||||
title: "Portal Badung",
|
||||
link: "/darmasaba/desa/berita/semua",
|
||||
},
|
||||
{
|
||||
title: "E-Government",
|
||||
link: "/darmasaba/inovasi/desa-digital-smart-village",
|
||||
},
|
||||
{
|
||||
title: "Transparansi",
|
||||
link: "/darmasaba/ppid/daftar-informasi-publik-desa-darmasaba",
|
||||
}
|
||||
]
|
||||
|
||||
function Footer() {
|
||||
return (
|
||||
@@ -46,7 +103,7 @@ function Footer() {
|
||||
<Group justify="apart" align="center" mt="lg">
|
||||
<Text c="#F3F2EC" ta="center" fz="md" fw={700} style={{ fontStyle: 'italic' }}>"Desa Kuat, Warga Sejahtera!"</Text>
|
||||
<ActionIcon size={80} radius="xl" variant="transparent">
|
||||
<Image src="/chatbot-removebg-preview.png" alt="Logo Desa" width={80} height={80} loading="lazy"/>
|
||||
<Image src="/chatbot-removebg-preview.png" alt="Logo Desa" width={80} height={80} loading="lazy" />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Stack>
|
||||
@@ -64,31 +121,39 @@ function Footer() {
|
||||
Darmasaba adalah desa budaya yang kaya akan tradisi dan nilai-nilai warisan Bali.
|
||||
</Text>
|
||||
<Flex gap="md" mt="sm" c="#F3F2EC">
|
||||
<ActionIcon variant="subtle" color="white"><IconBrandFacebook size={22} /></ActionIcon>
|
||||
<ActionIcon variant="subtle" color="white"><IconBrandInstagram size={22} /></ActionIcon>
|
||||
<ActionIcon variant="subtle" color="white"><IconBrandTwitter size={22} /></ActionIcon>
|
||||
<ActionIcon variant="subtle" color="white"><IconBrandWhatsapp size={22} /></ActionIcon>
|
||||
{sosialMedia.map((item) => (
|
||||
<ActionIcon
|
||||
key={item.title}
|
||||
component="a"
|
||||
href={item.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="subtle"
|
||||
color="white"
|
||||
>
|
||||
<item.icon size={22} />
|
||||
</ActionIcon>
|
||||
))}
|
||||
</Flex>
|
||||
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Stack gap="xs">
|
||||
<Text c="white" fz="md" fw={700}>Layanan Desa</Text>
|
||||
<Anchor c="#F3F2EC" fz="xs">Administrasi Kependudukan</Anchor>
|
||||
<Anchor c="#F3F2EC" fz="xs">Layanan Sosial</Anchor>
|
||||
<Anchor c="#F3F2EC" fz="xs">Pengaduan Masyarakat</Anchor>
|
||||
<Anchor c="#F3F2EC" fz="xs">Informasi Publik</Anchor>
|
||||
{layanandesa.map((item) => (
|
||||
<Anchor key={item.title} c="#F3F2EC" fz="xs" href={item.link}>{item.title}</Anchor>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Stack gap="xs">
|
||||
<Text c="white" fz="md" fw={700}>Tautan Penting</Text>
|
||||
<Anchor c="#F3F2EC" fz="xs">Portal Badung</Anchor>
|
||||
<Anchor c="#F3F2EC" fz="xs">E-Government</Anchor>
|
||||
<Anchor c="#F3F2EC" fz="xs">Transparansi</Anchor>
|
||||
<Anchor c="#F3F2EC" fz="xs">Unduhan</Anchor>
|
||||
{tautanPenting.map((item) => (
|
||||
<Anchor key={item.title} c="#F3F2EC" fz="xs" href={item.link}>{item.title}</Anchor>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -11,16 +11,20 @@ export function NavbarSearch() {
|
||||
// Close when clicking outside
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
||||
const target = event.target as HTMLElement;
|
||||
// Only close if clicking outside both the search input and results
|
||||
if (
|
||||
containerRef.current &&
|
||||
!containerRef.current.contains(target) &&
|
||||
!target.closest('.search-result-item') // Add a class to your search result items
|
||||
) {
|
||||
setIsOpen(false);
|
||||
stateNav.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Add event listener
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
// Clean up
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -86,10 +86,15 @@ function NavbarMobile({ listNavbar }: { listNavbar: MenuItem[] }) {
|
||||
align="center"
|
||||
p="xs"
|
||||
onClick={() => {
|
||||
router.push(item.href);
|
||||
stateNav.mobileOpen = false;
|
||||
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}
|
||||
|
||||
@@ -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 (
|
||||
<Button
|
||||
ref={ref}
|
||||
color={hovered ? "gray" : colors["blue-button"]}
|
||||
onMouseEnter={() => {
|
||||
stateNav.item = item.children || null
|
||||
stateNav.isSearch = false
|
||||
<Menu
|
||||
trigger="hover"
|
||||
position="bottom-start"
|
||||
offset={20}
|
||||
width={300}
|
||||
shadow="md"
|
||||
withinPortal
|
||||
onOpen={() => {
|
||||
stateNav.item = item.children || null;
|
||||
stateNav.isSearch = false;
|
||||
}}
|
||||
variant="subtle"
|
||||
radius="xl"
|
||||
onClick={() => {
|
||||
router.push(item.href)
|
||||
stateNav.clear()
|
||||
}}
|
||||
fw={500}
|
||||
>
|
||||
{item.name}
|
||||
</Button>
|
||||
<MenuTarget>
|
||||
<Button
|
||||
variant="subtle"
|
||||
color={isActive ? 'blue' : 'gray'}
|
||||
onClick={() => {
|
||||
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',
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</Button>
|
||||
</MenuTarget>
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)}
|
||||
@@ -32,33 +33,34 @@ export function NavbarSubMenu({ item }: { item: MenuItem[] | null }) {
|
||||
<Stack gap="xs" align="stretch">
|
||||
{item.map((link, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
variant="subtle"
|
||||
justify="space-between"
|
||||
size="lg"
|
||||
radius="md"
|
||||
color="gray.0"
|
||||
onClick={() => {
|
||||
key={index}
|
||||
variant="subtle"
|
||||
justify="space-between"
|
||||
size="lg"
|
||||
radius="md"
|
||||
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,
|
||||
transition: "all 0.2s ease",
|
||||
"&:hover": {
|
||||
background: theme.colors.gray[8],
|
||||
boxShadow: `0 0 12px ${theme.colors.blue[6]}55`,
|
||||
},
|
||||
},
|
||||
})}
|
||||
>
|
||||
{link.name}
|
||||
</Button>
|
||||
}
|
||||
}}
|
||||
rightSection={<IconArrowRight size={18} />}
|
||||
styles={(theme) => ({
|
||||
root: {
|
||||
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: pathname === link.href ? theme.colors.blue[1] : theme.colors.gray[0],
|
||||
}
|
||||
},
|
||||
})}
|
||||
>
|
||||
{link.name}
|
||||
</Button>
|
||||
))}
|
||||
</Stack>
|
||||
) : (
|
||||
|
||||
@@ -1,91 +1,144 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'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, Popover, 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 [opened, setOpened] = useState(false);
|
||||
|
||||
// Infinite scroll
|
||||
// buka popover saat ada query
|
||||
useEffect(() => {
|
||||
setOpened(!!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);
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, [snap.loading]);
|
||||
|
||||
return (
|
||||
<Stack maw={800} mx="auto">
|
||||
{/* 🔍 Search input */}
|
||||
<TextInput
|
||||
placeholder="Cari apapun..."
|
||||
value={snap.query}
|
||||
onChange={(e) => (
|
||||
searchState.query = e.currentTarget.value,
|
||||
debouncedFetch()
|
||||
)}
|
||||
radius="xl"
|
||||
rightSection={
|
||||
snap.query ? (
|
||||
<IconX
|
||||
size={16}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
searchState.query = '';
|
||||
searchState.results = [];
|
||||
}}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
const handleSelect = async (e: React.MouseEvent, item: any) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
{/* 📄 Hasil pencarian */}
|
||||
<div style={{ maxHeight: '400px', overflowY: 'auto' }}>
|
||||
{snap.results.map((item, i) => (
|
||||
<Box
|
||||
key={i}
|
||||
p="sm"
|
||||
const url = getDetailUrl(item);
|
||||
if (!url) return;
|
||||
|
||||
// Immediately close the search dropdown
|
||||
setOpened(false);
|
||||
searchState.results = []; // Clear results immediately
|
||||
searchState.loading = false;
|
||||
|
||||
// Use window.location for navigation to ensure full page reload
|
||||
window.location.href = url;
|
||||
};
|
||||
|
||||
return (
|
||||
<Box pos="relative">
|
||||
<Popover
|
||||
opened={opened && !!snap.query}
|
||||
onChange={(isOpen) => {
|
||||
if (!isOpen) {
|
||||
// Clear search state when popover is closed
|
||||
searchState.query = '';
|
||||
searchState.results = [];
|
||||
searchState.page = 1;
|
||||
searchState.nextPage = null;
|
||||
}
|
||||
setOpened(isOpen);
|
||||
}}
|
||||
width="target"
|
||||
position="bottom"
|
||||
shadow="md"
|
||||
withinPortal
|
||||
radius="md"
|
||||
zIndex={1000} // Add this line to ensure it appears above other elements
|
||||
styles={{
|
||||
dropdown: {
|
||||
zIndex: 1000, // Add this to ensure the dropdown appears above other elements
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Popover.Target>
|
||||
<TextInput
|
||||
placeholder="Cari apapun..."
|
||||
value={snap.query}
|
||||
onChange={(e) => {
|
||||
searchState.query = e.currentTarget.value;
|
||||
debouncedFetch();
|
||||
}}
|
||||
radius="xl"
|
||||
size="md"
|
||||
rightSection={
|
||||
snap.query ? (
|
||||
<IconX
|
||||
size={16}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
searchState.query = '';
|
||||
searchState.results = [];
|
||||
searchState.page = 1;
|
||||
searchState.nextPage = null;
|
||||
setOpened(false);
|
||||
}}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
</Popover.Target>
|
||||
|
||||
<Popover.Dropdown
|
||||
p={0}
|
||||
style={{
|
||||
borderBottom: '1px solid #eee',
|
||||
cursor: 'pointer',
|
||||
transition: 'background 0.2s',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
maxWidth: '100%'
|
||||
}}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.background = '#f5f5f5')}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')}
|
||||
onClick={() => {
|
||||
const url = getDetailUrl(item);
|
||||
window.location.href = url;
|
||||
maxHeight: 350,
|
||||
overflowY: 'auto',
|
||||
borderRadius: 12,
|
||||
zIndex: 1000, // Add this line to ensure dropdown stays above other elements
|
||||
position: 'relative', // Add this to contain child elements
|
||||
}}
|
||||
>
|
||||
<Text size="sm" fw={500}>
|
||||
{item.judul || item.namaPasar || item.nama || item.name}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
dari modul: {item.type}
|
||||
</Text>
|
||||
</Box>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* ⏳ Loader di bawah hasil */}
|
||||
{snap.loading && (
|
||||
<Center py="md">
|
||||
<Loader size="sm" />
|
||||
</Center>
|
||||
)}
|
||||
</Stack>
|
||||
{snap.results.length > 0 ? (
|
||||
snap.results.map((item, i) => (
|
||||
<Box
|
||||
key={i}
|
||||
p="sm"
|
||||
className="search-result-item" // Add this class
|
||||
style={{
|
||||
borderBottom: '1px solid #eee',
|
||||
cursor: 'pointer',
|
||||
transition: 'background 0.2s',
|
||||
position: 'relative', // Add this
|
||||
zIndex: 1, // Add this to ensure proper stacking context
|
||||
backgroundColor: 'white', // Ensure background is set
|
||||
}}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.background = '#f7f7f7')}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')}
|
||||
onClick={(e) => handleSelect(e, item)} // Pass the event here
|
||||
>
|
||||
<Text size="sm" fw={500}>
|
||||
{item.judul || item.namaPasar || item.nama || item.name}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
dari modul: {item.type}
|
||||
</Text>
|
||||
</Box>
|
||||
))
|
||||
) : (
|
||||
<Center py="md">
|
||||
{snap.loading ? <Loader size="sm" /> : <Text fz="sm">Tidak ada hasil</Text>}
|
||||
</Center>
|
||||
)}
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,9 +37,12 @@ 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',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Stack align="center" gap="xs" w="100%" py="xl">
|
||||
@@ -49,13 +52,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">
|
||||
|
||||
@@ -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}>
|
||||
@@ -67,49 +67,62 @@ export default function SDGS() {
|
||||
>
|
||||
{sdgsDesa && sdgsDesa.length > 0 ? (
|
||||
<SimpleGrid cols={{ base: 1, sm: 3 }} spacing="xl" verticalSpacing="xl">
|
||||
{sdgsDesa.map((item) => (
|
||||
<Paper
|
||||
key={item.id}
|
||||
p="lg"
|
||||
radius="xl"
|
||||
shadow="sm"
|
||||
withBorder
|
||||
style={{
|
||||
background: "linear-gradient(180deg, #FFFFFF, #F6F8FA)",
|
||||
border: "1px solid rgba(0,0,0,0.05)",
|
||||
transition: "all 0.3s ease",
|
||||
}}
|
||||
>
|
||||
<Center mb="lg">
|
||||
<Box
|
||||
p="md"
|
||||
style={{
|
||||
background: "rgba(240, 249, 255, 0.8)",
|
||||
backdropFilter: "blur(6px)",
|
||||
width: mobile ? 140 : 160,
|
||||
height: mobile ? 140 : 160,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: "1rem",
|
||||
boxShadow: "0 6px 16px rgba(0,0,0,0.06)",
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={item.image?.link ? item.image.link : "/placeholder-sdgs.png"}
|
||||
alt={item.name}
|
||||
w={mobile ? 90 : 110}
|
||||
h={mobile ? 90 : 110}
|
||||
fit="contain"
|
||||
loading="lazy"
|
||||
/>
|
||||
</Box>
|
||||
</Center>
|
||||
{sdgsDesa.map((item) => (
|
||||
<Paper
|
||||
key={item.id}
|
||||
p="lg"
|
||||
radius="xl"
|
||||
shadow="sm"
|
||||
withBorder
|
||||
style={{
|
||||
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">
|
||||
<Box
|
||||
p="md"
|
||||
style={{
|
||||
background: "rgba(240, 249, 255, 0.8)",
|
||||
backdropFilter: "blur(6px)",
|
||||
width: mobile ? 140 : 160,
|
||||
height: mobile ? 140 : 160,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: "1rem",
|
||||
boxShadow: "0 6px 16px rgba(0,0,0,0.06)",
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={item.image?.link ? item.image.link : "/placeholder-sdgs.png"}
|
||||
alt={item.name}
|
||||
w={mobile ? 90 : 110}
|
||||
h={mobile ? 90 : 110}
|
||||
fit="contain"
|
||||
loading="lazy"
|
||||
/>
|
||||
</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>
|
||||
</Paper>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
|
||||
) : (
|
||||
<Center mih={200} style={{ flexDirection: "column" }}>
|
||||
<IconMoodSad size={48} stroke={1.5} style={{ marginBottom: "1rem" }} />
|
||||
|
||||
@@ -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/edukasi-lingkungan',
|
||||
materiEdukasiLingkungan: '/darmasaba/lingkungan/edukasi-lingkungan',
|
||||
contohEdukasiLingkungan: '/darmasaba/lingkungan/edukasi-lingkungan',
|
||||
filosofiTriHita: '/darmasaba/lingkungan/konservasi-adat-bali',
|
||||
bentukKonservasiBerdasarkanAdat: '/darmasaba/lingkungan/konservasi-adat-bali',
|
||||
nilaiKonservasiAdat: '/darmasaba/lingkungan/konservasi-adat-bali',
|
||||
jenjangPendidikan: '/darmasaba/pendidikan/info-sekolah/semua',
|
||||
lembaga: '/darmasaba/pendidikan/info-sekolah/semua/lembaga',
|
||||
siswa: '/darmasaba/pendidikan/info-sekolah/semua/siswa',
|
||||
pengajar: '/darmasaba/pendidikan/info-sekolah/semua/pengajar',
|
||||
keunggulanProgram: '/darmasaba/pendidikan/beasiswa-desa',
|
||||
tujuanProgram: '/darmasaba/pendidikan/program-pendidikan-anak',
|
||||
programUnggulan: '/darmasaba/pendidikan/program-pendidikan-anak',
|
||||
lokasiJadwalBimbinganBelajarDesa: '/darmasaba/pendidikan/bimbingan-belajar-desa',
|
||||
fasilitasBimbinganBelajarDesa: '/darmasaba/pendidikan/bimbingan-belajar-desa',
|
||||
tujuanPendidikanNonFormal: '/darmasaba/pendidikan/pendidikan-non-formal',
|
||||
tempatKegiatan: '/darmasaba/pendidikan/pendidikan-non-formal',
|
||||
jenisProgramYangDiselenggarakan: '/darmasaba/pendidikan/pendidikan-non-formal',
|
||||
dataPerpustakaan: '/darmasaba/pendidikan/perpustakaan-digital/semua',
|
||||
dataPendidikan: '/darmasaba/pendidikan/data-pendidikan',
|
||||
|
||||
};
|
||||
|
||||
return typeUrlMap[type || ''] || '/darmasaba';
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
-webkit-backdrop-filter: blur(40px);
|
||||
backdrop-filter: blur(40px);
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
z-index: 50;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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[] }
|
||||
)
|
||||
Reference in New Issue
Block a user