Compare commits
6 Commits
nico/7-okt
...
nico/15-ok
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b574406e2 | |||
| ccf39bc778 | |||
| 3c21f7742c | |||
| a158241c0b | |||
| 80c5dc6361 | |||
| 8ad38fc907 |
@@ -1,5 +1,4 @@
|
||||
[
|
||||
{ "name": "Semua" },
|
||||
{ "name": "Pemerintahan" },
|
||||
{ "name": "Pembangunan" },
|
||||
{ "name": "Ekonomi" },
|
||||
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{ "nama": "Kebersihan" },
|
||||
{ "nama": "Infrastruktur" },
|
||||
{ "nama": "Sosial" },
|
||||
{ "nama": "Lingkungan" }
|
||||
]
|
||||
@@ -0,0 +1,9 @@
|
||||
[
|
||||
{ "id": "cmghqwjs4000404l8c5uvc300", "nama": "PAUD" },
|
||||
{ "id": "cmghqwjs4000404l8c5uvc301", "nama": "TK" },
|
||||
{ "id": "cmghqwjs4000404l8c5uvc302", "nama": "SD" },
|
||||
{ "id": "cmghqwjs4000404l8c5uvc303", "nama": "SMP" },
|
||||
{ "id": "cmghqwjs4000404l8c5uvc304", "nama": "SMA" },
|
||||
{ "id": "cmghqwjs4000404l8c5uvc305", "nama": "SMK" }
|
||||
]
|
||||
|
||||
@@ -143,7 +143,7 @@ model MediaSosial {
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
//========================================= PROFILE ========================================= //
|
||||
//========================================= DESA ANTI KORUPSI ========================================= //
|
||||
model DesaAntiKorupsi {
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
@@ -324,8 +324,8 @@ model PegawaiPPID {
|
||||
model StrukturOrganisasiPPID {
|
||||
id String @id @default(uuid())
|
||||
posisiOrganisasiId String @db.VarChar(50)
|
||||
pegawaiId String
|
||||
hubunganOrganisasiId String
|
||||
pegawaiId String
|
||||
hubunganOrganisasiId String
|
||||
posisiOrganisasi PosisiOrganisasiPPID @relation(fields: [posisiOrganisasiId], references: [id])
|
||||
pegawai PegawaiPPID @relation(fields: [pegawaiId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
@@ -1450,8 +1450,8 @@ model PegawaiBumDes {
|
||||
model StrukturOrganisasiBumDes {
|
||||
id String @id @default(uuid())
|
||||
posisiOrganisasiId String @db.VarChar(50)
|
||||
pegawaiId String
|
||||
hubunganOrganisasiId String
|
||||
pegawaiId String
|
||||
hubunganOrganisasiId String
|
||||
posisiOrganisasi PosisiOrganisasiBumDes @relation(fields: [posisiOrganisasiId], references: [id])
|
||||
pegawai PegawaiBumDes @relation(fields: [pegawaiId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
@@ -2087,6 +2087,9 @@ model DataPerpustakaan {
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime @default(now())
|
||||
isActive Boolean @default(true)
|
||||
|
||||
// relasi baru ke peminjaman
|
||||
peminjamanBuku PeminjamanBuku[]
|
||||
}
|
||||
|
||||
model KategoriBuku {
|
||||
@@ -2099,6 +2102,31 @@ model KategoriBuku {
|
||||
DataPerpustakaan DataPerpustakaan[]
|
||||
}
|
||||
|
||||
model PeminjamanBuku {
|
||||
id String @id @default(cuid())
|
||||
nama String
|
||||
noTelp String
|
||||
alamat String
|
||||
bukuId String
|
||||
tanggalPinjam DateTime @default(now())
|
||||
batasKembali DateTime // tenggat waktu pengembalian
|
||||
tanggalKembali DateTime? // diisi saat dikembalikan
|
||||
status StatusPeminjaman @default(Dipinjam)
|
||||
catatan String? // opsional, misal: kondisi buku
|
||||
buku DataPerpustakaan @relation(fields: [bukuId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime @default(now())
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
enum StatusPeminjaman {
|
||||
Dipinjam
|
||||
Dikembalikan
|
||||
Terlambat
|
||||
Dibatalkan
|
||||
}
|
||||
|
||||
// ========================================= USER ========================================= //
|
||||
|
||||
model User {
|
||||
|
||||
@@ -33,11 +33,12 @@ import detailDataPengangguran from "./data/ekonomi/jumlah-pengangguran/detail-da
|
||||
import kategoriProduk from "./data/ekonomi/pasar-desa/kategori-produk.json";
|
||||
import pegawai from "./data/ekonomi/struktur-organisasi/pegawai-bumdes.json";
|
||||
import posisiOrganisasi from "./data/ekonomi/struktur-organisasi/posisi-organisasi-bumdes.json";
|
||||
import kategoriBerita from "./data/kategori-berita.json";
|
||||
import kategoriBerita from "./data/desa/berita/kategori-berita.json";
|
||||
import contohEdukasiLingkungan from "./data/lingkungan/edukasi-lingkungan/contoh-kegiatan-di-desa-darmasaba.json";
|
||||
import materiEdukasiLingkungan from "./data/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan.json";
|
||||
import tujuanEdukasiLingkungan from "./data/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan.json";
|
||||
import bentukKonservasiBerdasarkanAdat from "./data/lingkungan/konservasi-adat-bali/bentuk-konservasi.json";
|
||||
import kategoriKegiatanData from "./data/lingkungan/gotong-royong/kategori-gotong-royong.json";
|
||||
import filosofiTriHita from "./data/lingkungan/konservasi-adat-bali/filosofi-tri-hita.json";
|
||||
import nilaiKonservasiAdat from "./data/lingkungan/konservasi-adat-bali/nilai-konservasi-adat.json";
|
||||
import caraMemperolehInformasi from "./data/list-caraMemperolehInformasi.json";
|
||||
@@ -55,6 +56,7 @@ import tujuanProgram from "./data/pendidikan/program-pendidikan-anak/tujuan-prog
|
||||
import roles from "./data/user/roles.json";
|
||||
import users from "./data/user/users.json";
|
||||
import fileStorage from "./data/file-storage.json";
|
||||
import jenjangPendidikan from "./data/pendidikan/info-sekolah/jenjang-pendidikan.json";
|
||||
import seedAssets from "./seed_assets";
|
||||
import { safeSeedUnique } from "./safeseedUnique";
|
||||
|
||||
@@ -885,6 +887,30 @@ import { safeSeedUnique } from "./safeseedUnique";
|
||||
}
|
||||
console.log("📊 detailDataPengangguran success ...");
|
||||
|
||||
// =========== KATEGORI GOTONG ROYONG ===========
|
||||
// Add IDs to the kategoriKegiatan data
|
||||
const kategoriKegiatan = kategoriKegiatanData.map((k, index) => ({
|
||||
...k,
|
||||
id: `kategori-${index + 1}`
|
||||
}));
|
||||
|
||||
for (const k of kategoriKegiatan) {
|
||||
await prisma.kategoriKegiatan.upsert({
|
||||
where: {
|
||||
id: k.id,
|
||||
},
|
||||
update: {
|
||||
nama: k.nama,
|
||||
},
|
||||
create: {
|
||||
id: k.id,
|
||||
nama: k.nama,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("kategori kegiatan success ...");
|
||||
|
||||
for (const e of tujuanEdukasiLingkungan) {
|
||||
await prisma.tujuanEdukasiLingkungan.upsert({
|
||||
where: {
|
||||
@@ -1139,6 +1165,22 @@ import { safeSeedUnique } from "./safeseedUnique";
|
||||
"✅ fasilitas bimbingan belajar desa seeded (editable later via UI)"
|
||||
);
|
||||
|
||||
for (const j of jenjangPendidikan) {
|
||||
await prisma.jenjangPendidikan.upsert({
|
||||
where: {
|
||||
id: j.id || undefined,
|
||||
},
|
||||
update: {
|
||||
nama: j.nama,
|
||||
},
|
||||
create: {
|
||||
nama: j.nama,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("✅ Jenjang Pendidikan seeded successfully");
|
||||
|
||||
// seed assets
|
||||
await seedAssets();
|
||||
|
||||
|
||||
@@ -332,7 +332,7 @@ const keunggulanProgram = proxy({
|
||||
].post(keunggulanProgram.create.form);
|
||||
if (res.status === 200) {
|
||||
keunggulanProgram.findMany.load();
|
||||
return toast.success("Data Berhasil Dibuat, Silahkan Menunggu Konfirmasi dari Admin di WhatsApp");
|
||||
return toast.success("Data Berhasil Dibuat");
|
||||
}
|
||||
console.log(res);
|
||||
return toast.error("failed create");
|
||||
|
||||
@@ -95,6 +95,41 @@ const dataPerpustakaan = proxy({
|
||||
}
|
||||
},
|
||||
},
|
||||
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);
|
||||
dataPerpustakaan.findManyAll.data = [];
|
||||
} finally {
|
||||
dataPerpustakaan.findManyAll.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
findUnique: {
|
||||
data: null as Prisma.DataPerpustakaanGetPayload<{
|
||||
include: {
|
||||
@@ -514,9 +549,312 @@ const kategoriBuku = proxy({
|
||||
},
|
||||
});
|
||||
|
||||
const templatePeminjamanBuku = z.object({
|
||||
nama: z.string().min(1, "Nama harus diisi"),
|
||||
noTelp: z.string().min(1, "No Telp harus diisi"),
|
||||
alamat: z.string().min(1, "Alamat harus diisi"),
|
||||
bukuId: z.string().min(1, "Buku ID harus diisi"),
|
||||
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")
|
||||
});
|
||||
|
||||
const defaultPeminjamanBuku = {
|
||||
nama: "",
|
||||
noTelp: "",
|
||||
alamat: "",
|
||||
bukuId: "",
|
||||
tanggalPinjam: "",
|
||||
batasKembali: "",
|
||||
tanggalKembali: "",
|
||||
catatan: ""
|
||||
};
|
||||
|
||||
interface FormEditData {
|
||||
nama: string;
|
||||
noTelp: string;
|
||||
alamat: string;
|
||||
bukuId: string;
|
||||
buku?: {
|
||||
id: string;
|
||||
judul: string;
|
||||
};
|
||||
tanggalPinjam: string;
|
||||
batasKembali: string;
|
||||
tanggalKembali: string;
|
||||
catatan: string;
|
||||
status: 'Dipinjam' | 'Dikembalikan' | 'Terlambat' | 'Dibatalkan';
|
||||
}
|
||||
|
||||
const editForm: FormEditData = {
|
||||
nama: "",
|
||||
noTelp: "",
|
||||
alamat: "",
|
||||
bukuId: "",
|
||||
tanggalPinjam: "",
|
||||
batasKembali: "",
|
||||
tanggalKembali: "",
|
||||
catatan: "",
|
||||
status: "Dipinjam"
|
||||
}
|
||||
|
||||
const peminjamanBuku = proxy({
|
||||
create: {
|
||||
form: { ...defaultPeminjamanBuku },
|
||||
loading: false,
|
||||
async create() {
|
||||
const cek = templatePeminjamanBuku.safeParse(peminjamanBuku.create.form);
|
||||
if (!cek.success) {
|
||||
const err = `[${cek.error.issues
|
||||
.map((v) => `${v.path.join(".")}`)
|
||||
.join("\n")}] required`;
|
||||
return toast.error(err);
|
||||
}
|
||||
|
||||
try {
|
||||
peminjamanBuku.create.loading = true;
|
||||
const res =
|
||||
await ApiFetch.api.pendidikan.perpustakaandigital.peminjamanbuku[
|
||||
"create"
|
||||
].post(peminjamanBuku.create.form);
|
||||
if (res.status === 200) {
|
||||
peminjamanBuku.findMany.load();
|
||||
return toast.success("Data Peminjaman Buku Berhasil Dibuat");
|
||||
}
|
||||
console.log(res);
|
||||
return toast.error("failed create");
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return toast.error("failed create");
|
||||
} finally {
|
||||
peminjamanBuku.create.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
findMany: {
|
||||
data: [] as Prisma.PeminjamanBukuGetPayload<{
|
||||
include: {
|
||||
buku: true;
|
||||
};
|
||||
}>[],
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
loading: false,
|
||||
search: "",
|
||||
load: async (page = 1, limit = 10, search = "") => {
|
||||
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 });
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
peminjamanBuku.findMany.data = res.data.data ?? [];
|
||||
peminjamanBuku.findMany.totalPages = res.data.totalPages ?? 1;
|
||||
} else {
|
||||
peminjamanBuku.findMany.data = [];
|
||||
peminjamanBuku.findMany.totalPages = 1;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Gagal fetch data peminjaman buku paginated:", err);
|
||||
peminjamanBuku.findMany.data = [];
|
||||
peminjamanBuku.findMany.totalPages = 1;
|
||||
} finally {
|
||||
peminjamanBuku.findMany.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
findUnique: {
|
||||
data: null as Prisma.PeminjamanBukuGetPayload<{
|
||||
include: {
|
||||
buku: true;
|
||||
};
|
||||
}> | null,
|
||||
loading: false,
|
||||
async load(id: string) {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/pendidikan/perpustakaandigital/peminjamanbuku/${id}`
|
||||
);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
peminjamanBuku.findUnique.data = data.data ?? null;
|
||||
} else {
|
||||
console.error("Failed to fetch data", res.status, res.statusText);
|
||||
peminjamanBuku.findUnique.data = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
peminjamanBuku.findUnique.data = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
delete: {
|
||||
loading: false,
|
||||
async delete(id: string) {
|
||||
if (!id) return toast.warn("ID tidak valid");
|
||||
|
||||
try {
|
||||
peminjamanBuku.delete.loading = true;
|
||||
|
||||
const response = await fetch(
|
||||
`/api/pendidikan/perpustakaandigital/peminjamanbuku/del/${id}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result?.success) {
|
||||
toast.success(
|
||||
result.message || "Data Peminjaman Buku berhasil dihapus"
|
||||
);
|
||||
await peminjamanBuku.findMany.load(); // refresh list
|
||||
} else {
|
||||
toast.error(result?.message || "Gagal menghapus Data Peminjaman Buku");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Gagal delete:", error);
|
||||
toast.error("Terjadi kesalahan saat menghapus Data Peminjaman Buku");
|
||||
} finally {
|
||||
peminjamanBuku.delete.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
update: {
|
||||
id: "",
|
||||
form: { ...editForm },
|
||||
loading: false,
|
||||
async load(id: string) {
|
||||
if (!id) {
|
||||
toast.warn("ID tidak valid");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/pendidikan/perpustakaandigital/peminjamanbuku/${id}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result?.success) {
|
||||
const data = result.data;
|
||||
this.id = data.id;
|
||||
this.form = {
|
||||
nama: data.nama,
|
||||
noTelp: data.noTelp,
|
||||
alamat: data.alamat,
|
||||
bukuId: data.bukuId,
|
||||
tanggalPinjam: data.tanggalPinjam,
|
||||
batasKembali: data.batasKembali,
|
||||
tanggalKembali: data.tanggalKembali,
|
||||
catatan: data.catatan,
|
||||
status: data.status
|
||||
};
|
||||
return data; // Return the loaded data
|
||||
} else {
|
||||
throw new Error(result?.message || "Gagal memuat data");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading peminjaman buku:", error);
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "Gagal memuat data"
|
||||
);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
async update() {
|
||||
const cek = templatePeminjamanBuku.safeParse(peminjamanBuku.update.form);
|
||||
if (!cek.success) {
|
||||
const err = `[${cek.error.issues
|
||||
.map((v) => `${v.path.join(".")}`)
|
||||
.join("\n")}] required`;
|
||||
toast.error(err);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
peminjamanBuku.update.loading = true;
|
||||
|
||||
const response = await fetch(
|
||||
`/api/pendidikan/perpustakaandigital/peminjamanbuku/${this.id}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
nama: this.form.nama,
|
||||
noTelp: this.form.noTelp,
|
||||
alamat: this.form.alamat,
|
||||
bukuId: this.form.bukuId,
|
||||
tanggalPinjam: this.form.tanggalPinjam,
|
||||
batasKembali: this.form.batasKembali,
|
||||
tanggalKembali: this.form.tanggalKembali,
|
||||
catatan: this.form.catatan,
|
||||
status: this.form.status
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(
|
||||
errorData.message || `HTTP error! status: ${response.status}`
|
||||
);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
toast.success("Berhasil update data peminjaman buku");
|
||||
await peminjamanBuku.findMany.load(); // refresh list
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(result.message || "Gagal update data peminjaman buku");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating data peminjaman buku:", error);
|
||||
toast.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Terjadi kesalahan saat update data peminjaman buku"
|
||||
);
|
||||
return false;
|
||||
} finally {
|
||||
peminjamanBuku.update.loading = false;
|
||||
}
|
||||
},
|
||||
reset() {
|
||||
peminjamanBuku.update.id = "";
|
||||
peminjamanBuku.update.form = { ...editForm };
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const perpustakaanDigitalState = proxy({
|
||||
dataPerpustakaan,
|
||||
kategoriBuku,
|
||||
peminjamanBuku,
|
||||
});
|
||||
|
||||
export default perpustakaanDigitalState;
|
||||
|
||||
@@ -561,6 +561,45 @@ const pegawai = proxy({
|
||||
}
|
||||
},
|
||||
},
|
||||
findManyAll: {
|
||||
data: null as
|
||||
| Prisma.PegawaiPPIDGetPayload<{
|
||||
include: {
|
||||
image: true;
|
||||
posisi: true;
|
||||
};
|
||||
}>[]
|
||||
| null,
|
||||
loading: false,
|
||||
search: "",
|
||||
load: async (search = "") => {
|
||||
// Change to arrow function
|
||||
pegawai.findManyAll.loading = true; // Use the full path to access the property
|
||||
pegawai.findManyAll.search = search;
|
||||
try {
|
||||
const query: any = { search };
|
||||
if (search) query.search = search;
|
||||
|
||||
const res = await ApiFetch.api.ppid.strukturppid.pegawai[
|
||||
"find-many-all"
|
||||
].get({
|
||||
query,
|
||||
});
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
pegawai.findManyAll.data = res.data.data || [];
|
||||
} else {
|
||||
console.error("Failed to load pegawai:", res.data?.message);
|
||||
pegawai.findManyAll.data = [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading pegawai:", error);
|
||||
pegawai.findManyAll.data = [];
|
||||
} finally {
|
||||
pegawai.findManyAll.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
findUnique: {
|
||||
data: null as
|
||||
| (Prisma.PegawaiPPIDGetPayload<{
|
||||
|
||||
@@ -27,7 +27,7 @@ function PelayananPendudukNonPermanent() {
|
||||
);
|
||||
|
||||
useShallowEffect(() => {
|
||||
pelayananPendudukNonPermanen.findById.load('1');
|
||||
pelayananPendudukNonPermanen.findById.load('edit');
|
||||
}, []);
|
||||
|
||||
if (!pelayananPendudukNonPermanen.findById.data) {
|
||||
|
||||
@@ -43,7 +43,7 @@ function PerizinanBerusaha() {
|
||||
try {
|
||||
setLoading(true);
|
||||
// You should get the ID from your router query or params
|
||||
const id = '1'; // Replace with actual ID or get from URL params
|
||||
const id = 'edit'; // Replace with actual ID or get from URL params
|
||||
await pelayananPerizinanBerusaha.findById.load(id);
|
||||
} catch (err) {
|
||||
setError('Gagal memuat data');
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Group,
|
||||
Pagination,
|
||||
Paper,
|
||||
Skeleton,
|
||||
@@ -16,11 +15,10 @@ import {
|
||||
TableThead,
|
||||
TableTr,
|
||||
Text,
|
||||
Title,
|
||||
Tooltip,
|
||||
Title
|
||||
} from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconDeviceImac, IconSearch, IconPlus } from '@tabler/icons-react';
|
||||
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
@@ -72,20 +70,7 @@ function ListAjukanIdeInovatif({ search }: { search: string }) {
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={4}>Daftar Ide Inovatif</Title>
|
||||
<Tooltip label="Ajukan Ide Baru" withArrow>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/inovasi/ajukan-ide-inovatif/create')}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
|
||||
<Title order={4}>Daftar Ide Inovatif</Title>
|
||||
<Box style={{ overflowX: "auto" }}>
|
||||
<Table highlightOnHover>
|
||||
<TableThead>
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
'use client'
|
||||
'use client';
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
||||
import desaDigitalState from '@/app/admin/(dashboard)/_state/inovasi/desa-digital';
|
||||
import colors from '@/con/colors';
|
||||
import ApiFetch from '@/lib/api-fetch';
|
||||
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Image,
|
||||
Paper,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { Dropzone } from '@mantine/dropzone';
|
||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
@@ -20,16 +31,14 @@ function EditDigitalSmartVillage() {
|
||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
|
||||
// ✅ hanya lokal state untuk form
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
deskripsi: '',
|
||||
imageId: '',
|
||||
});
|
||||
|
||||
// load data sekali saat mount
|
||||
useEffect(() => {
|
||||
const loadPenghargaan = async () => {
|
||||
const loadData = async () => {
|
||||
const id = params?.id as string;
|
||||
if (!id) return;
|
||||
|
||||
@@ -42,69 +51,67 @@ function EditDigitalSmartVillage() {
|
||||
imageId: data.imageId || '',
|
||||
});
|
||||
|
||||
if (data?.image?.link) {
|
||||
setPreviewImage(data.image.link);
|
||||
}
|
||||
if (data?.image?.link) setPreviewImage(data.image.link);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading desa digital smart village:", error);
|
||||
toast.error("Gagal memuat data desa digital smart village");
|
||||
console.error('Error loading data:', error);
|
||||
toast.error('Gagal memuat data desa digital smart village');
|
||||
}
|
||||
};
|
||||
|
||||
loadPenghargaan();
|
||||
loadData();
|
||||
}, [params?.id]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
// ✅ update global state hanya saat submit
|
||||
stateDesaDigital.edit.form = {
|
||||
...stateDesaDigital.edit.form,
|
||||
...formData,
|
||||
};
|
||||
stateDesaDigital.edit.form = { ...stateDesaDigital.edit.form, ...formData };
|
||||
|
||||
if (file) {
|
||||
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
|
||||
const uploaded = res.data?.data;
|
||||
|
||||
if (!uploaded?.id) {
|
||||
return toast.error("Gagal upload gambar");
|
||||
}
|
||||
if (!uploaded?.id) return toast.error('Gagal upload gambar');
|
||||
|
||||
stateDesaDigital.edit.form.imageId = uploaded.id;
|
||||
}
|
||||
|
||||
await stateDesaDigital.edit.update();
|
||||
toast.success("Desa digital smart village berhasil diperbarui!");
|
||||
router.push("/admin/inovasi/desa-digital-smart-village");
|
||||
toast.success('Desa digital smart village berhasil diperbarui!');
|
||||
router.push('/admin/inovasi/desa-digital-smart-village');
|
||||
} catch (error) {
|
||||
console.error("Error updating desa digital smart village:", error);
|
||||
toast.error("Terjadi kesalahan saat memperbarui desa digital smart village");
|
||||
console.error('Error updating desa digital:', error);
|
||||
toast.error('Terjadi kesalahan saat memperbarui data');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button variant="subtle" onClick={() => router.back()}>
|
||||
<IconArrowBack color={colors["blue-button"]} size={30} />
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
|
||||
<Stack gap={"xs"}>
|
||||
<Title order={3}>Edit Desa Digital Smart Village</Title>
|
||||
|
||||
{/* ✅ controlled input */}
|
||||
<TextInput
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
|
||||
placeholder="masukkan judul"
|
||||
/>
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
{/* Header */}
|
||||
<Group mb="md">
|
||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Edit Desa Digital Smart Village
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
{/* Form Card */}
|
||||
<Paper
|
||||
w={{ base: '100%', md: '55%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
{/* Dropzone Upload */}
|
||||
<Box>
|
||||
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
Gambar Desa Digital
|
||||
</Text>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const selectedFile = files[0];
|
||||
@@ -113,43 +120,43 @@ function EditDigitalSmartVillage() {
|
||||
setPreviewImage(URL.createObjectURL(selectedFile));
|
||||
}
|
||||
}}
|
||||
onReject={() => toast.error('File tidak valid.')}
|
||||
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
||||
maxSize={5 * 1024 ** 2}
|
||||
accept={{ 'image/*': [] }}
|
||||
radius="md"
|
||||
p="xl"
|
||||
>
|
||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
||||
<Group justify="center" gap="xl" mih={180}>
|
||||
<Dropzone.Accept>
|
||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||
<IconX size={48} color="red" stroke={1.5} />
|
||||
</Dropzone.Reject>
|
||||
<Dropzone.Idle>
|
||||
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||
<IconPhoto size={48} color="#868e96" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
|
||||
<div>
|
||||
<Text size="xl" inline>
|
||||
Drag gambar ke sini atau klik untuk pilih file
|
||||
<Stack gap="xs" align="center">
|
||||
<Text size="md" fw={500}>
|
||||
Seret gambar atau klik untuk memilih file
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" inline mt={7}>
|
||||
Maksimal 5MB dan harus format gambar
|
||||
<Text size="sm" c="dimmed">
|
||||
Maksimal 5MB, format gambar wajib
|
||||
</Text>
|
||||
</div>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
|
||||
{previewImage && (
|
||||
<Box mt="sm">
|
||||
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<Image
|
||||
src={previewImage}
|
||||
alt="Preview"
|
||||
alt="Preview Gambar"
|
||||
radius="md"
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
maxHeight: '200px',
|
||||
maxHeight: 220,
|
||||
objectFit: 'contain',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #ddd',
|
||||
border: `1px solid ${colors['blue-button']}`,
|
||||
}}
|
||||
loading="lazy"
|
||||
/>
|
||||
@@ -157,18 +164,43 @@ function EditDigitalSmartVillage() {
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Input Judul */}
|
||||
<TextInput
|
||||
label="Judul"
|
||||
placeholder="Masukkan judul inovasi"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
required
|
||||
/>
|
||||
|
||||
{/* Editor Deskripsi */}
|
||||
<Box>
|
||||
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
|
||||
{/* ✅ controlled editor */}
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
Deskripsi
|
||||
</Text>
|
||||
<EditEditor
|
||||
value={formData.deskripsi}
|
||||
onChange={(htmlContent) => {
|
||||
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }));
|
||||
}}
|
||||
onChange={(htmlContent) =>
|
||||
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }))
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Button onClick={handleSubmit}>Simpan</Button>
|
||||
{/* Tombol Simpan */}
|
||||
<Group justify="right">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
size="md"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||
}}
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Image,
|
||||
Paper,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Text,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
|
||||
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
@@ -10,95 +20,136 @@ import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||
import desaDigitalState from '../../../_state/inovasi/desa-digital';
|
||||
|
||||
function DetailDesaDigital() {
|
||||
const stateDesaDigital = useProxy(desaDigitalState)
|
||||
const stateDesaDigital = useProxy(desaDigitalState);
|
||||
const [modalHapus, setModalHapus] = useState(false);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const router = useRouter()
|
||||
const params = useParams()
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
|
||||
useShallowEffect(() => {
|
||||
stateDesaDigital.findUnique.load(params?.id as string)
|
||||
}, [])
|
||||
stateDesaDigital.findUnique.load(params?.id as string);
|
||||
}, []);
|
||||
|
||||
const handleHapus = () => {
|
||||
if (selectedId) {
|
||||
stateDesaDigital.delete.byId(selectedId)
|
||||
setModalHapus(false)
|
||||
setSelectedId(null)
|
||||
router.push("/admin/inovasi/desa-digital-smart-village")
|
||||
stateDesaDigital.delete.byId(selectedId);
|
||||
setModalHapus(false);
|
||||
setSelectedId(null);
|
||||
router.push("/admin/inovasi/desa-digital-smart-village");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!stateDesaDigital.findUnique.data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton h={500} />
|
||||
<Skeleton height={500} radius="md" />
|
||||
</Stack>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const data = stateDesaDigital.findUnique.data;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button variant="subtle" onClick={() => router.back()}>
|
||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||
</Button>
|
||||
</Box>
|
||||
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
|
||||
<Stack>
|
||||
<Text fz={"xl"} fw={"bold"}>Detail Desa Digital Smart Village</Text>
|
||||
{stateDesaDigital.findUnique.data ? (
|
||||
<Paper key={stateDesaDigital.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
||||
<Stack gap={"xs"}>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Judul</Text>
|
||||
<Text fz={"lg"}>{stateDesaDigital.findUnique.data?.name}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
|
||||
<Text fz={"lg"} style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: stateDesaDigital.findUnique.data?.deskripsi }} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
|
||||
<Image w={{ base: 150, md: 150, lg: 150 }} src={stateDesaDigital.findUnique.data?.image?.link} alt="gambar" loading="lazy"/>
|
||||
</Box>
|
||||
<Flex gap={"xs"} mt={10}>
|
||||
<Box py={10}>
|
||||
{/* Tombol Kembali */}
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
|
||||
mb={15}
|
||||
>
|
||||
Kembali
|
||||
</Button>
|
||||
|
||||
{/* Card Utama */}
|
||||
<Paper
|
||||
withBorder
|
||||
w={{ base: "100%", md: "60%" }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
|
||||
Detail Desa Digital Smart Village
|
||||
</Text>
|
||||
|
||||
{/* Sub Card Detail */}
|
||||
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
|
||||
<Stack gap="sm">
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Judul</Text>
|
||||
<Text fz="md" c="dimmed">{data?.name || '-'}</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Deskripsi</Text>
|
||||
<Text
|
||||
fz="md"
|
||||
c="dimmed"
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
dangerouslySetInnerHTML={{ __html: data?.deskripsi || '-' }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Gambar</Text>
|
||||
{data?.image?.link ? (
|
||||
<Image
|
||||
src={data.image.link}
|
||||
alt={data.name || 'Gambar Desa Digital'}
|
||||
w={200}
|
||||
h={200}
|
||||
radius="md"
|
||||
fit="cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
) : (
|
||||
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Tombol Aksi */}
|
||||
<Group gap="sm">
|
||||
<Tooltip label="Hapus Data" withArrow position="top">
|
||||
<Button
|
||||
color="red"
|
||||
onClick={() => {
|
||||
if (stateDesaDigital.findUnique.data) {
|
||||
setSelectedId(stateDesaDigital.findUnique.data.id);
|
||||
setModalHapus(true);
|
||||
}
|
||||
setSelectedId(data.id);
|
||||
setModalHapus(true);
|
||||
}}
|
||||
disabled={stateDesaDigital.delete.loading || !stateDesaDigital.findUnique.data}
|
||||
color={"red"}
|
||||
variant="light"
|
||||
radius="md"
|
||||
size="md"
|
||||
>
|
||||
<IconX size={20} />
|
||||
<IconTrash size={20} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label="Edit Data" withArrow position="top">
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (stateDesaDigital.findUnique.data) {
|
||||
router.push(`/admin/inovasi/desa-digital-smart-village/${stateDesaDigital.findUnique.data.id}/edit`);
|
||||
}
|
||||
}}
|
||||
disabled={!stateDesaDigital.findUnique.data}
|
||||
color={"green"}
|
||||
color="green"
|
||||
onClick={() => router.push(`/admin/inovasi/desa-digital-smart-village/${data.id}/edit`)}
|
||||
variant="light"
|
||||
radius="md"
|
||||
size="md"
|
||||
>
|
||||
<IconEdit size={20} />
|
||||
</Button>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</Paper>
|
||||
) : null}
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Modal Konfirmasi Hapus */}
|
||||
{/* Modal Konfirmasi */}
|
||||
<ModalKonfirmasiHapus
|
||||
opened={modalHapus}
|
||||
onClose={() => setModalHapus(false)}
|
||||
onConfirm={handleHapus}
|
||||
text='Apakah anda yakin ingin menghapus desa digital smart village ini?'
|
||||
text="Apakah Anda yakin ingin menghapus desa digital smart village ini?"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -22,7 +22,7 @@ import CreateEditor from '../../../_com/createEditor';
|
||||
import desaDigitalState from '../../../_state/inovasi/desa-digital';
|
||||
import { Dropzone } from '@mantine/dropzone';
|
||||
|
||||
function CreateDesaDigital() {
|
||||
export default function CreateDesaDigital() {
|
||||
const stateDesaDigital = useProxy(desaDigitalState);
|
||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
@@ -44,7 +44,6 @@ function CreateDesaDigital() {
|
||||
}
|
||||
|
||||
try {
|
||||
// Upload gambar dulu
|
||||
const uploadRes = await ApiFetch.api.fileStorage.create.post({
|
||||
file,
|
||||
name: file.name,
|
||||
@@ -55,10 +54,8 @@ function CreateDesaDigital() {
|
||||
return toast.error('Gagal mengunggah gambar');
|
||||
}
|
||||
|
||||
// Set imageId ke form
|
||||
stateDesaDigital.create.form.imageId = uploaded.id;
|
||||
|
||||
// Submit form
|
||||
const success = await stateDesaDigital.create.create();
|
||||
if (success) {
|
||||
resetForm();
|
||||
@@ -72,10 +69,16 @@ function CreateDesaDigital() {
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
{/* Header */}
|
||||
<Group mb="md">
|
||||
{/* Header dengan tombol kembali */}
|
||||
<Group mb="md" align="center">
|
||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
p="xs"
|
||||
radius="md"
|
||||
style={{ transition: 'background 0.2s ease' }}
|
||||
>
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
@@ -84,28 +87,32 @@ function CreateDesaDigital() {
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
{/* Card */}
|
||||
{/* Card Form */}
|
||||
<Paper
|
||||
w={{ base: '100%', md: '60%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
p={{ base: 'md', md: 'xl' }}
|
||||
radius="lg"
|
||||
shadow="md"
|
||||
style={{
|
||||
border: '1px solid #eaeaea',
|
||||
transition: 'box-shadow 0.3s ease',
|
||||
}}
|
||||
>
|
||||
<Stack gap="md">
|
||||
{/* Nama */}
|
||||
<Stack gap="lg">
|
||||
{/* Input Nama */}
|
||||
<TextInput
|
||||
label="Nama Desa Digital Smart Village"
|
||||
placeholder="Masukkan nama desa digital smart village"
|
||||
label="Nama Desa Digital"
|
||||
placeholder="Masukkan nama desa digital"
|
||||
defaultValue={stateDesaDigital.create.form.name}
|
||||
onChange={(e) => (stateDesaDigital.create.form.name = e.target.value)}
|
||||
required
|
||||
radius="md"
|
||||
withAsterisk
|
||||
/>
|
||||
|
||||
{/* Deskripsi */}
|
||||
<Box>
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
<Text fw={500} fz="sm" mb={6}>
|
||||
Deskripsi
|
||||
</Text>
|
||||
<CreateEditor
|
||||
@@ -118,8 +125,8 @@ function CreateDesaDigital() {
|
||||
|
||||
{/* Upload Gambar */}
|
||||
<Box>
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
Gambar
|
||||
<Text fw={500} fz="sm" mb={6}>
|
||||
Gambar Desa Digital
|
||||
</Text>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
@@ -134,6 +141,11 @@ function CreateDesaDigital() {
|
||||
accept={{ 'image/*': [] }}
|
||||
radius="md"
|
||||
p="xl"
|
||||
style={{
|
||||
border: '2px dashed #cfd8dc',
|
||||
backgroundColor: '#fafafa',
|
||||
transition: 'background-color 0.2s ease, border-color 0.2s ease',
|
||||
}}
|
||||
>
|
||||
<Group justify="center" gap="xl" mih={180}>
|
||||
<Dropzone.Accept>
|
||||
@@ -153,15 +165,22 @@ function CreateDesaDigital() {
|
||||
|
||||
{/* Preview */}
|
||||
{previewImage && (
|
||||
<Box mt="sm" style={{ textAlign: 'center' }}>
|
||||
<Box
|
||||
mt="sm"
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={previewImage}
|
||||
alt="Preview"
|
||||
radius="md"
|
||||
style={{
|
||||
maxHeight: 200,
|
||||
objectFit: 'contain',
|
||||
border: '1px solid #ddd',
|
||||
maxHeight: 220,
|
||||
objectFit: 'cover',
|
||||
border: '1px solid #e0e0e0',
|
||||
}}
|
||||
loading="lazy"
|
||||
/>
|
||||
@@ -170,7 +189,7 @@ function CreateDesaDigital() {
|
||||
</Box>
|
||||
|
||||
{/* Tombol Submit */}
|
||||
<Group justify="right">
|
||||
<Group justify="flex-end" mt="sm">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
@@ -179,6 +198,17 @@ function CreateDesaDigital() {
|
||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||
transition: 'transform 0.2s ease, box-shadow 0.2s ease',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
(e.currentTarget.style.transform = 'translateY(-2px)');
|
||||
(e.currentTarget.style.boxShadow =
|
||||
'0 6px 20px rgba(79, 172, 254, 0.5)');
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
(e.currentTarget.style.transform = 'translateY(0)');
|
||||
(e.currentTarget.style.boxShadow =
|
||||
'0 4px 15px rgba(79, 172, 254, 0.4)');
|
||||
}}
|
||||
>
|
||||
Simpan
|
||||
@@ -189,5 +219,3 @@ function CreateDesaDigital() {
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateDesaDigital;
|
||||
|
||||
@@ -55,7 +55,7 @@ function DetailInfoTeknologiTepatGuna() {
|
||||
<Paper
|
||||
withBorder
|
||||
w={{ base: "100%", md: "70%", lg: "60%" }}
|
||||
bg={colors['white-1']}
|
||||
bg="#ECEEF8"
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
'use client'
|
||||
'use client';
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
||||
import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Paper,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
@@ -15,13 +25,11 @@ function EditJenisLayanan() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
|
||||
// state lokal untuk form
|
||||
const [formData, setFormData] = useState({
|
||||
nama: "",
|
||||
deskripsi: "",
|
||||
nama: '',
|
||||
deskripsi: '',
|
||||
});
|
||||
|
||||
// load data dari backend ke local state
|
||||
useEffect(() => {
|
||||
const loadJenisLayanan = async () => {
|
||||
const id = params?.id as string;
|
||||
@@ -31,20 +39,19 @@ function EditJenisLayanan() {
|
||||
const data = await state.edit.load(id);
|
||||
if (data) {
|
||||
setFormData({
|
||||
nama: data.nama ?? "",
|
||||
deskripsi: data.deskripsi ?? "",
|
||||
nama: data.nama ?? '',
|
||||
deskripsi: data.deskripsi ?? '',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading jenis layanan:", error);
|
||||
toast.error("Gagal memuat data jenis layanan");
|
||||
console.error('Error loading jenis layanan:', error);
|
||||
toast.error('Gagal memuat data jenis layanan');
|
||||
}
|
||||
};
|
||||
|
||||
loadJenisLayanan();
|
||||
}, [params?.id]);
|
||||
|
||||
// submit update → baru sync ke global state
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
state.edit.form = {
|
||||
@@ -53,48 +60,85 @@ function EditJenisLayanan() {
|
||||
};
|
||||
|
||||
await state.edit.update();
|
||||
toast.success("Jenis layanan berhasil diperbarui!");
|
||||
router.push("/admin/inovasi/layanan-online-desa/jenis-layanan");
|
||||
toast.success('Jenis layanan berhasil diperbarui!');
|
||||
router.push('/admin/inovasi/layanan-online-desa/jenis-layanan');
|
||||
} catch (error) {
|
||||
console.error("Error updating jenis layanan:", error);
|
||||
toast.error("Terjadi kesalahan saat memperbarui jenis layanan");
|
||||
console.error('Error updating jenis layanan:', error);
|
||||
toast.error('Terjadi kesalahan saat memperbarui jenis layanan');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button variant="subtle" onClick={() => router.back()}>
|
||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Paper bg={colors["white-1"]} p="md" w={{ base: "100%", md: "50%" }}>
|
||||
<Stack gap="xs">
|
||||
<Title order={3}>Edit Jenis Layanan</Title>
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
{/* Header */}
|
||||
<Group mb="md">
|
||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
p="xs"
|
||||
radius="md"
|
||||
>
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Edit Jenis Layanan
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
{/* Form Container */}
|
||||
<Paper
|
||||
w={{ base: '100%', md: '60%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
{/* Input: Nama Jenis Layanan */}
|
||||
<TextInput
|
||||
label="Nama Jenis Layanan"
|
||||
placeholder="Masukkan nama jenis layanan"
|
||||
value={formData.nama}
|
||||
onChange={(e) =>
|
||||
setFormData((prev) => ({ ...prev, nama: e.target.value }))
|
||||
}
|
||||
label={<Text fz="sm" fw="bold">Nama Jenis Layanan</Text>}
|
||||
placeholder="masukkan nama jenis layanan"
|
||||
required
|
||||
/>
|
||||
|
||||
{/* Input: Deskripsi (Rich Text Editor) */}
|
||||
<Box>
|
||||
<Text fz="sm" fw="bold">Deskripsi</Text>
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
Deskripsi
|
||||
</Text>
|
||||
<EditEditor
|
||||
value={formData.deskripsi}
|
||||
onChange={(htmlContent) =>
|
||||
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }))
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
deskripsi: htmlContent,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>
|
||||
Simpan
|
||||
</Button>
|
||||
{/* Tombol Submit */}
|
||||
<Group justify="right" mt="sm">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
size="md"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||
}}
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
@@ -89,26 +89,26 @@ function ListArtikelKesehatan({ search }: { search: string }) {
|
||||
<Table highlightOnHover>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>Judul</TableTh>
|
||||
<TableTh>Konten</TableTh>
|
||||
<TableTh>Aksi</TableTh>
|
||||
<TableTh style={{ minWidth: 200 }}>Judul</TableTh>
|
||||
<TableTh style={{ minWidth: 200 }}>Konten</TableTh>
|
||||
<TableTh style={{ minWidth: 200 }}>Aksi</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>
|
||||
<TableTd style={{ minWidth: 200 }}>
|
||||
<Text fw={500} truncate="end" lineClamp={1}>
|
||||
{item.title}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<TableTd style={{ minWidth: 200 }} >
|
||||
<Text truncate fz="sm" c="dimmed" lineClamp={1}>
|
||||
{item.content}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<TableTd style={{ minWidth: 200 }}>
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
|
||||
@@ -223,7 +223,7 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
|
||||
|
||||
{/* Chart */}
|
||||
<Box mt="lg" style={{ width: '100%', minWidth: 300, height: 420, minHeight: 300 }}>
|
||||
<Paper bg={colors['white-1']} p={'md'}>
|
||||
<Paper withBorder bg={colors['white-1']} p={'md'}>
|
||||
<Title pb={10} order={4}>Grafik Hasil Kepuasan Masyarakat</Title>
|
||||
{mounted && diseaseChartData.length > 0 ? (
|
||||
<Center>
|
||||
|
||||
@@ -111,9 +111,7 @@ function ListInfoWabahPenyakit({ search }: { search: string }) {
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Box w={200}>
|
||||
<Text truncate fz="sm" c="dimmed">
|
||||
{item.deskripsiSingkat}
|
||||
</Text>
|
||||
<Text truncate="end" fz="sm" c="dimmed" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat }} />
|
||||
</Box>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
|
||||
@@ -66,7 +66,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
|
||||
Profil Desa
|
||||
Profile Desa
|
||||
</Title>
|
||||
|
||||
<Tabs
|
||||
|
||||
@@ -150,13 +150,13 @@ export default function EditKegiatanDesa() {
|
||||
onChange={(e) => setFormData({ ...formData, judul: e.target.value })}
|
||||
required
|
||||
/>
|
||||
<TextInput
|
||||
value={formData.deskripsiSingkat}
|
||||
label={<Text fz="sm" fw="bold">Deskripsi Singkat Kegiatan Desa</Text>}
|
||||
placeholder="masukkan deskripsi singkat kegiatan desa"
|
||||
onChange={(e) => setFormData({ ...formData, deskripsiSingkat: e.target.value })}
|
||||
required
|
||||
/>
|
||||
<Box>
|
||||
<Text fw="bold" fz="sm">Deskripsi Singkat Kegiatan Desa</Text>
|
||||
<EditEditor
|
||||
value={formData.deskripsiSingkat}
|
||||
onChange={(htmlContent) => setFormData(prev => ({ ...prev, deskripsiSingkat: htmlContent }))}
|
||||
/>
|
||||
</Box>
|
||||
<Select
|
||||
label="Kategori Kegiatan"
|
||||
data={gotongRoyongState.kategoriKegiatan.findMany.data?.map(k => ({
|
||||
|
||||
@@ -85,7 +85,7 @@ function DetailKegiatanDesa() {
|
||||
{/* Deskripsi Singkat */}
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Deskripsi Singkat</Text>
|
||||
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }}>{data.deskripsiSingkat || '-'}</Text>
|
||||
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.deskripsiSingkat || '-' }} />
|
||||
</Box>
|
||||
|
||||
{/* Deskripsi Lengkap */}
|
||||
|
||||
@@ -159,13 +159,15 @@ function CreateKegiatanDesa() {
|
||||
onChange={(e) => (stateKegiatanDesa.create.form.judul = e.target.value)}
|
||||
required
|
||||
/>
|
||||
<TextInput
|
||||
label="Deskripsi Singkat"
|
||||
placeholder="Masukkan deskripsi singkat"
|
||||
defaultValue={stateKegiatanDesa.create.form.deskripsiSingkat}
|
||||
onChange={(e) => (stateKegiatanDesa.create.form.deskripsiSingkat = e.target.value)}
|
||||
required
|
||||
/>
|
||||
<Box>
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
Deskripsi Singkat
|
||||
</Text>
|
||||
<CreateEditor
|
||||
value={stateKegiatanDesa.create.form.deskripsiSingkat}
|
||||
onChange={(val) => (stateKegiatanDesa.create.form.deskripsiSingkat = val)}
|
||||
/>
|
||||
</Box>
|
||||
<TextInput
|
||||
type="number"
|
||||
min={0}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
|
||||
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { IconSchool, IconStar } from '@tabler/icons-react';
|
||||
@@ -58,36 +58,42 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
||||
radius="lg"
|
||||
keepMounted={false}
|
||||
>
|
||||
<TabsList
|
||||
p="sm"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||
borderRadius: "1rem",
|
||||
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||
<ScrollArea type="auto" offsetScrollbars>
|
||||
<TabsList
|
||||
p="sm"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||
borderRadius: "1rem",
|
||||
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||
display: "flex",
|
||||
flexWrap: "nowrap",
|
||||
gap: "0.5rem",
|
||||
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
|
||||
}}
|
||||
>
|
||||
{tabs.map((tab, i) => (
|
||||
<Tooltip
|
||||
key={i}
|
||||
label={tab.tooltip}
|
||||
position="bottom"
|
||||
withArrow
|
||||
transitionProps={{ transition: 'pop', duration: 200 }}
|
||||
>
|
||||
<TabsTab
|
||||
value={tab.value}
|
||||
leftSection={tab.icon}
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "0.9rem",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
>
|
||||
{tabs.map((tab, i) => (
|
||||
<Tooltip
|
||||
key={i}
|
||||
label={tab.tooltip}
|
||||
position="bottom"
|
||||
withArrow
|
||||
transitionProps={{ transition: 'pop', duration: 200 }}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTab>
|
||||
</Tooltip>
|
||||
))}
|
||||
</TabsList>
|
||||
<TabsTab
|
||||
value={tab.value}
|
||||
leftSection={tab.icon}
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "0.9rem",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTab>
|
||||
</Tooltip>
|
||||
))}
|
||||
</TabsList>
|
||||
</ScrollArea>
|
||||
|
||||
{tabs.map((tab, i) => (
|
||||
<TabsPanel
|
||||
|
||||
@@ -99,22 +99,22 @@ function ListKeunggulanProgram({ search }: { search: string }) {
|
||||
<Table highlightOnHover>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh style={{ width: '30%' }}>Nama Keunggulan Program</TableTh>
|
||||
<TableTh style={{ width: '35%' }}>Deskripsi</TableTh>
|
||||
<TableTh style={{ width: '15%' }}>Edit</TableTh>
|
||||
<TableTh style={{ width: '15%' }}>Delete</TableTh>
|
||||
<TableTh style={{ minWidth: 200 }}>Nama Keunggulan Program</TableTh>
|
||||
<TableTh style={{ minWidth: 200 }}>Deskripsi</TableTh>
|
||||
<TableTh style={{ minWidth: 200 }}>Edit</TableTh>
|
||||
<TableTh style={{ minWidth: 200 }}>Delete</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>
|
||||
<TableTd style={{ minWidth: 200 }}>
|
||||
<Text fw={500} truncate="end" lineClamp={1}>
|
||||
{item.judul}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<TableTd style={{ minWidth: 200 }}>
|
||||
<Text
|
||||
fw={500}
|
||||
truncate="end"
|
||||
@@ -122,7 +122,7 @@ function ListKeunggulanProgram({ search }: { search: string }) {
|
||||
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
|
||||
/>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<TableTd style={{ minWidth: 200 }}>
|
||||
<Tooltip label="Edit" withArrow>
|
||||
<Button
|
||||
variant="light"
|
||||
@@ -138,7 +138,7 @@ function ListKeunggulanProgram({ search }: { search: string }) {
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<TableTd style={{ minWidth: 200 }}>
|
||||
<Tooltip label="Hapus" withArrow>
|
||||
<Button
|
||||
variant="light"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
|
||||
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { IconSchool, IconCalendar, IconBuildingCommunity } from '@tabler/icons-react';
|
||||
@@ -65,36 +65,42 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
||||
radius="lg"
|
||||
keepMounted={false}
|
||||
>
|
||||
<TabsList
|
||||
p="sm"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||
borderRadius: "1rem",
|
||||
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||
<ScrollArea type="auto" offsetScrollbars>
|
||||
<TabsList
|
||||
p="sm"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||
borderRadius: "1rem",
|
||||
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||
display: "flex",
|
||||
flexWrap: "nowrap",
|
||||
gap: "0.5rem",
|
||||
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
|
||||
}}
|
||||
>
|
||||
{tabs.map((tab, i) => (
|
||||
<Tooltip
|
||||
key={i}
|
||||
label={tab.tooltip}
|
||||
position="bottom"
|
||||
withArrow
|
||||
transitionProps={{ transition: 'pop', duration: 200 }}
|
||||
>
|
||||
<TabsTab
|
||||
value={tab.value}
|
||||
leftSection={tab.icon}
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "0.9rem",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
>
|
||||
{tabs.map((tab, i) => (
|
||||
<Tooltip
|
||||
key={i}
|
||||
label={tab.tooltip}
|
||||
position="bottom"
|
||||
withArrow
|
||||
transitionProps={{ transition: 'pop', duration: 200 }}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTab>
|
||||
</Tooltip>
|
||||
))}
|
||||
</TabsList>
|
||||
<TabsTab
|
||||
value={tab.value}
|
||||
leftSection={tab.icon}
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "0.9rem",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTab>
|
||||
</Tooltip>
|
||||
))}
|
||||
</TabsList>
|
||||
</ScrollArea>
|
||||
|
||||
{tabs.map((tab, i) => (
|
||||
<TabsPanel
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
|
||||
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
|
||||
import { IconBuilding, IconChalkboard, IconMicroscope, IconSchool } from '@tabler/icons-react';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
@@ -72,30 +72,36 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
||||
radius="lg"
|
||||
keepMounted={false}
|
||||
>
|
||||
<TabsList
|
||||
p="sm"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||
borderRadius: "1rem",
|
||||
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||
<ScrollArea type="auto" offsetScrollbars>
|
||||
<TabsList
|
||||
p="sm"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||
borderRadius: "1rem",
|
||||
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||
display: "flex",
|
||||
flexWrap: "nowrap",
|
||||
gap: "0.5rem",
|
||||
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
|
||||
}}
|
||||
>
|
||||
{tabs.map((tab, i) => (
|
||||
<Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}>
|
||||
<TabsTab
|
||||
value={tab.value}
|
||||
leftSection={tab.icon}
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "0.9rem",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTab>
|
||||
</Tooltip>
|
||||
))}
|
||||
</TabsList>
|
||||
>
|
||||
{tabs.map((tab, i) => (
|
||||
<Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}>
|
||||
<TabsTab
|
||||
value={tab.value}
|
||||
leftSection={tab.icon}
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "0.9rem",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTab>
|
||||
</Tooltip>
|
||||
))}
|
||||
</TabsList>
|
||||
</ScrollArea>
|
||||
|
||||
{tabs.map((tab, i) => (
|
||||
<TabsPanel
|
||||
@@ -106,6 +112,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
||||
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
|
||||
borderRadius: "1rem",
|
||||
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
|
||||
minHeight: "60vh",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
@@ -121,4 +128,3 @@ export default LayoutTabs;
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
|
||||
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { IconSchool, IconMapPin, IconBook2 } from '@tabler/icons-react';
|
||||
@@ -66,36 +66,42 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
||||
radius="lg"
|
||||
keepMounted={false}
|
||||
>
|
||||
<TabsList
|
||||
p="sm"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||
borderRadius: "1rem",
|
||||
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||
<ScrollArea type="auto" offsetScrollbars>
|
||||
<TabsList
|
||||
p="sm"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||
borderRadius: "1rem",
|
||||
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||
display: "flex",
|
||||
flexWrap: "nowrap",
|
||||
gap: "0.5rem",
|
||||
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
|
||||
}}
|
||||
>
|
||||
{tabs.map((tab, i) => (
|
||||
<Tooltip
|
||||
key={i}
|
||||
label={tab.tooltip}
|
||||
position="bottom"
|
||||
withArrow
|
||||
transitionProps={{ transition: 'pop', duration: 200 }}
|
||||
>
|
||||
<TabsTab
|
||||
value={tab.value}
|
||||
leftSection={tab.icon}
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "0.9rem",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
>
|
||||
{tabs.map((tab, i) => (
|
||||
<Tooltip
|
||||
key={i}
|
||||
label={tab.tooltip}
|
||||
position="bottom"
|
||||
withArrow
|
||||
transitionProps={{ transition: 'pop', duration: 200 }}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTab>
|
||||
</Tooltip>
|
||||
))}
|
||||
</TabsList>
|
||||
<TabsTab
|
||||
value={tab.value}
|
||||
leftSection={tab.icon}
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "0.9rem",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTab>
|
||||
</Tooltip>
|
||||
))}
|
||||
</TabsList>
|
||||
</ScrollArea>
|
||||
|
||||
{tabs.map((tab, i) => (
|
||||
<TabsPanel
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
|
||||
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { IconBook2, IconCategory } from '@tabler/icons-react';
|
||||
import { IconBook2, IconCategory, IconUser } from '@tabler/icons-react';
|
||||
|
||||
function LayoutTabs({ children }: { children: React.ReactNode }) {
|
||||
const router = useRouter();
|
||||
@@ -25,6 +25,13 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
||||
icon: <IconCategory size={18} stroke={1.8} />,
|
||||
tooltip: "Atur kategori untuk buku digital",
|
||||
},
|
||||
{
|
||||
label: "Peminjam",
|
||||
value: "peminjam",
|
||||
href: "/admin/pendidikan/perpustakaan-digital/peminjam",
|
||||
icon: <IconUser size={18} stroke={1.8} />,
|
||||
tooltip: "Data Peminjam Buku",
|
||||
},
|
||||
];
|
||||
|
||||
const currentTab = tabs.find(tab => tab.href === pathname);
|
||||
@@ -58,36 +65,42 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
||||
radius="lg"
|
||||
keepMounted={false}
|
||||
>
|
||||
<TabsList
|
||||
p="sm"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||
borderRadius: "1rem",
|
||||
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||
<ScrollArea type="auto" offsetScrollbars>
|
||||
<TabsList
|
||||
p="sm"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||
borderRadius: "1rem",
|
||||
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||
display: "flex",
|
||||
flexWrap: "nowrap",
|
||||
gap: "0.5rem",
|
||||
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
|
||||
}}
|
||||
>
|
||||
{tabs.map((tab, i) => (
|
||||
<Tooltip
|
||||
key={i}
|
||||
label={tab.tooltip}
|
||||
position="bottom"
|
||||
withArrow
|
||||
transitionProps={{ transition: 'pop', duration: 200 }}
|
||||
>
|
||||
<TabsTab
|
||||
value={tab.value}
|
||||
leftSection={tab.icon}
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "0.9rem",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
>
|
||||
{tabs.map((tab, i) => (
|
||||
<Tooltip
|
||||
key={i}
|
||||
label={tab.tooltip}
|
||||
position="bottom"
|
||||
withArrow
|
||||
transitionProps={{ transition: 'pop', duration: 200 }}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTab>
|
||||
</Tooltip>
|
||||
))}
|
||||
</TabsList>
|
||||
<TabsTab
|
||||
value={tab.value}
|
||||
leftSection={tab.icon}
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "0.9rem",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTab>
|
||||
</Tooltip>
|
||||
))}
|
||||
</TabsList>
|
||||
</ScrollArea>
|
||||
|
||||
{tabs.map((tab, i) => (
|
||||
<TabsPanel
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
||||
import perpustakaanDigitalState from '@/app/admin/(dashboard)/_state/pendidikan/perpustakaan-digital';
|
||||
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Paper,
|
||||
Select,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { DateInput } from '@mantine/dates';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
export type Status = "Dipinjam" | "Dikembalikan" | "Terlambat" | "Dibatalkan";
|
||||
|
||||
function EditPeminjam() {
|
||||
const stateEdit = useProxy(perpustakaanDigitalState.peminjamanBuku);
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
|
||||
const [formData, setFormData] = useState<{
|
||||
nama: string;
|
||||
noTelp: string;
|
||||
alamat: string;
|
||||
bukuId: string;
|
||||
buku?: {
|
||||
id: string;
|
||||
judul: string;
|
||||
};
|
||||
tanggalPinjam: string;
|
||||
batasKembali: string;
|
||||
tanggalKembali: string;
|
||||
status: Status;
|
||||
catatan: string;
|
||||
}>({
|
||||
nama: '',
|
||||
noTelp: '',
|
||||
alamat: '',
|
||||
bukuId: '',
|
||||
tanggalPinjam: '',
|
||||
batasKembali: '',
|
||||
tanggalKembali: '',
|
||||
status: 'Dipinjam', // Default status
|
||||
catatan: '',
|
||||
});
|
||||
|
||||
useShallowEffect(() => {
|
||||
perpustakaanDigitalState.dataPerpustakaan.findManyAll.load()
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const loadPeminjam = async () => {
|
||||
const id = params?.id as string;
|
||||
if (!id) return;
|
||||
|
||||
try {
|
||||
const data = await stateEdit.update.load(id);
|
||||
if (data) {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
nama: data.nama ?? prev.nama,
|
||||
noTelp: data.noTelp ?? prev.noTelp,
|
||||
alamat: data.alamat ?? prev.alamat,
|
||||
bukuId: data.bukuId ?? prev.bukuId,
|
||||
buku: data.buku ? {
|
||||
id: data.buku.id,
|
||||
judul: data.buku.judul
|
||||
} : undefined,
|
||||
tanggalPinjam: data.tanggalPinjam ?? prev.tanggalPinjam,
|
||||
batasKembali: data.batasKembali ?? prev.batasKembali,
|
||||
tanggalKembali: data.tanggalKembali ?? prev.tanggalKembali,
|
||||
status: (data.status as Status) ?? prev.status,
|
||||
catatan: data.catatan ?? prev.catatan,
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading peminjam:", error);
|
||||
toast.error("Gagal mengambil data peminjam");
|
||||
}
|
||||
};
|
||||
|
||||
loadPeminjam();
|
||||
}, [params?.id]);
|
||||
|
||||
|
||||
const handleChange = (field: string, value: string | Status) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
stateEdit.update.form = {
|
||||
...stateEdit.update.form,
|
||||
...formData,
|
||||
};
|
||||
|
||||
await stateEdit.update.update();
|
||||
toast.success("Peminjam berhasil diperbarui!");
|
||||
router.push("/admin/pendidikan/perpustakaan-digital/peminjam");
|
||||
} catch (error) {
|
||||
console.error("Error updating peminjam:", error);
|
||||
toast.error("Terjadi kesalahan saat memperbarui peminjam");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
{/* Header */}
|
||||
<Group mb="md">
|
||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Edit Peminjam Buku
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
{/* Card */}
|
||||
<Paper
|
||||
w={{ base: '100%', md: '60%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
value={formData.nama}
|
||||
onChange={(e) => handleChange('nama', e.target.value)}
|
||||
label={<Text fw="bold" fz="sm">Nama Peminjam</Text>}
|
||||
placeholder="Masukkan nama peminjam"
|
||||
required
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
value={formData.noTelp}
|
||||
onChange={(e) => handleChange('noTelp', e.target.value)}
|
||||
label={<Text fw="bold" fz="sm">No Telp Peminjam</Text>}
|
||||
placeholder="Masukkan no telp peminjam"
|
||||
required
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
value={formData.alamat}
|
||||
onChange={(e) => handleChange('alamat', e.target.value)}
|
||||
label={<Text fw="bold" fz="sm">Alamat Peminjam</Text>}
|
||||
placeholder="Masukkan alamat peminjam"
|
||||
required
|
||||
/>
|
||||
|
||||
<Box>
|
||||
<Text fw="bold" fz="sm" mb={6}>Buku</Text>
|
||||
<Select
|
||||
placeholder="Pilih buku"
|
||||
data={perpustakaanDigitalState.dataPerpustakaan.findManyAll.data?.map(p => ({ value: p.id, label: p.judul })) || []}
|
||||
value={formData.bukuId}
|
||||
onChange={(value) => value && setFormData({ ...formData, bukuId: value })}
|
||||
searchable
|
||||
clearable
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<DateInput
|
||||
value={formData.tanggalPinjam}
|
||||
onChange={(date) => {
|
||||
perpustakaanDigitalState.peminjamanBuku.update.form.tanggalPinjam =
|
||||
date ? new Date(date).toISOString() : '';
|
||||
}}
|
||||
label={<Text fw="bold" fz="sm">Tanggal Pinjam</Text>}
|
||||
placeholder="Masukkan tanggal pinjam"
|
||||
required
|
||||
/>
|
||||
|
||||
<DateInput
|
||||
value={formData.tanggalKembali}
|
||||
onChange={(date) => {
|
||||
perpustakaanDigitalState.peminjamanBuku.update.form.tanggalKembali =
|
||||
date ? new Date(date).toISOString() : '';
|
||||
}}
|
||||
label={<Text fw="bold" fz="sm">Tanggal Kembali</Text>}
|
||||
placeholder="Masukkan tanggal kembali"
|
||||
required
|
||||
/>
|
||||
|
||||
<DateInput
|
||||
value={formData.batasKembali}
|
||||
onChange={(date) => {
|
||||
perpustakaanDigitalState.peminjamanBuku.update.form.batasKembali =
|
||||
date ? new Date(date).toISOString() : '';
|
||||
}}
|
||||
label={<Text fw="bold" fz="sm">Batas Kembali</Text>}
|
||||
placeholder="Masukkan batas kembali"
|
||||
required
|
||||
/>
|
||||
|
||||
<Select
|
||||
value={formData.status}
|
||||
onChange={(val) => handleChange('status', val as Status)}
|
||||
label={<Text fw="bold" fz="sm">Status</Text>}
|
||||
placeholder="Pilih status"
|
||||
data={[
|
||||
{ value: "Dipinjam", label: "Dipinjam" },
|
||||
{ value: "Dikembalikan", label: "Dikembalikan" },
|
||||
{ value: "Terlambat", label: "Terlambat" },
|
||||
{ value: "Dibatalkan", label: "Dibatalkan" },
|
||||
]}
|
||||
required
|
||||
/>
|
||||
|
||||
<Box>
|
||||
<Text fw="bold" fz="sm" mb={6}>Catatan</Text>
|
||||
<EditEditor
|
||||
value={formData.catatan}
|
||||
onChange={(htmlContent) => handleChange('catatan', htmlContent)}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Group justify="right">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
size="md"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||
}}
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditPeminjam;
|
||||
@@ -0,0 +1,237 @@
|
||||
'use client';
|
||||
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||
import perpustakaanDigitalState from '@/app/admin/(dashboard)/_state/pendidikan/perpustakaan-digital';
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Paper,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Text,
|
||||
Tooltip
|
||||
} from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
|
||||
function DetailDataPeminjaman() {
|
||||
const stateDetail = useProxy(perpustakaanDigitalState.peminjamanBuku);
|
||||
const [modalHapus, setModalHapus] = useState(false);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
|
||||
useShallowEffect(() => {
|
||||
stateDetail.findUnique.load(params?.id as string);
|
||||
}, []);
|
||||
|
||||
const data = stateDetail.findUnique.data;
|
||||
|
||||
const handleHapus = () => {
|
||||
if (selectedId) {
|
||||
stateDetail.delete.delete(selectedId);
|
||||
setModalHapus(false);
|
||||
setSelectedId(null);
|
||||
router.push('/admin/pendidikan/perpustakaan-digital/peminjam');
|
||||
}
|
||||
};
|
||||
|
||||
const renderStatusBadge = (status: string) => {
|
||||
const normalized = status?.toUpperCase();
|
||||
|
||||
switch (normalized) {
|
||||
case 'DIPINJAM':
|
||||
return <Badge color="blue" variant="light">Dipinjam</Badge>;
|
||||
case 'DIKEMBALIKAN':
|
||||
return <Badge color="green" variant="light">Dikembalikan</Badge>;
|
||||
case 'TERLAMBAT':
|
||||
return <Badge color="orange" variant="light">Terlambat</Badge>;
|
||||
case 'DIBATALKAN':
|
||||
return <Badge color="red" variant="light">Dibatalkan</Badge>;
|
||||
default:
|
||||
return <Badge color="gray" variant="light">Tidak diketahui</Badge>;
|
||||
}
|
||||
};
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton height={500} radius="md" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
|
||||
mb={15}
|
||||
>
|
||||
Kembali
|
||||
</Button>
|
||||
|
||||
<Paper
|
||||
withBorder
|
||||
w={{ base: '100%', md: '60%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
|
||||
Detail Data Peminjam Buku
|
||||
</Text>
|
||||
|
||||
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
|
||||
<Stack gap="sm">
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">
|
||||
Nama
|
||||
</Text>
|
||||
<Text fz="md" c="dimmed">
|
||||
{data.nama || '-'}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">
|
||||
No Telp
|
||||
</Text>
|
||||
<Text
|
||||
fz="md"
|
||||
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
|
||||
>
|
||||
{data.noTelp || '-'}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{/* 🔹 Editable Status */}
|
||||
<Box>
|
||||
<Group justify="space-between" align="end">
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">
|
||||
Status
|
||||
</Text>
|
||||
{renderStatusBadge(data.status || '')}
|
||||
</Box>
|
||||
</Group>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">
|
||||
Alamat
|
||||
</Text>
|
||||
<Text fz="md" c="dimmed">
|
||||
{data.alamat || '-'}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">
|
||||
Buku
|
||||
</Text>
|
||||
<Text fz="md" c="dimmed">
|
||||
{data.buku?.judul || '-'}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">
|
||||
Tanggal Pinjam
|
||||
</Text>
|
||||
<Text fz="md" c="dimmed">
|
||||
{data.tanggalPinjam
|
||||
? new Date(data.tanggalPinjam).toLocaleDateString('id-ID')
|
||||
: '-'}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">
|
||||
Tanggal Kembali
|
||||
</Text>
|
||||
<Text fz="md" c="dimmed">
|
||||
{data.tanggalKembali
|
||||
? new Date(data.tanggalKembali).toLocaleDateString('id-ID')
|
||||
: '-'}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">
|
||||
Batas Kembali
|
||||
</Text>
|
||||
<Text fz="md" c="dimmed">
|
||||
{data.batasKembali
|
||||
? new Date(data.batasKembali).toLocaleDateString('id-ID')
|
||||
: '-'}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">
|
||||
Catatan
|
||||
</Text>
|
||||
<Text
|
||||
fz="md"
|
||||
c="dimmed"
|
||||
dangerouslySetInnerHTML={{ __html: data.catatan || '-' }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Group gap="sm" mt="sm">
|
||||
<Tooltip label="Hapus Peminjam Buku" withArrow position="top">
|
||||
<Button
|
||||
color="red"
|
||||
onClick={() => {
|
||||
setSelectedId(data.id);
|
||||
setModalHapus(true);
|
||||
}}
|
||||
variant="light"
|
||||
radius="md"
|
||||
size="md"
|
||||
disabled={stateDetail.delete.loading}
|
||||
>
|
||||
<IconTrash size={20} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label="Edit Peminjam Buku" withArrow position="top">
|
||||
<Button
|
||||
color="green"
|
||||
onClick={() =>
|
||||
router.push(`/admin/pendidikan/perpustakaan-digital/peminjam/${data.id}/edit`)
|
||||
}
|
||||
variant="light"
|
||||
radius="md"
|
||||
size="md"
|
||||
>
|
||||
<IconEdit size={20} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
<ModalKonfirmasiHapus
|
||||
opened={modalHapus}
|
||||
onClose={() => setModalHapus(false)}
|
||||
onConfirm={handleHapus}
|
||||
text="Apakah Anda yakin ingin menghapus data ini?"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default DetailDataPeminjaman;
|
||||
@@ -0,0 +1,157 @@
|
||||
'use client';
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Pagination,
|
||||
Paper,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Table,
|
||||
TableTbody,
|
||||
TableTd,
|
||||
TableTh,
|
||||
TableThead,
|
||||
TableTr,
|
||||
Text,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '../../../_com/header';
|
||||
import perpustakaanDigitalState from '../../../_state/pendidikan/perpustakaan-digital';
|
||||
|
||||
function PeminjamBuku() {
|
||||
const [search, setSearch] = useState('');
|
||||
return (
|
||||
<Box>
|
||||
<HeaderSearch
|
||||
title="Peminjam Buku"
|
||||
placeholder="Cari peminjam..."
|
||||
searchIcon={<IconSearch size={20} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
<ListPeminjamBuku search={search} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function ListPeminjamBuku({ search }: { search: string }) {
|
||||
const statePeminjam = useProxy(perpustakaanDigitalState.peminjamanBuku);
|
||||
const router = useRouter();
|
||||
|
||||
const { data, page, totalPages, loading, load } = statePeminjam.findMany;
|
||||
|
||||
useShallowEffect(() => {
|
||||
load(page, 10, search);
|
||||
}, [page, search]);
|
||||
|
||||
const filteredData = data || [];
|
||||
|
||||
// 🔹 Fungsi helper untuk badge status
|
||||
const renderStatusBadge = (status: string) => {
|
||||
const normalized = status?.toUpperCase();
|
||||
|
||||
switch (normalized) {
|
||||
case 'DIPINJAM':
|
||||
return <Badge color="blue" variant="light">Dipinjam</Badge>;
|
||||
case 'DIKEMBALIKAN':
|
||||
return <Badge color="green" variant="light">Dikembalikan</Badge>;
|
||||
case 'TERLAMBAT':
|
||||
return <Badge color="orange" variant="light">Terlambat</Badge>;
|
||||
case 'DIBATALKAN':
|
||||
return <Badge color="red" variant="light">Dibatalkan</Badge>;
|
||||
default:
|
||||
return <Badge color="gray" variant="light">Tidak diketahui</Badge>;
|
||||
}
|
||||
};
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton height={500} radius="md" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||
<Title order={4} mb="md">Daftar Peminjam Buku</Title>
|
||||
<Box style={{ overflowX: 'auto' }}>
|
||||
<Table highlightOnHover>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh style={{ width: '10%' }}>No</TableTh>
|
||||
<TableTh style={{ width: '60%' }}>Nama Peminjam</TableTh>
|
||||
<TableTh style={{ width: '15%' }}>Status</TableTh>
|
||||
<TableTh style={{ width: '15%' }}>Detail</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item, index) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>{index + 1}</TableTd>
|
||||
<TableTd>
|
||||
<Text truncate fz="sm">{item.nama}</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
{renderStatusBadge(item.status)}
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button
|
||||
variant="light"
|
||||
color="green"
|
||||
onClick={() =>
|
||||
router.push(`/admin/pendidikan/perpustakaan-digital/peminjam/${item.id}`)
|
||||
}
|
||||
>
|
||||
<IconDeviceImacCog size={20} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={4}>
|
||||
<Center py={20}>
|
||||
<Text c="dimmed">
|
||||
Tidak ada data Peminjam buku yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
)}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{totalPages > 1 && (
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
total={totalPages}
|
||||
mt="md"
|
||||
mb="md"
|
||||
color="blue"
|
||||
radius="md"
|
||||
/>
|
||||
</Center>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default PeminjamBuku;
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
|
||||
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { IconSchool, IconTarget } from '@tabler/icons-react';
|
||||
@@ -11,13 +11,6 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
label: "Program Unggulan",
|
||||
value: "program-unggulan",
|
||||
href: "/admin/pendidikan/program-pendidikan-anak/program-unggulan",
|
||||
icon: <IconSchool size={18} stroke={1.8} />,
|
||||
tooltip: "Lihat dan kelola program unggulan pendidikan anak",
|
||||
},
|
||||
{
|
||||
label: "Tujuan Program",
|
||||
value: "tujuan-program",
|
||||
@@ -25,6 +18,13 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
||||
icon: <IconTarget size={18} stroke={1.8} />,
|
||||
tooltip: "Atur tujuan program pendidikan anak",
|
||||
},
|
||||
{
|
||||
label: "Program Unggulan",
|
||||
value: "program-unggulan",
|
||||
href: "/admin/pendidikan/program-pendidikan-anak/program-unggulan",
|
||||
icon: <IconSchool size={18} stroke={1.8} />,
|
||||
tooltip: "Lihat dan kelola program unggulan pendidikan anak",
|
||||
}
|
||||
];
|
||||
|
||||
const currentTab = tabs.find(tab => tab.href === pathname);
|
||||
@@ -59,36 +59,42 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
||||
radius="lg"
|
||||
keepMounted={false}
|
||||
>
|
||||
<TabsList
|
||||
p="sm"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||
borderRadius: "1rem",
|
||||
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||
<ScrollArea type="auto" offsetScrollbars>
|
||||
<TabsList
|
||||
p="sm"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||
borderRadius: "1rem",
|
||||
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||
display: "flex",
|
||||
flexWrap: "nowrap",
|
||||
gap: "0.5rem",
|
||||
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
|
||||
}}
|
||||
>
|
||||
{tabs.map((tab, i) => (
|
||||
<Tooltip
|
||||
key={i}
|
||||
label={tab.tooltip}
|
||||
position="bottom"
|
||||
withArrow
|
||||
transitionProps={{ transition: 'pop', duration: 200 }}
|
||||
>
|
||||
<TabsTab
|
||||
value={tab.value}
|
||||
leftSection={tab.icon}
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "0.9rem",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
>
|
||||
{tabs.map((tab, i) => (
|
||||
<Tooltip
|
||||
key={i}
|
||||
label={tab.tooltip}
|
||||
position="bottom"
|
||||
withArrow
|
||||
transitionProps={{ transition: 'pop', duration: 200 }}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTab>
|
||||
</Tooltip>
|
||||
))}
|
||||
</TabsList>
|
||||
<TabsTab
|
||||
value={tab.value}
|
||||
leftSection={tab.icon}
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "0.9rem",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTab>
|
||||
</Tooltip>
|
||||
))}
|
||||
</TabsList>
|
||||
</ScrollArea>
|
||||
|
||||
{tabs.map((tab, i) => (
|
||||
<TabsPanel
|
||||
|
||||
@@ -103,18 +103,7 @@ function ListPegawaiPPID({ search }: { search: string }) {
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{(() => {
|
||||
console.log('Rendering table with items:', stateOrganisasi.findMany.data);
|
||||
return null;
|
||||
})()}
|
||||
{([...filteredData]
|
||||
.sort((a, b) => {
|
||||
if (a.isActive === b.isActive) {
|
||||
return a.namaLengkap.localeCompare(b.namaLengkap); // kalau status sama, urut nama
|
||||
}
|
||||
return Number(b.isActive) - Number(a.isActive); // aktif duluan
|
||||
}) // Aktif di atas
|
||||
).map((item) => (
|
||||
{filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>
|
||||
<Box w={150}>
|
||||
|
||||
@@ -21,10 +21,10 @@ function ListStrukturOrganisasiPPID() {
|
||||
const stateOrganisasi = useProxy(stateStrukturPPID.pegawai);
|
||||
|
||||
useEffect(() => {
|
||||
stateOrganisasi.findMany.load();
|
||||
stateOrganisasi.findManyAll.load();
|
||||
}, []);
|
||||
|
||||
if (stateOrganisasi.findMany.loading) {
|
||||
if (stateOrganisasi.findManyAll.loading) {
|
||||
return (
|
||||
<Center py={40}>
|
||||
<Loader size="lg" />
|
||||
@@ -32,7 +32,7 @@ function ListStrukturOrganisasiPPID() {
|
||||
);
|
||||
}
|
||||
|
||||
if (!stateOrganisasi.findMany.data || stateOrganisasi.findMany.data.length === 0) {
|
||||
if (!stateOrganisasi.findManyAll.data || stateOrganisasi.findManyAll.data.length === 0) {
|
||||
return (
|
||||
<Stack align="center" py={60} gap="sm">
|
||||
<IconUsers size={60} stroke={1.5} color="var(--mantine-color-gray-6)" />
|
||||
@@ -43,7 +43,7 @@ function ListStrukturOrganisasiPPID() {
|
||||
|
||||
const posisiMap = new Map<string, any>();
|
||||
|
||||
const aktifPegawai = stateOrganisasi.findMany.data.filter(p => p.isActive);
|
||||
const aktifPegawai = stateOrganisasi.findManyAll.data?.filter(p => p.isActive);
|
||||
|
||||
for (const pegawai of aktifPegawai) {
|
||||
const posisiId = pegawai.posisi.id;
|
||||
|
||||
@@ -377,22 +377,5 @@ export const navBar = [
|
||||
path: "/admin/pendidikan/data-pendidikan"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "User & Role",
|
||||
name: "User & Role",
|
||||
path: "",
|
||||
children: [
|
||||
{
|
||||
id: "User",
|
||||
name: "User",
|
||||
path: "/admin/user&role/user"
|
||||
},
|
||||
{
|
||||
id: "Role",
|
||||
name: "Role",
|
||||
path: "/admin/user&role/role"
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,11 +1,36 @@
|
||||
import LayoutTabs from "./(dashboard)/landing-page/profile/_lib/layoutTabs";
|
||||
import ProgramInovasi from "./(dashboard)/landing-page/profile/program-inovasi/page";
|
||||
'use client';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
// Dynamically import the components with SSR disabled to prevent hydration issues
|
||||
const LayoutTabs = dynamic(
|
||||
() => import('./(dashboard)/landing-page/profile/_lib/layoutTabs'),
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
const ProgramInovasi = dynamic(
|
||||
() => import('./(dashboard)/landing-page/profile/program-inovasi/page'),
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
export default function Page() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
// This ensures the component is only rendered on the client
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) {
|
||||
return null; // or return a loading state
|
||||
}
|
||||
|
||||
return (
|
||||
<LayoutTabs>
|
||||
<ProgramInovasi />
|
||||
</LayoutTabs>
|
||||
<div suppressHydrationWarning>
|
||||
<LayoutTabs>
|
||||
<ProgramInovasi />
|
||||
</LayoutTabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -9,7 +9,7 @@ export default async function KegiatanDesaFindFirst(context: Context) {
|
||||
|
||||
if (kategori) {
|
||||
where.kategoriKegiatan = {
|
||||
name: { equals: kategori, mode: 'insensitive' }
|
||||
nama: { equals: kategori, mode: 'insensitive' }
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -66,9 +66,6 @@ async function lembagaPendidikanFindMany(context: Context) {
|
||||
})
|
||||
]);
|
||||
|
||||
console.log('Fetched data count:', data.length);
|
||||
console.log('Total count:', total);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Success fetch lembaga pendidikan with pagination",
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
export default async function dataPerpustakaanFindManyAll(context: Context) {
|
||||
const search = (context.query.search as string) || "";
|
||||
const isActiveParam = context.query.isActive;
|
||||
|
||||
// Buat where clause dinamis
|
||||
const where: any = {};
|
||||
|
||||
if (isActiveParam !== undefined) {
|
||||
where.isActive = isActiveParam === "true";
|
||||
}
|
||||
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ judul: { contains: search, mode: "insensitive" } },
|
||||
{ deskripsi: { contains: search, mode: "insensitive" } },
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await prisma.dataPerpustakaan.findMany({
|
||||
where,
|
||||
include: {
|
||||
kategori: true,
|
||||
image: true,
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Success fetch all data perpustakaan (non-paginated)",
|
||||
total: data.length,
|
||||
data,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Find many all error:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed fetch all data perpustakaan",
|
||||
total: 0,
|
||||
data: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import dataPerpustakaanDelete from "./del";
|
||||
import dataPerpustakaanFindMany from "./findMany";
|
||||
import dataPerpustakaanFindUnique from "./findUnique";
|
||||
import dataPerpustakaanUpdate from "./updt";
|
||||
import dataPerpustakaanFindManyAll from "./findManyAll";
|
||||
|
||||
const DataPerpustakaan = new Elysia({
|
||||
prefix: "/dataperpustakaan",
|
||||
@@ -18,7 +19,7 @@ const DataPerpustakaan = new Elysia({
|
||||
kategoriId: t.String(),
|
||||
}),
|
||||
})
|
||||
|
||||
.get("/findManyAll", dataPerpustakaanFindManyAll)
|
||||
.get("/findMany", dataPerpustakaanFindMany)
|
||||
.get("/:id", async (context) => {
|
||||
const response = await dataPerpustakaanFindUnique(
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import Elysia from "elysia";
|
||||
import DataPerpustakaan from "./data-perpustakaan";
|
||||
import KategoriBuku from "./kategori-buku";
|
||||
import PeminjamanBuku from "./peminjaman";
|
||||
|
||||
const PerpustakaanDigital = new Elysia({
|
||||
prefix: "/perpustakaandigital",
|
||||
tags: ["Pendidikan / Perpustakaan Digital"],
|
||||
})
|
||||
.use(DataPerpustakaan)
|
||||
.use(KategoriBuku);
|
||||
.use(KategoriBuku)
|
||||
.use(PeminjamanBuku);
|
||||
|
||||
export default PerpustakaanDigital;
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
type FormCreate = {
|
||||
nama: string;
|
||||
noTelp: string;
|
||||
alamat: string;
|
||||
bukuId: string;
|
||||
tanggalPinjam: string;
|
||||
batasKembali: string;
|
||||
tanggalKembali?: string;
|
||||
catatan?: string;
|
||||
}
|
||||
|
||||
export default async function peminjamanBukuCreate(context: Context) {
|
||||
const body = (await context.body) as FormCreate;
|
||||
|
||||
try {
|
||||
const result = await prisma.peminjamanBuku.create({
|
||||
data: {
|
||||
nama: body.nama,
|
||||
noTelp: body.noTelp,
|
||||
alamat: body.alamat,
|
||||
bukuId: body.bukuId,
|
||||
tanggalPinjam: body.tanggalPinjam,
|
||||
batasKembali: body.batasKembali,
|
||||
tanggalKembali: body.tanggalKembali,
|
||||
catatan: body.catatan,
|
||||
},
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
message: "Berhasil membuat peminjaman buku",
|
||||
data: result,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error creating peminjaman buku:", error);
|
||||
throw new Error("Gagal membuat peminjaman buku: " + (error as Error).message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
export default async function peminjamanBukuDelete(context: Context) {
|
||||
const id = context.params.id as string;
|
||||
|
||||
await prisma.peminjamanBuku.delete({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
success: true,
|
||||
message: "Success delete peminjaman buku",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
async function peminjamanBukuFindMany(context: Context) {
|
||||
// Ambil parameter dari query
|
||||
const page = Number(context.query.page) || 1;
|
||||
const limit = Number(context.query.limit) || 10;
|
||||
const search = (context.query.search as string) || '';
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
// Buat where clause
|
||||
const where: any = {};
|
||||
|
||||
// Tambahkan pencarian (jika ada)
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ catatan: { contains: search, mode: 'insensitive' } },
|
||||
{ nama: { contains: search, mode: 'insensitive' } },
|
||||
{ buku: { judul: { contains: search, mode: 'insensitive' } } },
|
||||
{ noTelp: { contains: search, mode: 'insensitive' } },
|
||||
{ alamat: { contains: search, mode: 'insensitive' } },
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
// Ambil data dan total count secara paralel
|
||||
const [data, total] = await Promise.all([
|
||||
prisma.peminjamanBuku.findMany({
|
||||
where,
|
||||
include: {
|
||||
buku: {
|
||||
select: {
|
||||
id: true,
|
||||
judul: true,
|
||||
kategoriId: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
deletedAt: true,
|
||||
isActive: true,
|
||||
imageId: true,
|
||||
deskripsi: true
|
||||
}
|
||||
},
|
||||
},
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: { tanggalPinjam: 'desc' },
|
||||
}),
|
||||
prisma.peminjamanBuku.count({ where }),
|
||||
]);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Berhasil ambil data peminjaman buku dengan pagination",
|
||||
data,
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("Error di peminjamanBukuFindMany:", e);
|
||||
return {
|
||||
success: false,
|
||||
message: "Gagal mengambil data peminjaman buku",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default peminjamanBukuFindMany;
|
||||
@@ -0,0 +1,49 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
export default async function peminjamanBukuFindUnique(request: Request) {
|
||||
const url = new URL(request.url);
|
||||
const pathSegments = url.pathname.split('/');
|
||||
const id = pathSegments[pathSegments.length - 1];
|
||||
|
||||
if (!id) {
|
||||
return {
|
||||
success: false,
|
||||
message: "ID is required",
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof id !== 'string') {
|
||||
return {
|
||||
success: false,
|
||||
message: "ID is required",
|
||||
}
|
||||
}
|
||||
|
||||
const data = await prisma.peminjamanBuku.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
buku: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Data not found",
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Success get data peminjaman buku",
|
||||
data,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Find by ID error:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "Gagal mengambil data: " + (error instanceof Error ? error.message : 'Unknown error'),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import Elysia, { t } from "elysia";
|
||||
import peminjamanBukuCreate from "./create";
|
||||
import peminjamanBukuDelete from "./del";
|
||||
import peminjamanBukuFindMany from "./findMany";
|
||||
import peminjamanBukuFindUnique from "./findUnique";
|
||||
import peminjamanBukuUpdate from "./updt";
|
||||
|
||||
const PeminjamanBuku = new Elysia({
|
||||
prefix: "/peminjamanbuku",
|
||||
tags: ["Pendidikan / Perpustakaan Digital / Peminjaman Buku"],
|
||||
})
|
||||
|
||||
.post("/create", peminjamanBukuCreate, {
|
||||
body: t.Object({
|
||||
nama: t.String(),
|
||||
noTelp: t.String(),
|
||||
alamat: t.String(),
|
||||
bukuId: t.String(),
|
||||
tanggalPinjam: t.String(),
|
||||
batasKembali: t.String(),
|
||||
tanggalKembali: t.String(),
|
||||
catatan: t.String()
|
||||
}),
|
||||
})
|
||||
|
||||
.get("/findMany", peminjamanBukuFindMany)
|
||||
.get("/:id", async (context) => {
|
||||
const response = await peminjamanBukuFindUnique(
|
||||
new Request(context.request)
|
||||
);
|
||||
return response;
|
||||
})
|
||||
.put("/:id", peminjamanBukuUpdate, {
|
||||
body: t.Object({
|
||||
nama: t.String(),
|
||||
noTelp: t.String(),
|
||||
alamat: t.String(),
|
||||
bukuId: t.String(),
|
||||
tanggalPinjam: t.String(),
|
||||
batasKembali: t.String(),
|
||||
tanggalKembali: t.String(),
|
||||
catatan: t.String(),
|
||||
status: t.String()
|
||||
}),
|
||||
})
|
||||
.delete("/del/:id", peminjamanBukuDelete);
|
||||
|
||||
export default PeminjamanBuku;
|
||||
@@ -0,0 +1,49 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
type FormUpdate = {
|
||||
nama: string;
|
||||
noTelp: string;
|
||||
alamat: string;
|
||||
bukuId: string;
|
||||
tanggalPinjam: string;
|
||||
batasKembali: string;
|
||||
tanggalKembali?: string;
|
||||
catatan?: string;
|
||||
status?: 'Dipinjam' | 'Dikembalikan' | 'Terlambat' | 'Dibatalkan';
|
||||
};
|
||||
|
||||
export default async function dataPerpustakaanUpdate(context: Context) {
|
||||
const body = (await context.body) as FormUpdate;
|
||||
const id = context.params.id as string;
|
||||
|
||||
try {
|
||||
const result = await prisma.peminjamanBuku.update({
|
||||
where: { id },
|
||||
data: {
|
||||
nama: body.nama,
|
||||
noTelp: body.noTelp,
|
||||
alamat: body.alamat,
|
||||
bukuId: body.bukuId,
|
||||
tanggalPinjam: body.tanggalPinjam,
|
||||
batasKembali: body.batasKembali,
|
||||
tanggalKembali: body.tanggalKembali,
|
||||
catatan: body.catatan,
|
||||
status: body.status,
|
||||
},
|
||||
include: {
|
||||
buku: true,
|
||||
}
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
message: "Berhasil mengupdate data peminjaman buku",
|
||||
data: result,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error updating data peminjaman buku:", error);
|
||||
throw new Error(
|
||||
"Gagal mengupdate data peminjaman buku: " + (error as Error).message
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,61 +2,67 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
// Di findMany.ts
|
||||
export default async function pegawaiFindMany(context: Context) {
|
||||
const page = Number(context.query.page) || 1;
|
||||
const limit = Number(context.query.limit) || 10;
|
||||
const search = (context.query.search as string) || "";
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
// Buat where clause
|
||||
const isActiveParam = context.query.isActive;
|
||||
|
||||
// where clause dinamis
|
||||
const where: any = {};
|
||||
if (isActiveParam !== undefined) {
|
||||
where.isActive = isActiveParam === "true";
|
||||
}
|
||||
|
||||
// Tambahkan pencarian (jika ada)
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ namaLengkap: { contains: search, mode: "insensitive" } },
|
||||
{ alamat: { contains: search, mode: "insensitive" } },
|
||||
{ posisi: { nama: { contains: search, mode: "insensitive" } } },
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
const [data, total] = await Promise.all([
|
||||
// Ambil semua data terlebih dahulu (tanpa pagination)
|
||||
const [allData, total] = await Promise.all([
|
||||
prisma.pegawaiPPID.findMany({
|
||||
where,
|
||||
include: {
|
||||
posisi: true,
|
||||
image: true,
|
||||
},
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: { posisi: { hierarki: "asc" } },
|
||||
}),
|
||||
prisma.pegawaiPPID.count({
|
||||
where,
|
||||
}),
|
||||
prisma.pegawaiPPID.count({ where }),
|
||||
]);
|
||||
|
||||
// Sort manual berdasarkan hierarki posisi
|
||||
const sortedData = allData.sort((a, b) => {
|
||||
// Sort berdasarkan hierarki terlebih dahulu
|
||||
if (a.posisi.hierarki !== b.posisi.hierarki) {
|
||||
return a.posisi.hierarki - b.posisi.hierarki;
|
||||
}
|
||||
// Jika hierarki sama, sort berdasarkan nama posisi
|
||||
return a.posisi.nama.localeCompare(b.posisi.nama);
|
||||
});
|
||||
|
||||
// Lakukan pagination manual setelah sorting
|
||||
const paginatedData = sortedData.slice(skip, skip + limit);
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Success fetch pegawai with pagination",
|
||||
data,
|
||||
message: "Success fetch pegawai with hierarchy order",
|
||||
data: paginatedData,
|
||||
page,
|
||||
totalPages,
|
||||
total,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("Find many paginated error:", e);
|
||||
} catch (error) {
|
||||
console.error("Find many pegawai error:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed fetch pegawai with pagination",
|
||||
message: "Failed fetch pegawai",
|
||||
data: [],
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
export default async function pegawaiFindManyAll(context: Context) {
|
||||
const search = (context.query.search as string) || "";
|
||||
const isActiveParam = context.query.isActive;
|
||||
|
||||
// Buat where clause dinamis
|
||||
const where: any = {};
|
||||
|
||||
if (isActiveParam !== undefined) {
|
||||
where.isActive = isActiveParam === "true";
|
||||
}
|
||||
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ namaLengkap: { contains: search, mode: "insensitive" } },
|
||||
{ alamat: { contains: search, mode: "insensitive" } },
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await prisma.pegawaiPPID.findMany({
|
||||
where,
|
||||
include: {
|
||||
posisi: true,
|
||||
image: true,
|
||||
},
|
||||
orderBy: { posisi: { hierarki: "asc" } },
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Success fetch all pegawai (non-paginated)",
|
||||
total: data.length,
|
||||
data,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Find many all error:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed fetch all pegawai",
|
||||
total: 0,
|
||||
data: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import pegawaiCreate from "./create";
|
||||
import pegawaiNonActive from "./nonActive";
|
||||
import pegawaiUpdate from "./updt";
|
||||
import pegawaiDelete from "./del";
|
||||
import pegawaiFindManyAll from "./findManyAll";
|
||||
|
||||
|
||||
const Pegawai = new Elysia({
|
||||
@@ -15,6 +16,9 @@ const Pegawai = new Elysia({
|
||||
// ✅ Find all
|
||||
.get("/find-many", pegawaiFindMany)
|
||||
|
||||
// ✅ Find all (non-paginated)
|
||||
.get("/find-many-all", pegawaiFindManyAll)
|
||||
|
||||
// ✅ Find by ID
|
||||
.get("/:id", async (context) => {
|
||||
const response = await pegawaiFindUnique(context);
|
||||
|
||||
1187
src/app/api/[[...slugs]]/_lib/search/findMany.ts
Normal file
1187
src/app/api/[[...slugs]]/_lib/search/findMany.ts
Normal file
File diff suppressed because it is too large
Load Diff
10
src/app/api/[[...slugs]]/_lib/search/index.ts
Normal file
10
src/app/api/[[...slugs]]/_lib/search/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import Elysia from "elysia";
|
||||
import searchFindMany from "./findMany";
|
||||
|
||||
const Search = new Elysia({
|
||||
prefix: "/api/search",
|
||||
tags: ["Search"],
|
||||
})
|
||||
.get("/findMany", searchFindMany);
|
||||
|
||||
export default Search;
|
||||
148
src/app/api/[[...slugs]]/_lib/search/searchState.ts
Normal file
148
src/app/api/[[...slugs]]/_lib/search/searchState.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client';
|
||||
import { proxy } from 'valtio';
|
||||
import { debounce } from 'lodash';
|
||||
import ApiFetch from '@/lib/api-fetch';
|
||||
|
||||
interface SearchResult {
|
||||
type?: string;
|
||||
id: string | number;
|
||||
title?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const searchState = proxy({
|
||||
query: '',
|
||||
page: 1,
|
||||
limit: 10,
|
||||
type: '', // kosong = global search
|
||||
results: [] as SearchResult[],
|
||||
nextPage: null as number | null,
|
||||
loading: false,
|
||||
|
||||
async fetch() {
|
||||
if (!searchState.query) {
|
||||
searchState.results = [];
|
||||
return;
|
||||
}
|
||||
|
||||
searchState.loading = true;
|
||||
|
||||
const res = await ApiFetch.api.search.findMany.get({
|
||||
query: {
|
||||
query: searchState.query,
|
||||
page: searchState.page,
|
||||
limit: searchState.limit,
|
||||
type: searchState.type,
|
||||
},
|
||||
});
|
||||
|
||||
if (searchState.page === 1) {
|
||||
searchState.results = res.data?.data || [];
|
||||
} else {
|
||||
searchState.results.push(...(res.data?.data || []));
|
||||
}
|
||||
|
||||
searchState.nextPage = res.data?.nextPage || null;
|
||||
searchState.loading = false;
|
||||
},
|
||||
|
||||
async next() {
|
||||
if (!searchState.nextPage || searchState.loading) return;
|
||||
searchState.page = searchState.nextPage;
|
||||
await searchState.fetch();
|
||||
},
|
||||
});
|
||||
|
||||
// 🕒 debounce-nya tetap kita export biar bisa dipanggil manual
|
||||
export const debouncedFetch = debounce(() => {
|
||||
searchState.page = 1;
|
||||
searchState.fetch();
|
||||
}, 500);
|
||||
|
||||
export default searchState;
|
||||
|
||||
|
||||
// 'use client';
|
||||
// import { proxy, subscribe } from 'valtio';
|
||||
// import { debounce } from 'lodash';
|
||||
// import ApiFetch from '@/lib/api-fetch';
|
||||
|
||||
// interface SearchResult {
|
||||
// type?: string;
|
||||
// id: string | number;
|
||||
// title?: string;
|
||||
// [key: string]: any;
|
||||
// }
|
||||
|
||||
// const searchState = proxy({
|
||||
// query: '',
|
||||
// page: 1,
|
||||
// limit: 10,
|
||||
// type: '', // kosong = global search
|
||||
// results: [] as SearchResult[],
|
||||
// nextPage: null as number | null,
|
||||
// loading: false,
|
||||
|
||||
// // --- fetch utama ---
|
||||
// async fetch() {
|
||||
// if (!searchState.query.trim()) {
|
||||
// // 🧹 kalau query kosong, kosongin data dan stop
|
||||
// searchState.results = [];
|
||||
// searchState.nextPage = null;
|
||||
// searchState.loading = false;
|
||||
// return;
|
||||
// }
|
||||
|
||||
// searchState.loading = true;
|
||||
|
||||
// try {
|
||||
// const res = await ApiFetch.api.search.findMany.get({
|
||||
// query: {
|
||||
// query: searchState.query,
|
||||
// page: searchState.page,
|
||||
// limit: searchState.limit,
|
||||
// type: searchState.type,
|
||||
// },
|
||||
// });
|
||||
|
||||
// const newData = res.data?.data || [];
|
||||
|
||||
// // Kalau ini page pertama, replace data
|
||||
// if (searchState.page === 1) {
|
||||
// searchState.results = newData;
|
||||
// } else {
|
||||
// // Kalau page berikutnya, append data
|
||||
// searchState.results = [...searchState.results, ...newData];
|
||||
// }
|
||||
|
||||
// searchState.nextPage = res.data?.nextPage || null;
|
||||
// } catch (err) {
|
||||
// console.error('Search fetch error:', err);
|
||||
// } finally {
|
||||
// searchState.loading = false;
|
||||
// }
|
||||
// },
|
||||
|
||||
// // --- load next page (infinite scroll) ---
|
||||
// async next() {
|
||||
// if (!searchState.nextPage || searchState.loading) return;
|
||||
// searchState.page = searchState.nextPage;
|
||||
// await searchState.fetch();
|
||||
// },
|
||||
// });
|
||||
|
||||
// // --- debounce agar gak fetch tiap ketik ---
|
||||
// const debouncedFetch = debounce(() => {
|
||||
// // reset pagination setiap query berubah
|
||||
// searchState.page = 1;
|
||||
// searchState.fetch();
|
||||
// }, 500);
|
||||
|
||||
// // --- auto trigger setiap query berubah ---
|
||||
// subscribe(searchState, () => {
|
||||
// // kalau query berubah, jalankan debounce fetch
|
||||
// debouncedFetch();
|
||||
// });
|
||||
|
||||
// export default searchState;
|
||||
@@ -25,6 +25,7 @@ import LandingPage from "./_lib/landing_page";
|
||||
import Pendidikan from "./_lib/pendidikan";
|
||||
import User from "./_lib/user";
|
||||
import Role from "./_lib/user/role";
|
||||
import Search from "./_lib/search";
|
||||
|
||||
const ROOT = process.cwd();
|
||||
|
||||
@@ -95,6 +96,7 @@ const ApiServer = new Elysia()
|
||||
.use(Pendidikan)
|
||||
.use(User)
|
||||
.use(Role)
|
||||
.use(Search)
|
||||
|
||||
.onError(({ code }) => {
|
||||
if (code === "NOT_FOUND") {
|
||||
|
||||
@@ -135,7 +135,7 @@ export default function Content({ kategori }: { kategori: string }) {
|
||||
{item.kategoriBerita?.name || kategori}
|
||||
</Badge>
|
||||
<Text fw={600} size="lg" mt="sm" lineClamp={2}>{item.judul}</Text>
|
||||
<Text size="sm" c="dimmed" lineClamp={3} mt="xs" dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||
<Text size="sm" c="dimmed" lineClamp={3} style={{wordBreak: "break-word", whiteSpace: "normal"}} mt="xs" dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||
<Group justify="apart" mt="md" gap="xs">
|
||||
<Text size="xs" c="dimmed">
|
||||
{new Date(item.createdAt).toLocaleDateString('id-ID', {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// src/app/darmasaba/(pages)/desa/berita/[kategori]/page.tsx
|
||||
import { Suspense } from "react";
|
||||
import Content from "./Content";
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ function PelayananPendudukNonPermanent() {
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
await state.pelayananPendudukNonPermanen.findById.load('1');
|
||||
await state.pelayananPendudukNonPermanen.findById.load('edit');
|
||||
} catch (error) {
|
||||
console.error('Gagal memuat data:', error);
|
||||
} finally {
|
||||
|
||||
@@ -17,7 +17,7 @@ function PelayananPerizinanBerusaha() {
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
await state.pelayananPerizinanBerusaha.findById.load('1')
|
||||
await state.pelayananPerizinanBerusaha.findById.load('edit')
|
||||
} catch (error) {
|
||||
console.error('Gagal memuat data:', error);
|
||||
} finally {
|
||||
|
||||
@@ -77,9 +77,7 @@ function Page() {
|
||||
fallbackSrc="https://placehold.co/800x400?text=Gambar+tidak+tersedia"
|
||||
loading="lazy"
|
||||
/>
|
||||
<Text py="md" fz={{ base: "sm", md: "md" }} ta="justify" lh={1.8}>
|
||||
{state.findUnique.data?.deskripsi || 'Belum ada deskripsi untuk potensi desa ini.'}
|
||||
</Text>
|
||||
<Text py="md" fz={{ base: "sm", md: "md" }} ta="justify" lh={1.8} dangerouslySetInnerHTML={{ __html: state.findUnique.data?.deskripsi || 'Belum ada deskripsi untuk potensi desa ini.' }} />
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Container>
|
||||
|
||||
@@ -10,31 +10,36 @@ import ProfilPerbekel from './ui/profilPerbekel';
|
||||
// import LembagaDesa from './ui/lembagaDesa';
|
||||
import MotoDesa from './ui/motoDesa';
|
||||
import SemuaPerbekel from './ui/semuaPerbekel';
|
||||
import ScrollToTopButton from '@/app/darmasaba/_com/scrollToTopButton';
|
||||
|
||||
function Page() {
|
||||
return (
|
||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
<Container w={{ base: "100%", md: "50%" }}>
|
||||
<Stack align='center' gap={0}>
|
||||
<Text fz={{base: "h1", md: "2.5rem"}} c={colors["blue-button"]} fw={"bold"}>
|
||||
Profile Desa
|
||||
</Text>
|
||||
</Stack>
|
||||
</Container>
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<ProfileDesa />
|
||||
<SejarahDesa />
|
||||
<VisimisiDesa />
|
||||
<LambangDesa />
|
||||
<MaskotDesa />
|
||||
<ProfilPerbekel />
|
||||
<MotoDesa />
|
||||
<SemuaPerbekel/>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Box>
|
||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
<Container w={{ base: "100%", md: "50%" }}>
|
||||
<Stack align='center' gap={0}>
|
||||
<Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
|
||||
Profile Desa
|
||||
</Text>
|
||||
</Stack>
|
||||
</Container>
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<ProfileDesa />
|
||||
<SejarahDesa />
|
||||
<VisimisiDesa />
|
||||
<LambangDesa />
|
||||
<MaskotDesa />
|
||||
<ProfilPerbekel />
|
||||
<MotoDesa />
|
||||
<SemuaPerbekel />
|
||||
</Box>
|
||||
</Stack>
|
||||
{/* Tombol Scroll ke Atas */}
|
||||
<ScrollToTopButton />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +1,300 @@
|
||||
import colors from '@/con/colors';
|
||||
import { Stack, Box, Text, Image } from '@mantine/core';
|
||||
import React from 'react';
|
||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client'
|
||||
import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi'
|
||||
import colors from '@/con/colors'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Center,
|
||||
Container,
|
||||
Group,
|
||||
Image,
|
||||
Loader,
|
||||
Paper,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
Tooltip,
|
||||
Transition,
|
||||
} from '@mantine/core'
|
||||
import { IconRefresh, IconSearch, IconUsers } from '@tabler/icons-react'
|
||||
import { OrganizationChart } from 'primereact/organizationchart'
|
||||
import { useEffect } from 'react'
|
||||
import { useProxy } from 'valtio/utils'
|
||||
import BackButton from '../../desa/layanan/_com/BackButto'
|
||||
|
||||
function Page() {
|
||||
export default function Page() {
|
||||
return (
|
||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
<Text px={{ base: 'md', md: 100 }} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
|
||||
Struktur Organisasi dan SK Pengurus BUMDesa
|
||||
</Text>
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Stack gap={'lg'} justify='center'>
|
||||
<Image src={'/api/img/bpddarmasaba.png'} alt='' loading="lazy"/>
|
||||
<Box
|
||||
style={{
|
||||
minHeight: '100vh',
|
||||
background: colors['Bg'],
|
||||
color: '#E6F0FF',
|
||||
paddingBottom: 48,
|
||||
}}
|
||||
>
|
||||
<Container size="xl" py="xl">
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
<Stack align="center" gap="xl" mt="xl">
|
||||
<Title
|
||||
order={1}
|
||||
ta="center"
|
||||
c={colors['blue-button']}
|
||||
fz={{ base: 28, md: 36, lg: 44 }}
|
||||
|
||||
>
|
||||
Struktur Organisasi Dan SK Pengurus BumDes
|
||||
</Title>
|
||||
<Text ta="center" c="black" maw={800}>
|
||||
Gambaran visual peran dan pegawai yang ditugaskan. Arahkan kursor
|
||||
untuk melihat detail atau klik node untuk fokus tampilan.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
<Box mt="lg">
|
||||
<StrukturOrganisasiBumDes />
|
||||
</Box>
|
||||
</Container>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default Page;
|
||||
function StrukturOrganisasiBumDes() {
|
||||
const stateOrganisasi: any = useProxy(stateStrukturBumDes.pegawai)
|
||||
|
||||
useEffect(() => {
|
||||
void stateOrganisasi.findMany.load()
|
||||
}, [])
|
||||
|
||||
const isLoading =
|
||||
!stateOrganisasi.findMany.data &&
|
||||
stateOrganisasi.findMany.loading !== false
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Center py={48}>
|
||||
<Stack align="center" gap="sm">
|
||||
<Loader size="lg" />
|
||||
<Text fw={600}>Memuat struktur organisasi…</Text>
|
||||
<Text c="dimmed" size="sm">
|
||||
Mengambil data pegawai dan posisi. Mohon tunggu sebentar.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
!stateOrganisasi.findMany.data ||
|
||||
stateOrganisasi.findMany.data.length === 0
|
||||
) {
|
||||
return (
|
||||
<Center py={40}>
|
||||
<Stack align="center" gap="md">
|
||||
<Paper
|
||||
radius="md"
|
||||
p="xl"
|
||||
style={{
|
||||
width: 560,
|
||||
background: 'rgba(28,110,164,0.2)',
|
||||
border: `1px solid rgba(255,255,255,0.1)`,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<Center>
|
||||
<IconUsers size={56} />
|
||||
</Center>
|
||||
<Title order={3} mt="md">
|
||||
Data pegawai belum tersedia
|
||||
</Title>
|
||||
<Text c="dimmed" mt="xs">
|
||||
Belum ada data pegawai yang tercatat untuk BumDes. Silakan coba
|
||||
muat ulang atau periksa sumber data.
|
||||
</Text>
|
||||
<Group justify="center" mt="lg">
|
||||
<Button
|
||||
leftSection={<IconRefresh size={16} />}
|
||||
variant="gradient"
|
||||
gradient={{ from: 'indigo', to: 'cyan' }}
|
||||
onClick={() => stateOrganisasi.findMany.load()}
|
||||
>
|
||||
Muat Ulang
|
||||
</Button>
|
||||
<Button
|
||||
leftSection={<IconSearch size={16} />}
|
||||
variant="subtle"
|
||||
onClick={() =>
|
||||
stateOrganisasi.findMany.load({ query: { q: '' } })
|
||||
}
|
||||
>
|
||||
Cari Pegawai
|
||||
</Button>
|
||||
</Group>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Center>
|
||||
)
|
||||
}
|
||||
|
||||
const posisiMap = new Map<string, any>()
|
||||
|
||||
const aktifPegawai = stateOrganisasi.findMany.data.filter((p: any) => p.isActive);
|
||||
|
||||
for (const pegawai of aktifPegawai) {
|
||||
const posisiId = pegawai.posisi.id;
|
||||
if (!posisiMap.has(posisiId)) {
|
||||
posisiMap.set(posisiId, {
|
||||
...pegawai.posisi,
|
||||
pegawaiList: [],
|
||||
children: [],
|
||||
});
|
||||
}
|
||||
posisiMap.get(posisiId)!.pegawaiList.push(pegawai);
|
||||
}
|
||||
|
||||
// First, create a map of all unique positions
|
||||
const allPositions = new Map();
|
||||
aktifPegawai.forEach((pegawai: any) => {
|
||||
if (!allPositions.has(pegawai.posisi.id)) {
|
||||
allPositions.set(pegawai.posisi.id, {
|
||||
...pegawai.posisi,
|
||||
pegawaiList: [],
|
||||
children: []
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Then assign employees to their positions
|
||||
aktifPegawai.forEach((pegawai: any) => {
|
||||
const posisi = allPositions.get(pegawai.posisi.id);
|
||||
if (posisi) {
|
||||
posisi.pegawaiList.push(pegawai);
|
||||
}
|
||||
});
|
||||
|
||||
// Now build the hierarchy
|
||||
const root = [];
|
||||
for (const [_, posisi] of allPositions) {
|
||||
if (posisi.parentId) {
|
||||
const parent = allPositions.get(posisi.parentId);
|
||||
if (parent) {
|
||||
parent.children.push(posisi);
|
||||
} else {
|
||||
// Only add to root if it's a top-level position
|
||||
if (!posisi.parentId) {
|
||||
root.push(posisi);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
root.push(posisi);
|
||||
}
|
||||
}
|
||||
function toOrgChartFormat(node: any): any {
|
||||
return {
|
||||
expanded: true,
|
||||
type: 'person',
|
||||
styleClass: 'p-person',
|
||||
data: {
|
||||
name: node.pegawaiList?.[0]?.namaLengkap || 'Belum ditugaskan',
|
||||
title: node.nama || 'Tanpa jabatan',
|
||||
image: node.pegawaiList?.[0]?.image?.link || '/img/default.png',
|
||||
description: node.deskripsi || '',
|
||||
positionId: node.id || null,
|
||||
},
|
||||
children: node.children?.map(toOrgChartFormat) || [],
|
||||
}
|
||||
}
|
||||
|
||||
const chartData = root.map(toOrgChartFormat)
|
||||
|
||||
return (
|
||||
<Box py={16} >
|
||||
<Paper
|
||||
radius="md"
|
||||
p="md"
|
||||
style={{
|
||||
background: 'rgba(28,110,164,0.2)',
|
||||
border: `1px solid rgba(255,255,255,0.1)`,
|
||||
overflowX: 'auto',
|
||||
}}
|
||||
>
|
||||
<OrganizationChart
|
||||
value={chartData}
|
||||
nodeTemplate={nodeTemplate}
|
||||
/>
|
||||
</Paper>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function nodeTemplate(node: any) {
|
||||
const imageSrc = node?.data?.image || '/img/default.png'
|
||||
const name = node?.data?.name || 'Tanpa Nama'
|
||||
const title = node?.data?.title || 'Tanpa Jabatan'
|
||||
const description = node?.data?.description || ''
|
||||
|
||||
return (
|
||||
<Transition mounted transition="pop" duration={240}>
|
||||
{(styles) => (
|
||||
<Card
|
||||
radius="lg"
|
||||
withBorder
|
||||
style={{
|
||||
...styles,
|
||||
width: 260,
|
||||
padding: 16,
|
||||
background: 'rgba(28,110,164,0.3)',
|
||||
borderColor: 'rgba(255,255,255,0.15)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={imageSrc}
|
||||
alt={name}
|
||||
radius="md"
|
||||
width={120}
|
||||
height={120}
|
||||
fit="cover"
|
||||
style={{
|
||||
objectFit: 'cover',
|
||||
border: '2px solid rgba(255,255,255,0.2)',
|
||||
marginBottom: 12,
|
||||
}}
|
||||
loading='lazy'
|
||||
/>
|
||||
<Text fw={700}>{name}</Text>
|
||||
<Text size="sm" c="dimmed" mt={4}>
|
||||
{title}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" mt={8} lineClamp={3}>
|
||||
{description || 'Belum ada deskripsi.'}
|
||||
</Text>
|
||||
<Tooltip label="Kembali ke struktur organisasi" withArrow position="bottom">
|
||||
<Button
|
||||
variant="light"
|
||||
size="xs"
|
||||
mt="md"
|
||||
onClick={() => {
|
||||
const id = node?.data?.positionId
|
||||
if (id && (window as any).scrollTo) {
|
||||
;(window as any).scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
}}
|
||||
>
|
||||
Kembali
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Card>
|
||||
)}
|
||||
</Transition>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ function Page() {
|
||||
|
||||
useShallowEffect(() => {
|
||||
mitraState.findMany.load(page, 10);
|
||||
load(page, 10, search, selectedYear || '');
|
||||
load(page, 10, search, selectedYear ? `year:${selectedYear}` : '');
|
||||
}, [page, search, selectedYear]);
|
||||
|
||||
const mitraData = mitraState.findMany.data || [];
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import colors from '@/con/colors';
|
||||
import { BarChart as MantineBarChart } from '@mantine/charts';
|
||||
import { Box, Center, ColorSwatch, Flex, Paper, SimpleGrid, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { Box, Center, ColorSwatch, Flex, Paper, SimpleGrid, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||
|
||||
@@ -107,20 +107,7 @@ function Page() {
|
||||
<Box>
|
||||
<Paper p={"xl"} bg={colors['white-trans-1']}>
|
||||
<Box pb={30}>
|
||||
<Flex pb={30} justify={'flex-end'} gap={'xl'} align={'center'}>
|
||||
<Box>
|
||||
<Flex gap={{ base: 0, md: 5 }} align={'center'}>
|
||||
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Angka Kematian</Text>
|
||||
<ColorSwatch color="#EF3E3E" size={30} />
|
||||
</Flex>
|
||||
</Box>
|
||||
<Box>
|
||||
<Flex gap={{ base: 0, md: 5 }} align={'center'}>
|
||||
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Angka Kelahiran</Text>
|
||||
<ColorSwatch color="#3290CA" size={30} />
|
||||
</Flex>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Title order={2} mb="md">Data Kematian dan Kelahiran</Title>
|
||||
{chartData.length === 0 ? (
|
||||
<Text c="dimmed" ta="center" py="xl">
|
||||
Belum ada data yang tersedia untuk ditampilkan
|
||||
@@ -150,6 +137,20 @@ function Page() {
|
||||
</Center>
|
||||
</>
|
||||
)}
|
||||
<Flex pb={30} justify={'center'} gap={'xl'} align={'center'}>
|
||||
<Box>
|
||||
<Flex gap={{ base: 0, md: 5 }} align={'center'}>
|
||||
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Angka Kematian</Text>
|
||||
<ColorSwatch color="#EF3E3E" size={30} />
|
||||
</Flex>
|
||||
</Box>
|
||||
<Box>
|
||||
<Flex gap={{ base: 0, md: 5 }} align={'center'}>
|
||||
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Angka Kelahiran</Text>
|
||||
<ColorSwatch color="#3290CA" size={30} />
|
||||
</Flex>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
@@ -163,11 +164,11 @@ function Page() {
|
||||
}}
|
||||
>
|
||||
{/* Fasilitas Kesehatan */}
|
||||
<FasilitasKesehatan/>
|
||||
<FasilitasKesehatan />
|
||||
{/* Jadwal Kegiatan */}
|
||||
<JadwalKegiatan/>
|
||||
<JadwalKegiatan />
|
||||
{/* Artikel Kesehatan */}
|
||||
<ArtikelKesehatanPage/>
|
||||
<ArtikelKesehatanPage />
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
@@ -121,13 +121,13 @@ function Page() {
|
||||
</Badge>
|
||||
</Group>
|
||||
<Text fz="sm" c="dimmed">
|
||||
Diposting: 12 Februari 2025 · Dinas Kesehatan
|
||||
Diposting: {v.createdAt.toLocaleDateString()}
|
||||
</Text>
|
||||
<Divider />
|
||||
<Text fz="sm" lh={1.5}>
|
||||
<Text fz="sm" lh={1.5} lineClamp={3} truncate="end">
|
||||
{v.deskripsiSingkat}
|
||||
</Text>
|
||||
<Button variant="light" radius="md" size="md" onClick={() => router.push(`/admin/kesehatan/info-wabah-penyakit/${v.id}`)}>
|
||||
<Button variant="light" radius="md" size="md" onClick={() => router.push(`/darmasaba/kesehatan/info-wabah-penyakit/${v.id}`)}>
|
||||
Selengkapnya
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
@@ -97,18 +97,23 @@ function Page() {
|
||||
shadow="sm"
|
||||
withBorder
|
||||
bg={colors['white-trans-1']}
|
||||
style={{ transition: 'all 0.3s ease' }}
|
||||
style={{
|
||||
transition: 'all 0.3s ease',
|
||||
transform: 'translateY(0)',
|
||||
}}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.transform = 'translateY(-5px)')}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.transform = 'translateY(0)')}
|
||||
>
|
||||
<Stack align="center" gap="md">
|
||||
<Center>
|
||||
<Image
|
||||
src={v.image.link}
|
||||
alt={v.name}
|
||||
w={160}
|
||||
h={160}
|
||||
fit="contain"
|
||||
h={180}
|
||||
w="100%"
|
||||
radius="md"
|
||||
loading="lazy"
|
||||
fit="cover"
|
||||
style={{ aspectRatio: '4/3' }}
|
||||
/>
|
||||
</Center>
|
||||
<Stack gap={4} w="100%">
|
||||
@@ -151,8 +156,11 @@ function Page() {
|
||||
styles={{
|
||||
control: {
|
||||
border: `1px solid ${colors['blue-button']}`,
|
||||
transition: 'all 0.3s ease',
|
||||
'&:hover': { backgroundColor: colors['blue-button'], color: 'white' },
|
||||
},
|
||||
}}
|
||||
|
||||
/>
|
||||
</Center>
|
||||
|
||||
|
||||
@@ -28,11 +28,31 @@ export default function Page() {
|
||||
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<Box py="xl" px={{ base: "md", md: 100 }}>
|
||||
<Text fz="lg" fw="bold" c={colors["blue-button"]}>
|
||||
Tidak ada posyandu yang ditemukan
|
||||
</Text>
|
||||
</Box>
|
||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap="xl">
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<BackButton />
|
||||
<Flex mt="md" justify="space-between" align="center" wrap="wrap" gap="md">
|
||||
<Text
|
||||
ta="left"
|
||||
fz={{ base: "1.8rem", md: "2.5rem" }}
|
||||
c={colors["blue-button"]}
|
||||
fw="bold"
|
||||
>
|
||||
Posyandu Desa Darmasaba
|
||||
</Text>
|
||||
<TextInput
|
||||
placeholder="Cari posyandu berdasarkan nama..."
|
||||
aria-label="Pencarian Posyandu"
|
||||
radius="xl"
|
||||
size="md"
|
||||
leftSection={<IconSearch size={20} />}
|
||||
w={{ base: "100%", md: "35%" }}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,48 +1,31 @@
|
||||
'use client'
|
||||
import stateEdukasiLingkungan from '@/app/admin/(dashboard)/_state/lingkungan/edukasi-lingkungan';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, List, ListItem, Paper, SimpleGrid, Stack, Text, Tooltip } from '@mantine/core';
|
||||
import { Box, Paper, SimpleGrid, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconLeaf, IconPlant2, IconRecycle } from '@tabler/icons-react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||
|
||||
const data = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Tujuan Edukasi Lingkungan',
|
||||
icon: <IconLeaf size={28} color={colors['blue-button']} />,
|
||||
listDeskripsi: [
|
||||
'Meningkatkan kesadaran masyarakat akan pentingnya lingkungan bersih dan sehat',
|
||||
'Mendorong partisipasi warga dalam pengelolaan sampah, penghijauan, dan konservasi',
|
||||
'Mengurangi dampak negatif kegiatan manusia terhadap lingkungan',
|
||||
'Membentuk generasi muda peduli isu-isu lingkungan',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Materi Edukasi yang Diberikan',
|
||||
icon: <IconRecycle size={28} color={colors['blue-button']} />,
|
||||
listDeskripsi: [
|
||||
'Pengelolaan sampah: pilah organik & anorganik',
|
||||
'Pencegahan pencemaran lingkungan (air, udara, tanah)',
|
||||
'Pemanfaatan lahan hijau dan penghijauan desa',
|
||||
'Daur ulang dan kreativitas dari sampah',
|
||||
'Bahaya pembakaran sampah sembarangan',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Contoh Kegiatan di Desa Darmasaba',
|
||||
icon: <IconPlant2 size={28} color={colors['blue-button']} />,
|
||||
listDeskripsi: [
|
||||
'Pelatihan membuat kompos dari sampah rumah tangga',
|
||||
'Gerakan "Jumat Bersih" rutin',
|
||||
'Workshop pembuatan ecobrick',
|
||||
'Lomba kebersihan antar banjar',
|
||||
'Sosialisasi lingkungan di sekolah dan posyandu',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
function Page() {
|
||||
const tujuan = useProxy(stateEdukasiLingkungan.stateTujuanEdukasi.findById)
|
||||
const materi = useProxy(stateEdukasiLingkungan.stateMateriEdukasiLingkungan.findById)
|
||||
const contoh = useProxy(stateEdukasiLingkungan.stateContohEdukasiLingkungan.findById)
|
||||
|
||||
useShallowEffect(() => {
|
||||
tujuan.load('edit')
|
||||
materi.load('edit')
|
||||
contoh.load('edit')
|
||||
}, [])
|
||||
|
||||
if (tujuan.loading || !tujuan.data || materi.loading || !materi.data || contoh.loading || !contoh.data) {
|
||||
return (
|
||||
<Stack py={20}>
|
||||
<Skeleton radius="md" height={600} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
@@ -60,28 +43,84 @@ function Page() {
|
||||
</Box>
|
||||
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg">
|
||||
{data.map((item) => (
|
||||
<Paper key={item.id} p={20} bg={colors['white-trans-1']} shadow="md" radius="md">
|
||||
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg" style={{ alignItems: 'stretch' }}>
|
||||
{/* Tujuan Edukasi Lingkungan */}
|
||||
<Box style={{ display: 'flex', height: '100%' }}>
|
||||
<Paper p={20} bg={colors['white-trans-1']} shadow="md" radius="md" style={{ width: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<Stack gap="md">
|
||||
<Box>
|
||||
<Tooltip label={item.title} position="top" withArrow>
|
||||
<Tooltip label={tujuan.data?.judul} position="top" withArrow>
|
||||
<Stack gap={4} align="center">
|
||||
{item.icon}
|
||||
<IconLeaf size={28} color={colors['blue-button']} />
|
||||
<Text fz="h3" fw="bold" c={colors['blue-button']} ta="center">
|
||||
{item.title}
|
||||
{tujuan.data?.judul}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<List fz="h4" spacing="sm" withPadding>
|
||||
{item.listDeskripsi.map((desc, idx) => (
|
||||
<ListItem key={idx}>{desc}</ListItem>
|
||||
))}
|
||||
</List>
|
||||
<Text
|
||||
style={{
|
||||
wordBreak: "break-word",
|
||||
whiteSpace: "normal",
|
||||
flexGrow: 1
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: tujuan.data?.deskripsi || '' }}
|
||||
/>
|
||||
<Box style={{ flexGrow: 1 }} />
|
||||
</Stack>
|
||||
</Paper>
|
||||
))}
|
||||
</Box>
|
||||
{/* Materi Edukasi Lingkungan */}
|
||||
<Box style={{ display: 'flex', height: '100%' }}>
|
||||
<Paper p={20} bg={colors['white-trans-1']} shadow="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<Box>
|
||||
<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>
|
||||
</Stack>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Text
|
||||
style={{
|
||||
wordBreak: "break-word",
|
||||
whiteSpace: "normal",
|
||||
flexGrow: 1
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: materi.data?.deskripsi || '' }}
|
||||
/>
|
||||
<Box style={{ flexGrow: 1 }} />
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
{/* Contoh Edukasi Lingkungan */}
|
||||
<Box style={{ display: 'flex', height: '100%' }}>
|
||||
<Paper p={20} bg={colors['white-trans-1']} shadow="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<Box>
|
||||
<Tooltip label={contoh.data?.judul} position="top" withArrow>
|
||||
<Stack gap={4} align="center">
|
||||
<IconPlant2 size={28} color={colors['blue-button']} />
|
||||
<Text fz="h3" fw="bold" c={colors['blue-button']} ta="center">
|
||||
{contoh.data?.judul}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Text
|
||||
style={{
|
||||
wordBreak: "break-word",
|
||||
whiteSpace: "normal",
|
||||
flexGrow: 1
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: contoh.data?.deskripsi || '' }}
|
||||
/>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
@@ -75,7 +75,7 @@ export default function Content({ kategori }: { kategori: string }) {
|
||||
{featured.kategoriKegiatan?.nama || kategori}
|
||||
</Badge>
|
||||
<Title order={2} mb="md">{featured.judul}</Title>
|
||||
<Text color="dimmed" lineClamp={3} mb="md">{featured.deskripsiLengkap}</Text>
|
||||
<Text c="dimmed" lineClamp={3} mb="md" dangerouslySetInnerHTML={{ __html: featured.deskripsiLengkap }} />
|
||||
</div>
|
||||
<Group justify="apart" mt="auto">
|
||||
<Group gap="xs">
|
||||
@@ -135,9 +135,9 @@ export default function Content({ kategori }: { kategori: string }) {
|
||||
{item.kategoriKegiatan?.nama || kategori}
|
||||
</Badge>
|
||||
<Text fw={600} size="lg" mt="sm" lineClamp={2}>{item.judul}</Text>
|
||||
<Text size="sm" color="dimmed" lineClamp={3} style={{wordBreak: "break-word", whiteSpace: "normal"}} mt="xs" dangerouslySetInnerHTML={{ __html: item.deskripsiLengkap }} />
|
||||
<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" color="dimmed">
|
||||
<Text size="xs" c="dimmed">
|
||||
{new Date(item.createdAt).toLocaleDateString('id-ID', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// src/app/darmasaba/(pages)/desa/berita/[kategori]/page.tsx
|
||||
import { Suspense } from "react";
|
||||
import Content from "./content";
|
||||
|
||||
|
||||
@@ -1,113 +1,391 @@
|
||||
// 'use client'
|
||||
// import colors from '@/con/colors';
|
||||
// import { Box, Container, Grid, GridCol, Stack, Tabs, TabsList, TabsTab, Text, TextInput } from '@mantine/core';
|
||||
// import { IconSearch } from '@tabler/icons-react';
|
||||
// import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||||
// import React, { useEffect, useState } from 'react';
|
||||
// import BackButton from '../../../desa/layanan/_com/BackButto';
|
||||
|
||||
|
||||
// type HeaderSearchProps = {
|
||||
// placeholder?: string;
|
||||
// searchIcon?: React.ReactNode;
|
||||
// value?: string;
|
||||
// onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
// children?: React.ReactNode;
|
||||
// };
|
||||
|
||||
// function LayoutTabsGotongRoyong({
|
||||
// children,
|
||||
// placeholder = "pencarian",
|
||||
// searchIcon = <IconSearch size={20} />
|
||||
// }: HeaderSearchProps) {
|
||||
// const router = useRouter();
|
||||
// const pathname = usePathname();
|
||||
// const searchParams = useSearchParams();
|
||||
|
||||
// // Get active tab from URL path
|
||||
// const activeTab = pathname.split('/').pop() || 'semua';
|
||||
|
||||
// // Get initial search value from URL
|
||||
// const initialSearch = searchParams.get('search') || '';
|
||||
// const [searchValue, setSearchValue] = useState(initialSearch);
|
||||
// const [searchTimeout, setSearchTimeout] = useState<number | null>(null);
|
||||
|
||||
// // Update active tab state when pathname changes
|
||||
// const [activeTabState, setActiveTabState] = useState(activeTab);
|
||||
// useEffect(() => {
|
||||
// setActiveTabState(activeTab);
|
||||
// }, [activeTab]);
|
||||
|
||||
// // Clean up timeouts on unmount
|
||||
// useEffect(() => {
|
||||
// return () => {
|
||||
// if (searchTimeout !== null) {
|
||||
// clearTimeout(searchTimeout);
|
||||
// }
|
||||
// };
|
||||
// }, [searchTimeout]);
|
||||
|
||||
// // Handle search input change with debounce
|
||||
// const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
// const value = event.target.value;
|
||||
// setSearchValue(value);
|
||||
|
||||
// // Clear previous timeout
|
||||
// if (searchTimeout !== null) {
|
||||
// clearTimeout(searchTimeout);
|
||||
// }
|
||||
|
||||
// // Set new timeout
|
||||
// const newTimeout = window.setTimeout(() => {
|
||||
// const params = new URLSearchParams(searchParams.toString());
|
||||
|
||||
// if (value) {
|
||||
// params.set('search', value);
|
||||
// } else {
|
||||
// params.delete('search');
|
||||
// }
|
||||
|
||||
// // Only update URL if the search value has actually changed
|
||||
// if (params.toString() !== searchParams.toString()) {
|
||||
// router.push(`/darmasaba/lingkungan/gotong-royong/${activeTab}?${params.toString()}`);
|
||||
// }
|
||||
// }, 500); // 500ms debounce delay
|
||||
|
||||
// setSearchTimeout(newTimeout);
|
||||
// };
|
||||
// const tabs = [
|
||||
// {
|
||||
// label: "Semua",
|
||||
// value: "semua",
|
||||
// href: "/darmasaba/lingkungan/gotong-royong/semua"
|
||||
// },
|
||||
// {
|
||||
// label: "Kebersihan",
|
||||
// value: "kebersihan",
|
||||
// href: "/darmasaba/lingkungan/gotong-royong/kebersihan"
|
||||
// },
|
||||
// {
|
||||
// label: "Infrastruktur",
|
||||
// value: "infrastruktur",
|
||||
// href: "/darmasaba/lingkungan/gotong-royong/infrastruktur"
|
||||
// },
|
||||
// {
|
||||
// label: "Sosial",
|
||||
// value: "sosial",
|
||||
// href: "/darmasaba/lingkungan/gotong-royong/sosial"
|
||||
// },
|
||||
// {
|
||||
// label: "Lingkungan",
|
||||
// value: "lingkungan",
|
||||
// href: "/darmasaba/lingkungan/gotong-royong/lingkungan"
|
||||
// }
|
||||
// ];
|
||||
// const handleTabChange = (value: string | null) => {
|
||||
// if (!value) return;
|
||||
// const tab = tabs.find(t => t.value === value);
|
||||
// if (tab) {
|
||||
// const params = new URLSearchParams(searchParams.toString());
|
||||
// router.push(`/darmasaba/lingkungan/gotong-royong/${value}${params.toString() ? `?${params.toString()}` : ''}`);
|
||||
// }
|
||||
// };
|
||||
|
||||
// return (
|
||||
// <Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
|
||||
// {/* Header */}
|
||||
// <Box px={{ base: "md", md: 100 }}>
|
||||
// <BackButton />
|
||||
// </Box>
|
||||
// <Container size="lg" px="md">
|
||||
// <Stack align="center" gap="0" >
|
||||
// <Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" ta="center">
|
||||
// Gotong Royong Desa Darmasaba
|
||||
// </Text>
|
||||
// <Text ta="center" px="md">
|
||||
// Gotong royong rutin dilakukan oleh warga desa untuk meningkatkan kualitas hidup dan kesejahteraan masyarakat Desa Darmasaba
|
||||
// </Text>
|
||||
// </Stack>
|
||||
// </Container>
|
||||
|
||||
// <Tabs
|
||||
// color={colors['blue-button']}
|
||||
// variant="pills"
|
||||
// value={activeTabState}
|
||||
// onChange={handleTabChange}
|
||||
// >
|
||||
// <Box px={{ base: "md", md: 100 }} py="md" bg={colors['BG-trans']}>
|
||||
// <Grid>
|
||||
// <GridCol span={{ base: 12, md: 9, lg: 8, xl: 9 }}>
|
||||
// <TabsList>
|
||||
// {tabs.map((tab, index) => (
|
||||
// <TabsTab
|
||||
// key={index}
|
||||
// value={tab.value}
|
||||
// onClick={() => router.push(tab.href)}
|
||||
// >
|
||||
// {tab.label}
|
||||
// </TabsTab>
|
||||
// ))}
|
||||
// </TabsList>
|
||||
// </GridCol>
|
||||
// <GridCol span={{ base: 12, md: 3, lg: 4, xl: 3 }}>
|
||||
// <TextInput
|
||||
// radius="lg"
|
||||
// placeholder={placeholder}
|
||||
// leftSection={searchIcon}
|
||||
// w="100%"
|
||||
// value={searchValue}
|
||||
// onChange={handleSearchChange}
|
||||
// />
|
||||
// </GridCol>
|
||||
// </Grid>
|
||||
// </Box>
|
||||
|
||||
// {children}
|
||||
// </Tabs>
|
||||
// </Stack>
|
||||
// );
|
||||
// }
|
||||
|
||||
// export default LayoutTabsGotongRoyong;
|
||||
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
// 'use client'
|
||||
// import colors from '@/con/colors';
|
||||
// import { Box, Group, Stack, Tabs, TabsList, TabsTab, Text, TextInput } from '@mantine/core';
|
||||
// import { IconSearch } from '@tabler/icons-react';
|
||||
// import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||||
// import React, { useEffect, useState } from 'react';
|
||||
// import BackButton from '../../layanan/_com/BackButto';
|
||||
|
||||
// type HeaderSearchProps = {
|
||||
// placeholder?: string;
|
||||
// searchIcon?: React.ReactNode;
|
||||
// value?: string;
|
||||
// onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
// children?: React.ReactNode;
|
||||
// };
|
||||
|
||||
// function LayoutTabsBerita({
|
||||
// children,
|
||||
// placeholder = "pencarian",
|
||||
// searchIcon = <IconSearch size={20} />
|
||||
// }: HeaderSearchProps) {
|
||||
// const router = useRouter();
|
||||
// const pathname = usePathname();
|
||||
// const searchParams = useSearchParams();
|
||||
|
||||
// const activeTab = pathname.split('/').pop() || 'semua';
|
||||
// const initialSearch = searchParams.get('search') || '';
|
||||
// const [searchValue, setSearchValue] = useState(initialSearch);
|
||||
// const [searchTimeout, setSearchTimeout] = useState<number | null>(null);
|
||||
|
||||
// const [activeTabState, setActiveTabState] = useState(activeTab);
|
||||
// useEffect(() => {
|
||||
// setActiveTabState(activeTab);
|
||||
// }, [activeTab]);
|
||||
|
||||
// useEffect(() => {
|
||||
// return () => {
|
||||
// if (searchTimeout !== null) clearTimeout(searchTimeout);
|
||||
// };
|
||||
// }, [searchTimeout]);
|
||||
|
||||
// const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
// const value = event.target.value;
|
||||
// setSearchValue(value);
|
||||
|
||||
// if (searchTimeout !== null) clearTimeout(searchTimeout);
|
||||
|
||||
// const newTimeout = window.setTimeout(() => {
|
||||
// const params = new URLSearchParams(searchParams.toString());
|
||||
|
||||
// if (value) params.set('search', value);
|
||||
// else params.delete('search');
|
||||
|
||||
// if (params.toString() !== searchParams.toString()) {
|
||||
// router.push(`/darmasaba/desa/berita/${activeTab}?${params.toString()}`);
|
||||
// }
|
||||
// }, 500);
|
||||
|
||||
// setSearchTimeout(newTimeout);
|
||||
// };
|
||||
|
||||
// const tabs = [
|
||||
// { label: "Semua", value: "semua", href: "/darmasaba/desa/berita/semua" },
|
||||
// { label: "Budaya", value: "budaya", href: "/darmasaba/desa/berita/budaya" },
|
||||
// { label: "Pemerintahan", value: "pemerintahan", href: "/darmasaba/desa/berita/pemerintahan" },
|
||||
// { label: "Ekonomi", value: "ekonomi", href: "/darmasaba/desa/berita/ekonomi" },
|
||||
// { label: "Pembangunan", value: "pembangunan", href: "/darmasaba/desa/berita/pembangunan" },
|
||||
// { label: "Sosial", value: "sosial", href: "/darmasaba/desa/berita/sosial" },
|
||||
// { label: "Teknologi", value: "teknologi", href: "/darmasaba/desa/berita/teknologi" },
|
||||
// ];
|
||||
|
||||
// const handleTabChange = (value: string | null) => {
|
||||
// if (!value) return;
|
||||
// const tab = tabs.find(t => t.value === value);
|
||||
// if (tab) {
|
||||
// const params = new URLSearchParams(searchParams.toString());
|
||||
// router.push(`/darmasaba/desa/berita/${value}${params.toString() ? `?${params.toString()}` : ''}`);
|
||||
// }
|
||||
// };
|
||||
|
||||
// return (
|
||||
// <Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
|
||||
// {/* Header */}
|
||||
// <Box px={{ base: "md", md: 100 }}>
|
||||
// <BackButton />
|
||||
// </Box>
|
||||
|
||||
// <Box px={{ base: 'md', md: 100 }}>
|
||||
// <Group justify='space-between' align="center">
|
||||
// <Stack gap="0">
|
||||
// <Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" >
|
||||
// Portal Berita Darmasaba
|
||||
// </Text>
|
||||
// <Text>
|
||||
// Temukan berbagai potensi dan keunggulan yang dimiliki Desa Darmasaba
|
||||
// </Text>
|
||||
// </Stack>
|
||||
// <Box>
|
||||
// <TextInput
|
||||
// radius="lg"
|
||||
// placeholder={placeholder}
|
||||
// leftSection={searchIcon}
|
||||
// w="100%"
|
||||
// value={searchValue}
|
||||
// onChange={handleSearchChange}
|
||||
// />
|
||||
// </Box>
|
||||
// </Group>
|
||||
// </Box>
|
||||
|
||||
// <Tabs
|
||||
// color={colors['blue-button']}
|
||||
// variant="pills"
|
||||
// value={activeTabState}
|
||||
// onChange={handleTabChange}
|
||||
// >
|
||||
// <Box px={{ base: "md", md: 100 }} py="md" bg={colors['BG-trans']}>
|
||||
// {/* SCROLLABLE TABS */}
|
||||
// <Box style={{ overflowX: 'auto', whiteSpace: 'nowrap' }}>
|
||||
// <TabsList style={{ display: 'flex', flexWrap: 'nowrap', gap: '0.5rem' }}>
|
||||
// {tabs.map((tab, index) => (
|
||||
// <TabsTab
|
||||
// key={index}
|
||||
// value={tab.value}
|
||||
// onClick={() => router.push(tab.href)}
|
||||
// style={{
|
||||
// flex: '0 0 auto', // Prevent shrinking
|
||||
// minWidth: 100, // optional: makes them touch-friendly
|
||||
// textAlign: 'center'
|
||||
// }}
|
||||
// >
|
||||
// {tab.label}
|
||||
// </TabsTab>
|
||||
// ))}
|
||||
// </TabsList>
|
||||
// </Box>
|
||||
// </Box>
|
||||
|
||||
// {children}
|
||||
// </Tabs>
|
||||
// </Stack>
|
||||
// );
|
||||
// }
|
||||
|
||||
// export default LayoutTabsBerita;
|
||||
|
||||
|
||||
'use client'
|
||||
import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Container, Grid, GridCol, Stack, Tabs, TabsList, TabsTab, Text, TextInput } from '@mantine/core';
|
||||
import { Box, Group, Stack, Tabs, TabsList, TabsTab, Text, TextInput } from '@mantine/core';
|
||||
import { IconSearch } from '@tabler/icons-react';
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import BackButton from '../../../desa/layanan/_com/BackButto';
|
||||
|
||||
|
||||
type HeaderSearchProps = {
|
||||
placeholder?: string;
|
||||
searchIcon?: React.ReactNode;
|
||||
value?: string;
|
||||
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
function LayoutTabsGotongRoyong({
|
||||
children,
|
||||
placeholder = "pencarian",
|
||||
searchIcon = <IconSearch size={20} />
|
||||
}: HeaderSearchProps) {
|
||||
function LayoutTabsGotongRoyong({ children }: { children: React.ReactNode }) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
// Get active tab from URL path
|
||||
|
||||
const kategoriState = useProxy(gotongRoyongState.kategoriKegiatan);
|
||||
|
||||
// tab aktif dari url
|
||||
const activeTab = pathname.split('/').pop() || 'semua';
|
||||
|
||||
// Get initial search value from URL
|
||||
const initialSearch = searchParams.get('search') || '';
|
||||
const [searchValue, setSearchValue] = useState(initialSearch);
|
||||
const [searchTimeout, setSearchTimeout] = useState<number | null>(null);
|
||||
|
||||
// Update active tab state when pathname changes
|
||||
const [activeTabState, setActiveTabState] = useState(activeTab);
|
||||
|
||||
useEffect(() => {
|
||||
kategoriState.findMany.load(); // ambil kategori dari DB
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveTabState(activeTab);
|
||||
}, [activeTab]);
|
||||
|
||||
// Clean up timeouts on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (searchTimeout !== null) {
|
||||
clearTimeout(searchTimeout);
|
||||
}
|
||||
};
|
||||
}, [searchTimeout]);
|
||||
// search
|
||||
const initialSearch = searchParams.get('search') || '';
|
||||
const [searchValue, setSearchValue] = useState(initialSearch);
|
||||
const [searchTimeout, setSearchTimeout] = useState<number | null>(null);
|
||||
|
||||
// Handle search input change with debounce
|
||||
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.target.value;
|
||||
setSearchValue(value);
|
||||
|
||||
// Clear previous timeout
|
||||
if (searchTimeout !== null) {
|
||||
clearTimeout(searchTimeout);
|
||||
}
|
||||
|
||||
// Set new timeout
|
||||
|
||||
if (searchTimeout !== null) clearTimeout(searchTimeout);
|
||||
|
||||
const newTimeout = window.setTimeout(() => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
|
||||
if (value) {
|
||||
params.set('search', value);
|
||||
} else {
|
||||
params.delete('search');
|
||||
}
|
||||
|
||||
// Only update URL if the search value has actually changed
|
||||
if (params.toString() !== searchParams.toString()) {
|
||||
router.push(`/darmasaba/lingkungan/gotong-royong/${activeTab}?${params.toString()}`);
|
||||
}
|
||||
}, 500); // 500ms debounce delay
|
||||
|
||||
if (value) params.set('search', value);
|
||||
else params.delete('search');
|
||||
|
||||
router.push(`/darmasaba/lingkungan/gotong-royong/${activeTab}${params.toString() ? `?${params.toString()}` : ''}`);
|
||||
}, 500);
|
||||
|
||||
setSearchTimeout(newTimeout);
|
||||
};
|
||||
|
||||
// --- tabs dinamis ---
|
||||
const tabs = [
|
||||
{
|
||||
label: "Semua",
|
||||
value: "semua",
|
||||
href: "/darmasaba/lingkungan/gotong-royong/semua"
|
||||
},
|
||||
{
|
||||
label: "Kebersihan",
|
||||
value: "kebersihan",
|
||||
href: "/darmasaba/lingkungan/gotong-royong/kebersihan"
|
||||
},
|
||||
{
|
||||
label: "Infrastruktur",
|
||||
value: "infrastruktur",
|
||||
href: "/darmasaba/lingkungan/gotong-royong/infrastruktur"
|
||||
},
|
||||
{
|
||||
label: "Sosial",
|
||||
value: "sosial",
|
||||
href: "/darmasaba/lingkungan/gotong-royong/sosial"
|
||||
},
|
||||
{
|
||||
label: "Lingkungan",
|
||||
value: "lingkungan",
|
||||
href: "/darmasaba/lingkungan/gotong-royong/lingkungan"
|
||||
}
|
||||
{ label: "Semua", value: "semua", href: "/darmasaba/lingkungan/gotong-royong/semua" },
|
||||
...(kategoriState.findMany.data || []).map((kat: any) => ({
|
||||
label: kat.nama,
|
||||
value: kat.nama.toLowerCase(),
|
||||
href: `/darmasaba/lingkungan/gotong-royong/${kat.nama.toLowerCase()}`
|
||||
}))
|
||||
];
|
||||
|
||||
const handleTabChange = (value: string | null) => {
|
||||
if (!value) return;
|
||||
const tab = tabs.find(t => t.value === value);
|
||||
if (tab) {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
router.push(`/darmasaba/lingkungan/gotong-royong/${value}${params.toString() ? `?${params.toString()}` : ''}`);
|
||||
router.push(`${tab.href}${params.toString() ? `?${params.toString()}` : ''}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -117,17 +395,29 @@ function LayoutTabsGotongRoyong({
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
<Container size="lg" px="md">
|
||||
<Stack align="center" gap="0" >
|
||||
<Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" ta="center">
|
||||
Gotong Royong Desa Darmasaba
|
||||
</Text>
|
||||
<Text ta="center" px="md">
|
||||
Gotong royong rutin dilakukan oleh warga desa untuk meningkatkan kualitas hidup dan kesejahteraan masyarakat Desa Darmasaba
|
||||
</Text>
|
||||
</Stack>
|
||||
</Container>
|
||||
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Group justify='space-between' align="center">
|
||||
<Stack gap="0">
|
||||
<Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold">
|
||||
Portal Gotong royong Darmasaba
|
||||
</Text>
|
||||
<Text>Temukan berbagai kegiatan lingkungan yang dimiliki Desa Darmasaba</Text>
|
||||
</Stack>
|
||||
<Box>
|
||||
<TextInput
|
||||
radius="lg"
|
||||
placeholder="pencarian"
|
||||
leftSection={<IconSearch size={20} />}
|
||||
w="100%"
|
||||
value={searchValue}
|
||||
onChange={handleSearchChange}
|
||||
/>
|
||||
</Box>
|
||||
</Group>
|
||||
</Box>
|
||||
|
||||
{/* TABS */}
|
||||
<Tabs
|
||||
color={colors['blue-button']}
|
||||
variant="pills"
|
||||
@@ -135,31 +425,24 @@ function LayoutTabsGotongRoyong({
|
||||
onChange={handleTabChange}
|
||||
>
|
||||
<Box px={{ base: "md", md: 100 }} py="md" bg={colors['BG-trans']}>
|
||||
<Grid>
|
||||
<GridCol span={{ base: 12, md: 9, lg: 8, xl: 9 }}>
|
||||
<TabsList>
|
||||
{tabs.map((tab, index) => (
|
||||
<TabsTab
|
||||
key={index}
|
||||
value={tab.value}
|
||||
onClick={() => router.push(tab.href)}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTab>
|
||||
))}
|
||||
</TabsList>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 3, lg: 4, xl: 3 }}>
|
||||
<TextInput
|
||||
radius="lg"
|
||||
placeholder={placeholder}
|
||||
leftSection={searchIcon}
|
||||
w="100%"
|
||||
value={searchValue}
|
||||
onChange={handleSearchChange}
|
||||
/>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
<Box style={{ overflowX: 'auto', whiteSpace: 'nowrap' }}>
|
||||
<TabsList style={{ display: 'flex', flexWrap: 'nowrap', gap: '0.5rem' }}>
|
||||
{tabs.map((tab, index) => (
|
||||
<TabsTab
|
||||
key={index}
|
||||
value={tab.value}
|
||||
onClick={() => router.push(tab.href)}
|
||||
style={{
|
||||
flex: '0 0 auto',
|
||||
minWidth: 100,
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTab>
|
||||
))}
|
||||
</TabsList>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{children}
|
||||
@@ -168,4 +451,4 @@ function LayoutTabsGotongRoyong({
|
||||
);
|
||||
}
|
||||
|
||||
export default LayoutTabsGotongRoyong;
|
||||
export default LayoutTabsGotongRoyong;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Badge, Box, Button, Card, Center, Container, Divider, Flex, Grid, GridC
|
||||
import { IconArrowRight, IconCalendar } from '@tabler/icons-react';
|
||||
import { useTransitionRouter } from 'next-view-transitions';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
function Page() {
|
||||
@@ -14,8 +14,7 @@ function Page() {
|
||||
|
||||
// Parameter URL
|
||||
const search = searchParams.get('search') || '';
|
||||
const currentPage = parseInt(searchParams.get('page') || '1');
|
||||
const [page, setPage] = useState(currentPage);
|
||||
const page = parseInt(searchParams.get('page') || '1');
|
||||
|
||||
// Gunakan proxy untuk state
|
||||
const state = useProxy(gotongRoyongState.kegiatanDesa);
|
||||
@@ -37,12 +36,14 @@ function Page() {
|
||||
}, [page, search]);
|
||||
|
||||
// Update URL saat page berubah
|
||||
useEffect(() => {
|
||||
const url = new URLSearchParams();
|
||||
const handlePageChange = (newPage: number) => {
|
||||
const url = new URLSearchParams(searchParams.toString());
|
||||
if (search) url.set('search', search);
|
||||
if (page > 1) url.set('page', page.toString());
|
||||
if (newPage > 1) url.set('page', newPage.toString());
|
||||
else url.delete('page'); // biar page=1 ga muncul di URL
|
||||
|
||||
router.replace(`?${url.toString()}`);
|
||||
}, [page, search]);
|
||||
};
|
||||
|
||||
const featuredData = featured.data;
|
||||
const paginatedNews = state.findMany.data || [];
|
||||
@@ -77,9 +78,7 @@ function Page() {
|
||||
{featuredData.kategoriKegiatan?.nama || 'Gotong royong'}
|
||||
</Badge>
|
||||
<Title order={2} mb="md">{featuredData.judul}</Title>
|
||||
<Text c="dimmed" lineClamp={3} mb="md">
|
||||
{featuredData.deskripsiSingkat}
|
||||
</Text>
|
||||
<Text c="dimmed" lineClamp={3} mb="md" dangerouslySetInnerHTML={{ __html: featuredData.deskripsiSingkat }} />
|
||||
</div>
|
||||
<Group justify="apart" mt="auto">
|
||||
<Group gap="xs">
|
||||
@@ -146,7 +145,7 @@ function Page() {
|
||||
|
||||
<Text fw={600} size="lg" mt="sm" lineClamp={2}>{item.judul}</Text>
|
||||
|
||||
<Text size="sm" c="dimmed" lineClamp={3} mt="xs">{item.deskripsiSingkat}</Text>
|
||||
<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">
|
||||
@@ -169,7 +168,7 @@ function Page() {
|
||||
<Pagination
|
||||
total={totalPages}
|
||||
value={page}
|
||||
onChange={setPage}
|
||||
onChange={handlePageChange}
|
||||
siblings={1}
|
||||
boundaries={1}
|
||||
withEdges
|
||||
|
||||
@@ -1,47 +1,30 @@
|
||||
'use client'
|
||||
import stateKonservasiAdatBali from '@/app/admin/(dashboard)/_state/lingkungan/konservasi-adat-bali';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Center, List, ListItem, Paper, SimpleGrid, Stack, Text } from '@mantine/core';
|
||||
import { Box, Center, Paper, SimpleGrid, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||
|
||||
const data = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Filosofi Tri Hita Karana',
|
||||
listDeskripsi: (
|
||||
<List fz={'lg'} spacing="sm" ta={'justify'}>
|
||||
<ListItem>Parahyangan: Hubungan manusia dengan Tuhan yang dijaga penuh kesadaran spiritual</ListItem>
|
||||
<ListItem>Pawongan: Harmoni dan kerja sama antar manusia dalam masyarakat</ListItem>
|
||||
<ListItem>Palemahan: Pelestarian lingkungan dan hubungan manusia dengan alam</ListItem>
|
||||
</List>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Bentuk Konservasi Berdasarkan Adat',
|
||||
listDeskripsi: (
|
||||
<List fz={'lg'} spacing="sm" ta={'justify'}>
|
||||
<ListItem>Pelestarian Hutan Adat seperti Alas Pala Sangeh dan Wana Kerthi</ListItem>
|
||||
<ListItem>Subak: Sistem irigasi tradisional yang menekankan kebersamaan dan keberlanjutan</ListItem>
|
||||
<ListItem>Hari Raya Tumpek Uduh: Perayaan untuk menghormati pohon dan tumbuhan</ListItem>
|
||||
<ListItem>Perarem & Awig-Awig: Aturan adat untuk menjaga lingkungan dari kerusakan</ListItem>
|
||||
<ListItem>Ritual penyucian alam seperti Melasti dan Piodalan Segara</ListItem>
|
||||
</List>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Nilai Konservasi Adat',
|
||||
listDeskripsi: (
|
||||
<List fz={'lg'} spacing="sm" ta={'justify'}>
|
||||
<ListItem>Menjaga keseimbangan ekosistem dan lingkungan hidup</ListItem>
|
||||
<ListItem>Melestarikan spiritualitas lokal dan kesucian alam</ListItem>
|
||||
<ListItem>Meningkatkan kesadaran kolektif untuk hidup selaras dengan alam</ListItem>
|
||||
<ListItem>Menjamin keberlanjutan sumber daya alam untuk generasi mendatang</ListItem>
|
||||
</List>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
function Page() {
|
||||
const filosofi = useProxy(stateKonservasiAdatBali.stateFilosofiTriHita.findById)
|
||||
const nilai = useProxy(stateKonservasiAdatBali.stateNilaiKonservasiAdat.findById)
|
||||
const bentuk = useProxy(stateKonservasiAdatBali.stateBentukKonservasiBerdasarkanAdat.findById)
|
||||
|
||||
useShallowEffect(() => {
|
||||
filosofi.load('edit')
|
||||
nilai.load('edit')
|
||||
bentuk.load('edit')
|
||||
}, [])
|
||||
|
||||
if (filosofi.loading || !filosofi.data || nilai.loading || !nilai.data || bentuk.loading || !bentuk.data) {
|
||||
return (
|
||||
<Stack py={20} align="center">
|
||||
<Skeleton radius="md" height={600} width="100%" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap="24">
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
@@ -56,24 +39,99 @@ function Page() {
|
||||
</Text>
|
||||
</Box>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg">
|
||||
{data.map((item) => (
|
||||
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg" style={{ alignItems: 'stretch' }}>
|
||||
{/* Filsosofi */}
|
||||
<Box style={{ display: 'flex', height: '100%' }}>
|
||||
<Paper
|
||||
key={item.id}
|
||||
p="lg"
|
||||
bg="linear-gradient(145deg, #DFE3E8FF 0%, #EFF1F4FF 100%)"
|
||||
style={{ borderRadius: 16, boxShadow: '0 0 20px rgba(28, 110, 164, 0.5)' }}
|
||||
style={{
|
||||
borderRadius: 16,
|
||||
boxShadow: '0 0 20px rgba(28, 110, 164, 0.5)',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}}
|
||||
>
|
||||
<Stack gap="md" px={20}>
|
||||
<Stack gap="md" px={20} style={{ height: '100%' }}>
|
||||
<Center>
|
||||
<Text fz="xl" fw="bold" c="black">
|
||||
{item.title}
|
||||
</Text>
|
||||
{filosofi.data?.judul}
|
||||
</Text>
|
||||
</Center>
|
||||
{item.listDeskripsi}
|
||||
<div
|
||||
style={{
|
||||
wordBreak: "break-word",
|
||||
whiteSpace: "normal",
|
||||
flexGrow: 1
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: filosofi.data?.deskripsi || '' }}
|
||||
/>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))}
|
||||
</Box>
|
||||
{/* Nilai */}
|
||||
<Box style={{ display: 'flex', height: '100%' }}>
|
||||
<Paper
|
||||
p="lg"
|
||||
bg="linear-gradient(145deg, #DFE3E8FF 0%, #EFF1F4FF 100%)"
|
||||
style={{
|
||||
borderRadius: 16,
|
||||
boxShadow: '0 0 20px rgba(28, 110, 164, 0.5)',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}}
|
||||
>
|
||||
<Stack gap="md" px={20} style={{ height: '100%' }}>
|
||||
<Center>
|
||||
<Text fz="xl" fw="bold" c="black">
|
||||
{nilai.data?.judul}
|
||||
</Text>
|
||||
</Center>
|
||||
<div
|
||||
style={{
|
||||
wordBreak: "break-word",
|
||||
whiteSpace: "normal",
|
||||
flexGrow: 1,
|
||||
minHeight: 0
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: nilai.data?.deskripsi || '' }}
|
||||
/>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
{/* Bentuk */}
|
||||
<Box>
|
||||
<Paper
|
||||
p="lg"
|
||||
bg="linear-gradient(145deg, #DFE3E8FF 0%, #EFF1F4FF 100%)"
|
||||
style={{
|
||||
borderRadius: 16,
|
||||
boxShadow: '0 0 20px rgba(28, 110, 164, 0.5)',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}}
|
||||
>
|
||||
<Stack gap="md" px={20} style={{ height: '100%' }}>
|
||||
<Center>
|
||||
<Text fz="xl" fw="bold" c="black">
|
||||
{bentuk.data?.judul}
|
||||
</Text>
|
||||
</Center>
|
||||
<div
|
||||
style={{
|
||||
wordBreak: "break-word",
|
||||
whiteSpace: "normal",
|
||||
flexGrow: 1,
|
||||
minHeight: 0
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: bentuk.data?.deskripsi || '' }}
|
||||
/>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
'use client'
|
||||
import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Flex, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { Box, Center, Flex, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
|
||||
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||
import { Icon, IconChartLine, IconClipboardTextFilled, IconLeaf, IconRecycle, IconScale, IconSearch, IconTent, IconTrashFilled, IconTrophy, IconTruckFilled } from '@tabler/icons-react';
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||
import dynamic from 'next/dynamic';
|
||||
@@ -20,20 +20,26 @@ function Page() {
|
||||
const state = useProxy(pengelolaanSampahState.pengelolaanSampah)
|
||||
const state2 = useProxy(pengelolaanSampahState.keteranganSampah)
|
||||
|
||||
const [search, setSearch] = useState('')
|
||||
const [debouncedSearch] = useDebouncedValue(search, 500);
|
||||
|
||||
const {
|
||||
data,
|
||||
load
|
||||
load,
|
||||
|
||||
} = state.findMany
|
||||
|
||||
const {
|
||||
data: data2,
|
||||
load: load2
|
||||
load: load2,
|
||||
page,
|
||||
totalPages,
|
||||
} = state2.findMany
|
||||
|
||||
useShallowEffect(() => {
|
||||
load()
|
||||
load2()
|
||||
}, [])
|
||||
load2(page, 3, debouncedSearch)
|
||||
}, [page, debouncedSearch])
|
||||
|
||||
const iconMap: Record<string, Icon> = {
|
||||
ekowisata: IconLeaf,
|
||||
@@ -104,8 +110,10 @@ function Page() {
|
||||
px={{ base: 70, md: 150 }}
|
||||
leftSection={<IconSearch size={20} />}
|
||||
placeholder='Cari Bank Sampah Terdekat'
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
|
||||
|
||||
<SimpleGrid cols={{ base: 1, md: 2 }} spacing="lg">
|
||||
{/* Left side - List of bank locations */}
|
||||
<Box>
|
||||
@@ -131,9 +139,17 @@ function Page() {
|
||||
</Paper>
|
||||
))}
|
||||
</Stack>
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => load(newPage)} // ini penting!
|
||||
total={totalPages}
|
||||
my="md"
|
||||
/>
|
||||
</Center>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
|
||||
{/* Right side - Single map showing all locations */}
|
||||
<Box style={{ position: 'sticky', top: '20px' }}>
|
||||
<Paper p="md" bg={colors['white-trans-1']} radius="lg" h="100%">
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
'use client'
|
||||
import beasiswaDesaState from '@/app/admin/(dashboard)/_state/pendidikan/beasiswa-desa';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Center, Group, Image, Modal, Paper, Select, SimpleGrid, Stack, Stepper, StepperStep, Text, TextInput, Title } from '@mantine/core';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import { Box, Button, Center, Group, Image, Modal, Pagination, Paper, Select, SimpleGrid, Skeleton, Stack, Stepper, StepperStep, Text, TextInput, Title } from '@mantine/core';
|
||||
import { useDisclosure, useShallowEffect } from '@mantine/hooks';
|
||||
import { IconArrowRight, IconCoin, IconInfoCircle, IconSchool, IconUsers } from '@tabler/icons-react';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||
import { useTransitionRouter } from 'next-view-transitions';
|
||||
|
||||
const dataBeasiswa = [
|
||||
{ id: 1, nama: 'Penerima Beasiswa', jumlah: '250+', icon: IconUsers },
|
||||
@@ -14,15 +15,11 @@ const dataBeasiswa = [
|
||||
{ id: 3, nama: 'Dana Tersalurkan', jumlah: '1.5M', icon: IconCoin },
|
||||
];
|
||||
|
||||
const dataProgram = [
|
||||
{ id: 1, judul: "Pelatihan SoftSkill", deskripsi: "Pengembangan diri untuk mempersiapkan karir masa depan." },
|
||||
{ id: 2, judul: "Peningkatan Akses Pendidikan", deskripsi: "Memberi kesempatan bagi masyarakat kurang mampu untuk tetap sekolah." },
|
||||
{ id: 3, judul: "Pendampingan Intensif", deskripsi: "Bimbingan dari mentor berpengalaman untuk mendukung akademik." },
|
||||
];
|
||||
|
||||
function Page() {
|
||||
const beasiswaDesa = useProxy(beasiswaDesaState.beasiswaPendaftar)
|
||||
const ungggulanDesa = useProxy(beasiswaDesaState.keunggulanProgram)
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const router = useTransitionRouter()
|
||||
const resetForm = () => {
|
||||
beasiswaDesa.create.form = {
|
||||
namaLengkap: "",
|
||||
@@ -41,6 +38,12 @@ function Page() {
|
||||
};
|
||||
};
|
||||
|
||||
const { data, page, totalPages, loading, load } = ungggulanDesa.findMany;
|
||||
|
||||
useShallowEffect(() => {
|
||||
load(page, 3, "");
|
||||
}, [page])
|
||||
|
||||
const handleSubmit = async () => {
|
||||
await beasiswaDesa.create.create();
|
||||
resetForm();
|
||||
@@ -51,6 +54,14 @@ function Page() {
|
||||
const nextStep = () => setActive((current) => (current < 5 ? current + 1 : current));
|
||||
const prevStep = () => setActive((current) => (current > 0 ? current - 1 : current));
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton height={200} radius="md" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap={40}>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
@@ -70,13 +81,13 @@ function Page() {
|
||||
<Button size="lg" radius="xl" bg={colors['blue-button']} rightSection={<IconArrowRight size={20} />} onClick={open}>
|
||||
Daftar Sekarang
|
||||
</Button>
|
||||
<Button size="lg" radius="xl" variant="light" color={colors['blue-button']} rightSection={<IconInfoCircle size={20} />}>
|
||||
<Button onClick={() => router.push('/darmasaba/pendidikan/beasiswa-desa/pelajari-lebih-lanjut')} size="lg" radius="xl" variant="light" color={colors['blue-button']} rightSection={<IconInfoCircle size={20} />}>
|
||||
Pelajari Lebih Lanjut
|
||||
</Button>
|
||||
</Group>
|
||||
</Box>
|
||||
<Box>
|
||||
<Image alt="Beasiswa Desa" src="/beasiswa-siswa.png" radius="lg" loading="lazy"/>
|
||||
<Image alt="Beasiswa Desa" src="/beasiswa-siswa.png" radius="lg" loading="lazy" />
|
||||
</Box>
|
||||
</SimpleGrid>
|
||||
|
||||
@@ -101,14 +112,29 @@ function Page() {
|
||||
Keunggulan Program
|
||||
</Title>
|
||||
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg">
|
||||
{dataProgram.map((v, k) => (
|
||||
{data.map((v, k) => (
|
||||
<Paper key={k} p="xl" radius="xl" shadow="sm" bg={colors['white-trans-1']}>
|
||||
<Title order={3} fw={700} c={colors['blue-button']} mb="xs">{v.judul}</Title>
|
||||
<Text fz="sm" c="dimmed">{v.deskripsi}</Text>
|
||||
<Text fz="sm" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: v.deskripsi }}/>
|
||||
</Paper>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
total={totalPages}
|
||||
mt="md"
|
||||
mb="md"
|
||||
color="blue"
|
||||
radius="md"
|
||||
/>
|
||||
</Center>
|
||||
|
||||
<Title py={40} ta="center" order={1} fw={900} c={colors['blue-button']}>
|
||||
Timeline Pendaftaran
|
||||
</Title>
|
||||
@@ -142,66 +168,66 @@ function Page() {
|
||||
>
|
||||
<Paper p="lg" radius="xl" withBorder shadow="sm">
|
||||
<Stack gap="sm">
|
||||
<TextInput
|
||||
label="Nama Lengkap"
|
||||
placeholder="Masukkan nama lengkap"
|
||||
onChange={(val) => { beasiswaDesa.create.form.namaLengkap = val.target.value }} />
|
||||
<TextInput
|
||||
type="number"
|
||||
label="NIK"
|
||||
placeholder="Masukkan NIK"
|
||||
onChange={(val) => { beasiswaDesa.create.form.nik = val.target.value }} />
|
||||
<TextInput
|
||||
label="Tempat Lahir"
|
||||
placeholder="Masukkan tempat lahir"
|
||||
onChange={(val) => { beasiswaDesa.create.form.tempatLahir = val.target.value }} />
|
||||
<TextInput
|
||||
type="date"
|
||||
label="Tanggal Lahir"
|
||||
placeholder="Pilih tanggal lahir"
|
||||
onChange={(val) => { beasiswaDesa.create.form.tanggalLahir = val.target.value }} />
|
||||
<Select
|
||||
label="Jenis Kelamin"
|
||||
placeholder="Pilih jenis kelamin"
|
||||
data={[{ value: "LAKI_LAKI", label: "Laki-laki" }, { value: "PEREMPUAN", label: "Perempuan" }]}
|
||||
onChange={(val) => { if (val) beasiswaDesa.create.form.jenisKelamin = val }} />
|
||||
<TextInput
|
||||
label="Kewarganegaraan"
|
||||
placeholder="Masukkan kewarganegaraan"
|
||||
onChange={(val) => { beasiswaDesa.create.form.kewarganegaraan = val.target.value }} />
|
||||
<Select
|
||||
label="Agama"
|
||||
placeholder="Pilih agama"
|
||||
data={[{ value: "ISLAM", label: "Islam" }, { value: "KRISTEN_PROTESTAN", label: "Kristen Protestan" }, { value: "KRISTEN_KATOLIK", label: "Kristen Katolik" }, { value: "HINDU", label: "Hindu" }, { value: "BUDDHA", label: "Buddha" }, { value: "KONGHUCU", label: "Konghucu" }, { value: "LAINNYA", label: "Lainnya" }]}
|
||||
onChange={(val) => { if (val) beasiswaDesa.create.form.agama = val }} />
|
||||
<TextInput
|
||||
label="Alamat KTP"
|
||||
placeholder="Masukkan alamat sesuai KTP"
|
||||
onChange={(val) => { beasiswaDesa.create.form.alamatKTP = val.target.value }} />
|
||||
<TextInput
|
||||
label="Alamat Domisili"
|
||||
placeholder="Masukkan alamat domisili"
|
||||
onChange={(val) => { beasiswaDesa.create.form.alamatDomisili = val.target.value }} />
|
||||
<TextInput
|
||||
type="number"
|
||||
label="Nomor HP"
|
||||
placeholder="Masukkan nomor HP"
|
||||
onChange={(val) => { beasiswaDesa.create.form.noHp = val.target.value }} />
|
||||
<TextInput
|
||||
type="email"
|
||||
label="Email"
|
||||
placeholder="Masukkan alamat email"
|
||||
onChange={(val) => { beasiswaDesa.create.form.email = val.target.value }} />
|
||||
<Select
|
||||
label="Status Pernikahan"
|
||||
placeholder="Pilih status pernikahan"
|
||||
data={[{ value: "BELUM_MENIKAH", label: "Belum Menikah" }, { value: "MENIKAH", label: "Menikah" }, { value: "JANDA_DUDA", label: "Janda/Duda" }]}
|
||||
onChange={(val) => { if (val) beasiswaDesa.create.form.statusPernikahan = val }} />
|
||||
<Select
|
||||
label="Ukuran Baju"
|
||||
placeholder="Pilih ukuran baju"
|
||||
data={[{ value: "S", label: "S" }, { value: "M", label: "M" }, { value: "L", label: "L" }, { value: "XL", label: "XL" }, { value: "XXL", label: "XXL" }, { value: "LAINNYA", label: "Lainnya" }]}
|
||||
onChange={(val) => { if (val) beasiswaDesa.create.form.ukuranBaju = val }} />
|
||||
<TextInput
|
||||
label="Nama Lengkap"
|
||||
placeholder="Masukkan nama lengkap"
|
||||
onChange={(val) => { beasiswaDesa.create.form.namaLengkap = val.target.value }} />
|
||||
<TextInput
|
||||
type="number"
|
||||
label="NIK"
|
||||
placeholder="Masukkan NIK"
|
||||
onChange={(val) => { beasiswaDesa.create.form.nik = val.target.value }} />
|
||||
<TextInput
|
||||
label="Tempat Lahir"
|
||||
placeholder="Masukkan tempat lahir"
|
||||
onChange={(val) => { beasiswaDesa.create.form.tempatLahir = val.target.value }} />
|
||||
<TextInput
|
||||
type="date"
|
||||
label="Tanggal Lahir"
|
||||
placeholder="Pilih tanggal lahir"
|
||||
onChange={(val) => { beasiswaDesa.create.form.tanggalLahir = val.target.value }} />
|
||||
<Select
|
||||
label="Jenis Kelamin"
|
||||
placeholder="Pilih jenis kelamin"
|
||||
data={[{ value: "LAKI_LAKI", label: "Laki-laki" }, { value: "PEREMPUAN", label: "Perempuan" }]}
|
||||
onChange={(val) => { if (val) beasiswaDesa.create.form.jenisKelamin = val }} />
|
||||
<TextInput
|
||||
label="Kewarganegaraan"
|
||||
placeholder="Masukkan kewarganegaraan"
|
||||
onChange={(val) => { beasiswaDesa.create.form.kewarganegaraan = val.target.value }} />
|
||||
<Select
|
||||
label="Agama"
|
||||
placeholder="Pilih agama"
|
||||
data={[{ value: "ISLAM", label: "Islam" }, { value: "KRISTEN_PROTESTAN", label: "Kristen Protestan" }, { value: "KRISTEN_KATOLIK", label: "Kristen Katolik" }, { value: "HINDU", label: "Hindu" }, { value: "BUDDHA", label: "Buddha" }, { value: "KONGHUCU", label: "Konghucu" }, { value: "LAINNYA", label: "Lainnya" }]}
|
||||
onChange={(val) => { if (val) beasiswaDesa.create.form.agama = val }} />
|
||||
<TextInput
|
||||
label="Alamat KTP"
|
||||
placeholder="Masukkan alamat sesuai KTP"
|
||||
onChange={(val) => { beasiswaDesa.create.form.alamatKTP = val.target.value }} />
|
||||
<TextInput
|
||||
label="Alamat Domisili"
|
||||
placeholder="Masukkan alamat domisili"
|
||||
onChange={(val) => { beasiswaDesa.create.form.alamatDomisili = val.target.value }} />
|
||||
<TextInput
|
||||
type="number"
|
||||
label="Nomor HP"
|
||||
placeholder="Masukkan nomor HP"
|
||||
onChange={(val) => { beasiswaDesa.create.form.noHp = val.target.value }} />
|
||||
<TextInput
|
||||
type="email"
|
||||
label="Email"
|
||||
placeholder="Masukkan alamat email"
|
||||
onChange={(val) => { beasiswaDesa.create.form.email = val.target.value }} />
|
||||
<Select
|
||||
label="Status Pernikahan"
|
||||
placeholder="Pilih status pernikahan"
|
||||
data={[{ value: "BELUM_MENIKAH", label: "Belum Menikah" }, { value: "MENIKAH", label: "Menikah" }, { value: "JANDA_DUDA", label: "Janda/Duda" }]}
|
||||
onChange={(val) => { if (val) beasiswaDesa.create.form.statusPernikahan = val }} />
|
||||
<Select
|
||||
label="Ukuran Baju"
|
||||
placeholder="Pilih ukuran baju"
|
||||
data={[{ value: "S", label: "S" }, { value: "M", label: "M" }, { value: "L", label: "L" }, { value: "XL", label: "XL" }, { value: "XXL", label: "XXL" }, { value: "LAINNYA", label: "Lainnya" }]}
|
||||
onChange={(val) => { if (val) beasiswaDesa.create.form.ukuranBaju = val }} />
|
||||
<Group justify="flex-end" mt="md">
|
||||
<Button variant="default" radius="xl" onClick={close}>Batal</Button>
|
||||
<Button radius="xl" bg={colors['blue-button']} onClick={handleSubmit}>Kirim</Button>
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Container,
|
||||
Group,
|
||||
Modal,
|
||||
Paper,
|
||||
Select,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Timeline,
|
||||
Title
|
||||
} from '@mantine/core';
|
||||
import { IconArrowLeft } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import beasiswaDesaState from '@/app/admin/(dashboard)/_state/pendidikan/beasiswa-desa';
|
||||
import colors from '@/con/colors';
|
||||
|
||||
|
||||
export default function BeasiswaPage() {
|
||||
const router = useRouter();
|
||||
const beasiswaDesa = useProxy(beasiswaDesaState.beasiswaPendaftar)
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const resetForm = () => {
|
||||
beasiswaDesa.create.form = {
|
||||
namaLengkap: "",
|
||||
nik: "",
|
||||
tempatLahir: "",
|
||||
tanggalLahir: "",
|
||||
jenisKelamin: "",
|
||||
kewarganegaraan: "",
|
||||
agama: "",
|
||||
alamatKTP: "",
|
||||
alamatDomisili: "",
|
||||
noHp: "",
|
||||
email: "",
|
||||
statusPernikahan: "",
|
||||
ukuranBaju: "",
|
||||
};
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
await beasiswaDesa.create.create();
|
||||
resetForm();
|
||||
close();
|
||||
};
|
||||
|
||||
return (
|
||||
<Box bg="#f1f5fb" pb="xl" pt="md">
|
||||
{/* Tombol Kembali */}
|
||||
<Container size="lg">
|
||||
<Button
|
||||
variant="subtle"
|
||||
color="blue"
|
||||
leftSection={<IconArrowLeft size={18} />}
|
||||
onClick={() => router.back()}
|
||||
mb="lg"
|
||||
>
|
||||
Kembali
|
||||
</Button>
|
||||
</Container>
|
||||
|
||||
{/* Hero Section */}
|
||||
<Container size="lg" py="xl">
|
||||
<Stack gap="md" maw={600}>
|
||||
<Title order={2} c="blue.9">
|
||||
Program Beasiswa Pendidikan Desa Darmasaba
|
||||
</Title>
|
||||
<Text c="dimmed">
|
||||
Program ini bertujuan untuk mendukung pendidikan generasi muda di Desa Darmasaba
|
||||
agar dapat melanjutkan studi ke jenjang lebih tinggi dengan dukungan finansial dan pendampingan.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Container>
|
||||
|
||||
{/* Tentang Program */}
|
||||
<Container size="lg" py="xl">
|
||||
<Title order={3} mb="sm">
|
||||
Tentang Program
|
||||
</Title>
|
||||
<Text>
|
||||
Program Beasiswa Desa Darmasaba adalah inisiatif pemerintah desa untuk meningkatkan akses
|
||||
pendidikan bagi siswa berprestasi dan kurang mampu. Melalui program ini, desa memberikan bantuan
|
||||
biaya sekolah, bimbingan akademik, serta pelatihan soft skill bagi peserta terpilih.
|
||||
</Text>
|
||||
</Container>
|
||||
|
||||
{/* Syarat dan Ketentuan */}
|
||||
<Container size="lg" py="xl">
|
||||
<Title order={3} mb="sm">
|
||||
Syarat Pendaftaran
|
||||
</Title>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="lg">
|
||||
<Paper shadow="sm" p="md" radius="lg" withBorder>
|
||||
<Text fw={500}>Domisili Desa Darmasaba</Text>
|
||||
<Text c="dimmed" fz="sm">
|
||||
Peserta harus merupakan warga desa yang berdomisili minimal 2 tahun.
|
||||
</Text>
|
||||
</Paper>
|
||||
|
||||
<Paper shadow="sm" p="md" radius="lg" withBorder>
|
||||
<Text fw={500}>Nilai Akademik</Text>
|
||||
<Text c="dimmed" fz="sm">
|
||||
Rata-rata nilai raport minimal 80 atau setara.
|
||||
</Text>
|
||||
</Paper>
|
||||
|
||||
<Paper shadow="sm" p="md" radius="lg" withBorder>
|
||||
<Text fw={500}>Surat Rekomendasi</Text>
|
||||
<Text c="dimmed" fz="sm">
|
||||
Diperlukan surat rekomendasi dari sekolah atau guru wali kelas.
|
||||
</Text>
|
||||
</Paper>
|
||||
</SimpleGrid>
|
||||
</Container>
|
||||
|
||||
{/* Proses Seleksi */}
|
||||
<Container size="lg" py="xl">
|
||||
<Title order={3} mb="sm">
|
||||
Proses Seleksi
|
||||
</Title>
|
||||
|
||||
<Timeline active={4} bulletSize={24} lineWidth={2}>
|
||||
<Timeline.Item title="Pendaftaran Online">
|
||||
<Text c="dimmed" size="sm">
|
||||
Calon peserta mengisi formulir pendaftaran dan mengunggah dokumen pendukung.
|
||||
</Text>
|
||||
</Timeline.Item>
|
||||
|
||||
<Timeline.Item title="Seleksi Administrasi">
|
||||
<Text c="dimmed" size="sm">
|
||||
Panitia memverifikasi kelengkapan dan validitas berkas.
|
||||
</Text>
|
||||
</Timeline.Item>
|
||||
|
||||
<Timeline.Item title="Wawancara dan Penilaian">
|
||||
<Text c="dimmed" size="sm">
|
||||
Peserta yang lolos administrasi akan diundang untuk wawancara langsung dengan tim seleksi.
|
||||
</Text>
|
||||
</Timeline.Item>
|
||||
|
||||
<Timeline.Item title="Pengumuman Penerima">
|
||||
<Text c="dimmed" size="sm">
|
||||
Daftar penerima beasiswa diumumkan melalui website resmi Desa Darmasaba.
|
||||
</Text>
|
||||
</Timeline.Item>
|
||||
</Timeline>
|
||||
</Container>
|
||||
|
||||
{/* Testimoni */}
|
||||
<Container size="lg" py="xl">
|
||||
<Title order={3} mb="sm">
|
||||
Cerita Sukses Penerima Beasiswa
|
||||
</Title>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="lg">
|
||||
<Paper shadow="md" p="lg" radius="lg">
|
||||
<Text fs={'italic'}>
|
||||
“Program ini sangat membantu saya melanjutkan kuliah di Universitas Udayana. Terima kasih Desa Darmasaba!”
|
||||
</Text>
|
||||
<Text mt="sm" fw={600}>
|
||||
– Ni Kadek Ayu S., Penerima Beasiswa 2024
|
||||
</Text>
|
||||
</Paper>
|
||||
|
||||
<Paper shadow="md" p="lg" radius="lg">
|
||||
<Text fs={'italic'}>
|
||||
“Selain bantuan dana, kami juga mendapatkan pelatihan komputer dan bahasa Inggris.”
|
||||
</Text>
|
||||
<Text mt="sm" fw={600}>
|
||||
– I Made Gede A., Penerima Beasiswa 2023
|
||||
</Text>
|
||||
</Paper>
|
||||
</SimpleGrid>
|
||||
</Container>
|
||||
|
||||
{/* CTA Akhir */}
|
||||
<Container size="lg" py="xl" ta="center">
|
||||
<Title order={3}>Siap Bergabung dengan Program Ini?</Title>
|
||||
<Text c="dimmed" mb="md">
|
||||
Segera daftar dan wujudkan mimpimu bersama Desa Darmasaba.
|
||||
</Text>
|
||||
<Button onClick={open} size="lg" radius="xl" color="blue">
|
||||
Daftar Sekarang
|
||||
</Button>
|
||||
</Container>
|
||||
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={close}
|
||||
radius="xl"
|
||||
size="lg"
|
||||
transitionProps={{ transition: 'fade', duration: 200 }}
|
||||
title={
|
||||
<Text fz="xl" fw={800} c={colors['blue-button']}>
|
||||
Formulir Beasiswa
|
||||
</Text>
|
||||
}
|
||||
>
|
||||
<Paper p="lg" radius="xl" withBorder shadow="sm">
|
||||
<Stack gap="sm">
|
||||
<TextInput
|
||||
label="Nama Lengkap"
|
||||
placeholder="Masukkan nama lengkap"
|
||||
onChange={(val) => { beasiswaDesa.create.form.namaLengkap = val.target.value }} />
|
||||
<TextInput
|
||||
type="number"
|
||||
label="NIK"
|
||||
placeholder="Masukkan NIK"
|
||||
onChange={(val) => { beasiswaDesa.create.form.nik = val.target.value }} />
|
||||
<TextInput
|
||||
label="Tempat Lahir"
|
||||
placeholder="Masukkan tempat lahir"
|
||||
onChange={(val) => { beasiswaDesa.create.form.tempatLahir = val.target.value }} />
|
||||
<TextInput
|
||||
type="date"
|
||||
label="Tanggal Lahir"
|
||||
placeholder="Pilih tanggal lahir"
|
||||
onChange={(val) => { beasiswaDesa.create.form.tanggalLahir = val.target.value }} />
|
||||
<Select
|
||||
label="Jenis Kelamin"
|
||||
placeholder="Pilih jenis kelamin"
|
||||
data={[{ value: "LAKI_LAKI", label: "Laki-laki" }, { value: "PEREMPUAN", label: "Perempuan" }]}
|
||||
onChange={(val) => { if (val) beasiswaDesa.create.form.jenisKelamin = val }} />
|
||||
<TextInput
|
||||
label="Kewarganegaraan"
|
||||
placeholder="Masukkan kewarganegaraan"
|
||||
onChange={(val) => { beasiswaDesa.create.form.kewarganegaraan = val.target.value }} />
|
||||
<Select
|
||||
label="Agama"
|
||||
placeholder="Pilih agama"
|
||||
data={[{ value: "ISLAM", label: "Islam" }, { value: "KRISTEN_PROTESTAN", label: "Kristen Protestan" }, { value: "KRISTEN_KATOLIK", label: "Kristen Katolik" }, { value: "HINDU", label: "Hindu" }, { value: "BUDDHA", label: "Buddha" }, { value: "KONGHUCU", label: "Konghucu" }, { value: "LAINNYA", label: "Lainnya" }]}
|
||||
onChange={(val) => { if (val) beasiswaDesa.create.form.agama = val }} />
|
||||
<TextInput
|
||||
label="Alamat KTP"
|
||||
placeholder="Masukkan alamat sesuai KTP"
|
||||
onChange={(val) => { beasiswaDesa.create.form.alamatKTP = val.target.value }} />
|
||||
<TextInput
|
||||
label="Alamat Domisili"
|
||||
placeholder="Masukkan alamat domisili"
|
||||
onChange={(val) => { beasiswaDesa.create.form.alamatDomisili = val.target.value }} />
|
||||
<TextInput
|
||||
type="number"
|
||||
label="Nomor HP"
|
||||
placeholder="Masukkan nomor HP"
|
||||
onChange={(val) => { beasiswaDesa.create.form.noHp = val.target.value }} />
|
||||
<TextInput
|
||||
type="email"
|
||||
label="Email"
|
||||
placeholder="Masukkan alamat email"
|
||||
onChange={(val) => { beasiswaDesa.create.form.email = val.target.value }} />
|
||||
<Select
|
||||
label="Status Pernikahan"
|
||||
placeholder="Pilih status pernikahan"
|
||||
data={[{ value: "BELUM_MENIKAH", label: "Belum Menikah" }, { value: "MENIKAH", label: "Menikah" }, { value: "JANDA_DUDA", label: "Janda/Duda" }]}
|
||||
onChange={(val) => { if (val) beasiswaDesa.create.form.statusPernikahan = val }} />
|
||||
<Select
|
||||
label="Ukuran Baju"
|
||||
placeholder="Pilih ukuran baju"
|
||||
data={[{ value: "S", label: "S" }, { value: "M", label: "M" }, { value: "L", label: "L" }, { value: "XL", label: "XL" }, { value: "XXL", label: "XXL" }, { value: "LAINNYA", label: "Lainnya" }]}
|
||||
onChange={(val) => { if (val) beasiswaDesa.create.form.ukuranBaju = val }} />
|
||||
<Group justify="flex-end" mt="md">
|
||||
<Button variant="default" radius="xl" onClick={close}>Batal</Button>
|
||||
<Button radius="xl" bg={colors['blue-button']} onClick={handleSubmit}>Kirim</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Modal>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -51,7 +51,7 @@ function Page() {
|
||||
<Stack gap="sm">
|
||||
<Box>
|
||||
<Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
|
||||
Tujuan Program
|
||||
{stateTujuanProgram.findById.data?.judul}
|
||||
</Badge>
|
||||
<Tooltip label="Gambaran manfaat utama program" position="top-start" withArrow>
|
||||
<Box>
|
||||
@@ -66,7 +66,7 @@ function Page() {
|
||||
<Stack gap="sm">
|
||||
<Box>
|
||||
<Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
|
||||
Lokasi & Jadwal
|
||||
{stateLokasiDanJadwal.findById.data?.judul}
|
||||
</Badge>
|
||||
<Tooltip label="Tempat dan waktu pelaksanaan" position="top-start" withArrow>
|
||||
<Box>
|
||||
@@ -81,7 +81,7 @@ function Page() {
|
||||
<Stack gap="sm">
|
||||
<Box>
|
||||
<Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
|
||||
Fasilitas
|
||||
{stateFasilitas.findById.data?.judul}
|
||||
</Badge>
|
||||
<Tooltip label="Sarana yang disediakan untuk peserta" position="top-start" withArrow>
|
||||
<Box>
|
||||
|
||||
@@ -1,5 +1,101 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
// 'use client'
|
||||
// import colors from '@/con/colors';
|
||||
// import {
|
||||
// ActionIcon,
|
||||
// Box,
|
||||
// Button,
|
||||
// Container,
|
||||
// Group,
|
||||
// Paper,
|
||||
// Stack,
|
||||
// Text,
|
||||
// VisuallyHidden
|
||||
// } from '@mantine/core';
|
||||
// import { IconArrowLeft } from '@tabler/icons-react';
|
||||
// import { useRouter, useSearchParams } from 'next/navigation';
|
||||
// import React, { useState } from 'react';
|
||||
|
||||
// type LayoutSekolahProps = {
|
||||
// title?: string;
|
||||
// jenjangPendidikanList?: string[];
|
||||
// children: React.ReactNode;
|
||||
// };
|
||||
|
||||
// export default function LayoutSekolah({
|
||||
// title = 'Cari Informasi Sekolah',
|
||||
// jenjangPendidikanList = ['Semua', 'TK', 'SD', 'SMP', 'SMA'],
|
||||
// children,
|
||||
// }: LayoutSekolahProps) {
|
||||
// const router = useRouter();
|
||||
// const searchParams = useSearchParams();
|
||||
// const initialJenjangPendidikan = searchParams.get('jenjangPendidikan') || 'Semua';
|
||||
|
||||
// const [jenjangPendidikanAktif, setJenjangPendidikanAktif] = useState(initialJenjangPendidikan);
|
||||
|
||||
// // Cleanup timeout
|
||||
|
||||
|
||||
// // Handle jenjang pendidikan click
|
||||
// const handleJenjangPendidikanChange = (k: string) => {
|
||||
// // arahkan langsung ke route jenjang pendidikan
|
||||
// if (k.toLowerCase() === 'semua') {
|
||||
// setJenjangPendidikanAktif(k);
|
||||
// router.push(`/darmasaba/pendidikan/info-sekolah/semua`);
|
||||
// } else {
|
||||
// setJenjangPendidikanAktif(k);
|
||||
// router.push(`/darmasaba/pendidikan/info-sekolah/${encodeURIComponent(k.toLowerCase())}`);
|
||||
// }
|
||||
// };
|
||||
|
||||
|
||||
// return (
|
||||
// <Box style={{ minHeight: '100vh', background: colors.Bg, paddingBottom: 48 }}>
|
||||
// <Container size="xl" py={{ base: 'md', md: 'xl' }}>
|
||||
// <Stack gap="lg">
|
||||
// {/* Back Button */}
|
||||
// <ActionIcon onClick={() => window.history.back()} variant="light" radius="md" size="lg">
|
||||
// <IconArrowLeft size={20} />
|
||||
// <VisuallyHidden>Kembali</VisuallyHidden>
|
||||
// </ActionIcon>
|
||||
|
||||
// {/* Search & Filter */}
|
||||
// <Paper radius="lg" p="xl" withBorder>
|
||||
// <Stack gap="md">
|
||||
// <Text ta="center" fw={800} fz={28}>{title}</Text>
|
||||
|
||||
// <Text ta="center" fz={"md"} c="black">
|
||||
// Temukan data lengkap mengenai lembaga pendidikan, jumlah siswa terdaftar, dan tenaga pengajar berdasarkan jenjang pendidikan yang tersedia (TK, SD, SMP, SMA). Gunakan tombol di bawah untuk melihat detail sesuai kebutuhanmu.
|
||||
// </Text>
|
||||
// <Group justify="center" gap="xs" wrap="wrap">
|
||||
// {jenjangPendidikanList.map((k) => {
|
||||
// const aktif = k === jenjangPendidikanAktif;
|
||||
// return (
|
||||
// <Button
|
||||
// key={k}
|
||||
// onClick={() => handleJenjangPendidikanChange(k)}
|
||||
// radius="xl"
|
||||
// size="sm"
|
||||
// variant={aktif ? 'filled' : 'light'}
|
||||
// >
|
||||
// {k}
|
||||
// </Button>
|
||||
// );
|
||||
// })}
|
||||
// </Group>
|
||||
// </Stack>
|
||||
// </Paper>
|
||||
|
||||
// {/* Slot konten */}
|
||||
// {children}
|
||||
// </Stack>
|
||||
// </Container>
|
||||
// </Box>
|
||||
// );
|
||||
// }
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
// pastikan path benar
|
||||
import {
|
||||
ActionIcon,
|
||||
Box,
|
||||
@@ -9,44 +105,48 @@ import {
|
||||
Paper,
|
||||
Stack,
|
||||
Text,
|
||||
VisuallyHidden
|
||||
VisuallyHidden,
|
||||
Loader,
|
||||
} from '@mantine/core';
|
||||
import { IconArrowLeft } from '@tabler/icons-react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import React, { useState } from 'react';
|
||||
import { useSnapshot } from 'valtio';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud';
|
||||
|
||||
type LayoutSekolahProps = {
|
||||
title?: string;
|
||||
jenjangPendidikanList?: string[];
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function LayoutSekolah({
|
||||
title = 'Cari Informasi Sekolah',
|
||||
jenjangPendidikanList = ['Semua', 'TK', 'SD', 'SMP', 'SMA'],
|
||||
children,
|
||||
}: LayoutSekolahProps) {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const initialJenjangPendidikan = searchParams.get('jenjangPendidikan') || 'Semua';
|
||||
const snap = useSnapshot(infoSekolahPaud.jenjangPendidikan);
|
||||
|
||||
const [jenjangPendidikanAktif, setJenjangPendidikanAktif] = useState(initialJenjangPendidikan);
|
||||
const [jenjangPendidikanAktif, setJenjangPendidikanAktif] = useState<string>(
|
||||
searchParams.get('jenjangPendidikan') || 'Semua'
|
||||
);
|
||||
|
||||
// Cleanup timeout
|
||||
// Load jenjang pendidikan dari backend
|
||||
useEffect(() => {
|
||||
if (!snap.findMany.data) infoSekolahPaud.jenjangPendidikan.findMany.load(1, 100);
|
||||
}, []);
|
||||
|
||||
|
||||
// Handle jenjang pendidikan click
|
||||
const handleJenjangPendidikanChange = (k: string) => {
|
||||
// arahkan langsung ke route jenjang pendidikan
|
||||
if (k.toLowerCase() === 'semua') {
|
||||
setJenjangPendidikanAktif(k);
|
||||
router.push(`/darmasaba/pendidikan/info-sekolah/semua`);
|
||||
} else {
|
||||
setJenjangPendidikanAktif(k);
|
||||
router.push(`/darmasaba/pendidikan/info-sekolah/${encodeURIComponent(k.toLowerCase())}`);
|
||||
}
|
||||
const handleJenjangPendidikanChange = (nama: string) => {
|
||||
setJenjangPendidikanAktif(nama);
|
||||
const path =
|
||||
nama.toLowerCase() === 'semua'
|
||||
? `/darmasaba/pendidikan/info-sekolah/semua`
|
||||
: `/darmasaba/pendidikan/info-sekolah/${encodeURIComponent(nama.toLowerCase())}`;
|
||||
router.push(path);
|
||||
};
|
||||
|
||||
|
||||
// List tab dari data state
|
||||
const jenjangList = ['Semua', ...(snap.findMany.data?.map((v) => v.nama) || [])];
|
||||
|
||||
return (
|
||||
<Box style={{ minHeight: '100vh', background: colors.Bg, paddingBottom: 48 }}>
|
||||
@@ -61,31 +161,41 @@ export default function LayoutSekolah({
|
||||
{/* Search & Filter */}
|
||||
<Paper radius="lg" p="xl" withBorder>
|
||||
<Stack gap="md">
|
||||
<Text ta="center" fw={800} fz={28}>{title}</Text>
|
||||
|
||||
<Text ta="center" fz={"md"} c="black">
|
||||
Temukan data lengkap mengenai lembaga pendidikan, jumlah siswa terdaftar, dan tenaga pengajar berdasarkan jenjang pendidikan yang tersedia (TK, SD, SMP, SMA). Gunakan tombol di bawah untuk melihat detail sesuai kebutuhanmu.
|
||||
<Text ta="center" fw={800} fz={28}>
|
||||
{title}
|
||||
</Text>
|
||||
<Group justify="center" gap="xs" wrap="wrap">
|
||||
{jenjangPendidikanList.map((k) => {
|
||||
const aktif = k === jenjangPendidikanAktif;
|
||||
return (
|
||||
<Button
|
||||
key={k}
|
||||
onClick={() => handleJenjangPendidikanChange(k)}
|
||||
radius="xl"
|
||||
size="sm"
|
||||
variant={aktif ? 'filled' : 'light'}
|
||||
>
|
||||
{k}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</Group>
|
||||
|
||||
<Text ta="center" fz="md" c="black">
|
||||
Temukan data lengkap mengenai lembaga pendidikan, jumlah siswa terdaftar, dan tenaga
|
||||
pengajar berdasarkan jenjang pendidikan (TK, SD, SMP, SMA).
|
||||
</Text>
|
||||
|
||||
{snap.findMany.loading ? (
|
||||
<Group justify="center" py="sm">
|
||||
<Loader size="sm" />
|
||||
</Group>
|
||||
) : (
|
||||
<Group justify="center" gap="xs" wrap="wrap">
|
||||
{jenjangList.map((k) => {
|
||||
const aktif = k === jenjangPendidikanAktif;
|
||||
return (
|
||||
<Button
|
||||
key={k}
|
||||
onClick={() => handleJenjangPendidikanChange(k)}
|
||||
radius="xl"
|
||||
size="sm"
|
||||
variant={aktif ? 'filled' : 'light'}
|
||||
>
|
||||
{k}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</Group>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Slot konten */}
|
||||
{/* Konten anak */}
|
||||
{children}
|
||||
</Stack>
|
||||
</Container>
|
||||
|
||||
@@ -62,7 +62,7 @@ function Page() {
|
||||
<Tooltip label="Fokus utama program" withArrow>
|
||||
<Title order={2} fw="bold" c={colors['blue-button']} mb="xs" flex="center">
|
||||
<IconTarget size={28} style={{ marginRight: 8 }} />
|
||||
Tujuan Program
|
||||
{stateTujuanPendidikanNonFormal.findById.data?.judul}
|
||||
</Title>
|
||||
</Tooltip>
|
||||
<Text fz="md" lh={1.7} c="dark" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: stateTujuanPendidikanNonFormal.findById.data?.deskripsi }} />
|
||||
@@ -79,7 +79,7 @@ function Page() {
|
||||
<Tooltip label="Lokasi pelaksanaan kegiatan" withArrow>
|
||||
<Title order={2} fw="bold" c={colors['blue-button']} mb="xs" flex="center">
|
||||
<IconMapPin size={28} style={{ marginRight: 8 }} />
|
||||
Tempat Kegiatan
|
||||
{stateTempatKegiatan.findById.data?.judul}
|
||||
</Title>
|
||||
</Tooltip>
|
||||
<Text fz="md" style={{wordBreak: "break-word", whiteSpace: "normal"}} lh={1.7} c="dark" dangerouslySetInnerHTML={{ __html: stateTempatKegiatan.findById.data?.deskripsi }} />
|
||||
@@ -98,7 +98,7 @@ function Page() {
|
||||
<Tooltip label="Ragam jenis program yang tersedia" withArrow>
|
||||
<Title order={2} fw="bold" c={colors['blue-button']} mb="xs" flex="center">
|
||||
<IconBook2 size={28} style={{ marginRight: 8 }} />
|
||||
Jenis Program yang Diselenggarakan
|
||||
{stateJenisProgram.findById.data?.judul}
|
||||
</Title>
|
||||
</Tooltip>
|
||||
<Text fz="md" lh={1.7} c="dark" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: stateJenisProgram.findById.data?.deskripsi }} />
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
'use client';
|
||||
|
||||
import perpustakaanDigitalState from '@/app/admin/(dashboard)/_state/pendidikan/perpustakaan-digital';
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Image,
|
||||
Loader,
|
||||
Paper,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconArrowLeft, IconBook2 } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import ModalPeminjaman from '../../_lib/modalPeminjaman';
|
||||
|
||||
export default function DetailBukuUser() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const stateDetail = useProxy(perpustakaanDigitalState.dataPerpustakaan);
|
||||
const [opened, setOpened] = useState(false);
|
||||
|
||||
useShallowEffect(() => {
|
||||
if (params?.id) stateDetail.findUnique.load(params.id as string);
|
||||
}, [params?.id]);
|
||||
|
||||
const data = stateDetail.findUnique.data;
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<Center h="70vh">
|
||||
<Loader color={colors['blue-button']} size="lg" />
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box bg={colors.Bg} py="xl" px={{ base: 'md', md: 100 }}>
|
||||
{/* Tombol Kembali */}
|
||||
<Button
|
||||
variant="subtle"
|
||||
leftSection={<IconArrowLeft size={20} color={colors['blue-button']} />}
|
||||
onClick={() => router.back()}
|
||||
mb="lg"
|
||||
>
|
||||
Kembali
|
||||
</Button>
|
||||
|
||||
<Paper
|
||||
withBorder
|
||||
shadow="md"
|
||||
radius="xl"
|
||||
bg="white"
|
||||
p={{ base: 'md', md: 'xl' }}
|
||||
maw={800}
|
||||
mx="auto"
|
||||
>
|
||||
<Stack gap="lg" align="center">
|
||||
{/* Cover Buku */}
|
||||
<Image
|
||||
src={data.image?.link || '/placeholder-book.jpg'}
|
||||
alt={data.judul}
|
||||
w={220}
|
||||
h={300}
|
||||
fit="contain"
|
||||
radius="md"
|
||||
loading="lazy"
|
||||
/>
|
||||
|
||||
{/* Judul & Kategori */}
|
||||
<Stack gap={4} align="center">
|
||||
<Title order={2} ta="center" c={colors['blue-button']}>
|
||||
{data.judul}
|
||||
</Title>
|
||||
{data.kategori?.name && (
|
||||
<Badge color="cyan" variant="light" size="md">
|
||||
{data.kategori.name}
|
||||
</Badge>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{/* Deskripsi Buku */}
|
||||
<Paper bg="#F9FAFF" p="md" radius="md" shadow="xs" w="100%">
|
||||
<Text fw={600} mb={6} c={colors['blue-button']}>
|
||||
Deskripsi Buku
|
||||
</Text>
|
||||
<Text
|
||||
fz="sm"
|
||||
lh={1.6}
|
||||
ta="justify"
|
||||
c="dimmed"
|
||||
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
|
||||
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
|
||||
/>
|
||||
</Paper>
|
||||
|
||||
{/* Tombol Pinjam */}
|
||||
<Button
|
||||
size="md"
|
||||
radius="xl"
|
||||
color="blue"
|
||||
mt="sm"
|
||||
leftSection={<IconBook2 size={20} />}
|
||||
onClick={() => setOpened(true)}
|
||||
>
|
||||
Pinjam Buku Ini
|
||||
</Button>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Modal Peminjaman */}
|
||||
<ModalPeminjaman
|
||||
opened={opened}
|
||||
onClose={() => setOpened(false)}
|
||||
buku={data}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,42 +1,52 @@
|
||||
'use client'
|
||||
import perpustakaanDigitalState from '@/app/admin/(dashboard)/_state/pendidikan/perpustakaan-digital';
|
||||
import colors from '@/con/colors';
|
||||
import { ActionIcon, Box, Center, Group, Image, Paper, SimpleGrid, Skeleton, Spoiler, Stack, Text, Tooltip, Badge } from '@mantine/core';
|
||||
import { ActionIcon, Badge, Box, Button, Center, Group, Image, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconBook2, IconRefresh } from '@tabler/icons-react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useTransitionRouter } from 'next-view-transitions';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
function Content({ kategoriBuku }: { kategoriBuku: string }) {
|
||||
const state = useProxy(perpustakaanDigitalState);
|
||||
const [expandedId, setExpandedId] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const searchParams = useSearchParams();
|
||||
const searchQuery = searchParams.get('search') || '';
|
||||
const router = useTransitionRouter()
|
||||
|
||||
const decodedKategoriBuku = decodeURIComponent(kategoriBuku);
|
||||
const kategoriFilter = decodedKategoriBuku.toLowerCase() === 'semua' ? '' : decodedKategoriBuku;
|
||||
|
||||
const loadData = useCallback(async (searchQuery: string = '') => {
|
||||
const loadData = useCallback(async (searchQuery: string = '', page: number = 1) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await state.dataPerpustakaan.findMany.load(1, 100, searchQuery, kategoriFilter);
|
||||
const currentKategoriFilter = decodedKategoriBuku.toLowerCase() === 'semua' ? '' : decodedKategoriBuku;
|
||||
await state.dataPerpustakaan.findMany.load(page, 3, searchQuery, currentKategoriFilter);
|
||||
setCurrentPage(page);
|
||||
setTotalPages(state.dataPerpustakaan.findMany.totalPages);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [kategoriFilter, state.dataPerpustakaan.findMany]);
|
||||
}, [state.dataPerpustakaan.findMany, decodedKategoriBuku]);
|
||||
|
||||
useShallowEffect(() => {
|
||||
loadData(searchQuery);
|
||||
}, [searchQuery, loadData]);
|
||||
}, [searchQuery, loadData, kategoriBuku]);
|
||||
|
||||
const handleRefresh = () => {
|
||||
loadData();
|
||||
};
|
||||
|
||||
if (isLoading || !state.dataPerpustakaan.findMany.load || !state.dataPerpustakaan.findMany.data) {
|
||||
const handlePageChange = (newPage: number) => {
|
||||
loadData(searchQuery, newPage);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
};
|
||||
|
||||
if ((isLoading && !state.dataPerpustakaan.findMany.data) || !state.dataPerpustakaan.findMany.load) {
|
||||
return (
|
||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
|
||||
<Box px={{ base: 'md', md: 100 }} pb={50}>
|
||||
@@ -57,18 +67,23 @@ function Content({ kategoriBuku }: { kategoriBuku: string }) {
|
||||
Koleksi Buku
|
||||
</Text>
|
||||
</Group>
|
||||
<Tooltip label="Muat ulang koleksi" withArrow>
|
||||
<ActionIcon
|
||||
variant="gradient"
|
||||
gradient={{ from: 'blue', to: 'cyan' }}
|
||||
onClick={handleRefresh}
|
||||
loading={isLoading}
|
||||
radius="xl"
|
||||
size="lg"
|
||||
>
|
||||
<IconRefresh size={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Group>
|
||||
<Text size="sm" c="dimmed">
|
||||
Halaman {currentPage} dari {totalPages}
|
||||
</Text>
|
||||
<Tooltip label="Muat ulang koleksi" withArrow>
|
||||
<ActionIcon
|
||||
variant="gradient"
|
||||
gradient={{ from: 'blue', to: 'cyan' }}
|
||||
onClick={handleRefresh}
|
||||
loading={isLoading}
|
||||
radius="xl"
|
||||
size="lg"
|
||||
>
|
||||
<IconRefresh size={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
{!state.dataPerpustakaan.findMany.data || state.dataPerpustakaan.findMany.data.length === 0 ? (
|
||||
@@ -132,30 +147,23 @@ function Content({ kategoriBuku }: { kategoriBuku: string }) {
|
||||
</Badge>
|
||||
)}
|
||||
</Stack>
|
||||
<Spoiler
|
||||
maxHeight={80}
|
||||
showLabel={
|
||||
<Text fw={600} fz="sm" c={colors['blue-button']} ta="center" mt="xs">
|
||||
Lihat deskripsi
|
||||
</Text>
|
||||
}
|
||||
hideLabel={
|
||||
<Text fw={600} fz="sm" c={colors['blue-button']} ta="center" mt="xs">
|
||||
Sembunyikan deskripsi
|
||||
</Text>
|
||||
}
|
||||
expanded={expandedId === v.id}
|
||||
onExpandedChange={(isExpanded) => setExpandedId(isExpanded ? v.id : null)}
|
||||
>
|
||||
<Text
|
||||
ta="justify"
|
||||
fz="sm"
|
||||
lh={1.5}
|
||||
c="dimmed"
|
||||
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
|
||||
style={{wordBreak: "break-word", whiteSpace: "normal"}}
|
||||
/>
|
||||
</Spoiler>
|
||||
<Text
|
||||
ta="justify"
|
||||
fz="sm"
|
||||
lh={1.5}
|
||||
lineClamp={5}
|
||||
truncate="end"
|
||||
c="dimmed"
|
||||
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
|
||||
style={{
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: 'normal',
|
||||
}}
|
||||
/>
|
||||
{/* 📗 Tombol Detail */}
|
||||
<Button variant='light' color='blue' onClick={() => router.push(`/darmasaba/pendidikan/perpustakaan-digital/${v.kategori?.name}/${v.id}`)}>
|
||||
Lihat Detail
|
||||
</Button>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</motion.div>
|
||||
@@ -163,6 +171,15 @@ function Content({ kategoriBuku }: { kategoriBuku: string }) {
|
||||
</SimpleGrid>
|
||||
)}
|
||||
</Box>
|
||||
<Center mt="lg">
|
||||
<Pagination
|
||||
value={currentPage}
|
||||
onChange={handlePageChange}
|
||||
total={totalPages}
|
||||
color="blue"
|
||||
radius="md"
|
||||
/>
|
||||
</Center>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
'use client'
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ActionIcon, Box, Flex, Grid, GridCol, Stack, Tabs, TabsList, TabsTab, Text, TextInput } from '@mantine/core';
|
||||
import { IconSearch, IconUser } from '@tabler/icons-react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||||
import BackButton from '../../../desa/layanan/_com/BackButto';
|
||||
'use client';
|
||||
|
||||
import perpustakaanDigitalState from '@/app/admin/(dashboard)/_state/pendidikan/perpustakaan-digital';
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
Box,
|
||||
Grid,
|
||||
GridCol,
|
||||
Stack,
|
||||
Tabs,
|
||||
TabsList,
|
||||
TabsTab,
|
||||
Text,
|
||||
TextInput
|
||||
} from '@mantine/core';
|
||||
import { IconSearch } from '@tabler/icons-react';
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSnapshot } from 'valtio';
|
||||
import BackButton from '../../../desa/layanan/_com/BackButto';
|
||||
|
||||
|
||||
type LayoutBukuProps = {
|
||||
placeholder?: string;
|
||||
@@ -15,7 +28,7 @@ type LayoutBukuProps = {
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
function LayoutTabs({
|
||||
export default function LayoutTabs({
|
||||
placeholder = 'Cari buku digital...',
|
||||
searchIcon = <IconSearch size={20} />,
|
||||
children,
|
||||
@@ -23,6 +36,7 @@ function LayoutTabs({
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const snap = useSnapshot(perpustakaanDigitalState);
|
||||
|
||||
const activeTab = pathname.split('/').pop() || 'semua';
|
||||
const initialSearch = searchParams.get('search') || '';
|
||||
@@ -30,6 +44,11 @@ function LayoutTabs({
|
||||
const [searchTimeout, setSearchTimeout] = useState<number | null>(null);
|
||||
const [activeTabState, setActiveTabState] = useState(activeTab);
|
||||
|
||||
// 🟦 Ambil kategori buku saat mount
|
||||
useEffect(() => {
|
||||
perpustakaanDigitalState.kategoriBuku.findMany.load();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveTabState(activeTab);
|
||||
}, [activeTab]);
|
||||
@@ -51,7 +70,9 @@ function LayoutTabs({
|
||||
if (value) params.set('search', value);
|
||||
|
||||
router.push(
|
||||
`/darmasaba/pendidikan/perpustakaan-digital/${activeTab}${params.toString() ? `?${params.toString()}` : ''}`
|
||||
`/darmasaba/pendidikan/perpustakaan-digital/${activeTab}${
|
||||
params.toString() ? `?${params.toString()}` : ''
|
||||
}`
|
||||
);
|
||||
};
|
||||
|
||||
@@ -63,41 +84,40 @@ function LayoutTabs({
|
||||
}
|
||||
};
|
||||
|
||||
// 🟩 Tabs dinamis berdasarkan kategori dari state
|
||||
const kategoriTabs =
|
||||
snap.kategoriBuku.findMany.data?.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.name.toLowerCase().replace(/\s+/g, '-'),
|
||||
href: `/darmasaba/pendidikan/perpustakaan-digital/${encodeURIComponent(item.name.toLowerCase().replace(/\s+/g, '-'))}`,
|
||||
})) ?? [];
|
||||
|
||||
const tabs = [
|
||||
{ label: 'Semua', value: 'semua', href: '/darmasaba/pendidikan/perpustakaan-digital/semua' },
|
||||
{ label: 'Dokumenter', value: 'dokumenter', href: '/darmasaba/pendidikan/perpustakaan-digital/dokumenter' },
|
||||
{ label: 'Sayuran', value: 'sayuran', href: '/darmasaba/pendidikan/perpustakaan-digital/sayuran' },
|
||||
{ label: 'Dongeng', value: 'dongeng', href: '/darmasaba/pendidikan/perpustakaan-digital/dongeng' },
|
||||
...kategoriTabs,
|
||||
];
|
||||
|
||||
const handleTabChange = (value: string | null) => {
|
||||
if (!value) return;
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
router.push(`/darmasaba/pendidikan/perpustakaan-digital/${value}${params.toString() ? `?${params.toString()}` : ''}`);
|
||||
router.push(
|
||||
`/darmasaba/pendidikan/perpustakaan-digital/${value}${
|
||||
params.toString() ? `?${params.toString()}` : ''
|
||||
}`
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack pos="relative" bg="var(--mantine-color-gray-0)" py="xl" gap="22">
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Flex justify="space-between" align="center">
|
||||
<BackButton />
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
component={Link}
|
||||
href="/login"
|
||||
radius="xl"
|
||||
size="lg"
|
||||
aria-label="Masuk ke akun"
|
||||
>
|
||||
<IconUser size={26} stroke={1.5} />
|
||||
</ActionIcon>
|
||||
</Flex>
|
||||
</Box>
|
||||
|
||||
<Box pb={20}>
|
||||
<Text ta="center" fz={{ base: '1.6rem', md: '2.4rem' }} fw={700} c={colors['blue-button']}>
|
||||
Perpustakaan Digital Darmasaba
|
||||
</Text>
|
||||
|
||||
<Tabs color="blue" variant="pills" value={activeTabState} onChange={handleTabChange}>
|
||||
<Box px={{ base: 'md', md: 100 }} py="md" bg="var(--mantine-color-gray-1)" style={{ borderRadius: 16 }}>
|
||||
<Grid align="center" gutter="md">
|
||||
@@ -116,6 +136,7 @@ function LayoutTabs({
|
||||
))}
|
||||
</TabsList>
|
||||
</GridCol>
|
||||
|
||||
<GridCol span={{ base: 12, md: 3 }}>
|
||||
<TextInput
|
||||
radius="xl"
|
||||
@@ -130,11 +151,10 @@ function LayoutTabs({
|
||||
</GridCol>
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
{children}
|
||||
</Tabs>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default LayoutTabs;
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
'use client';
|
||||
|
||||
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
||||
import perpustakaanDigitalState from '@/app/admin/(dashboard)/_state/pendidikan/perpustakaan-digital';
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Divider,
|
||||
Group,
|
||||
Image,
|
||||
Modal,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
} from '@mantine/core';
|
||||
import { DateInput } from '@mantine/dates';
|
||||
import {
|
||||
IconArrowRight,
|
||||
IconBook2,
|
||||
IconUser
|
||||
} from '@tabler/icons-react';
|
||||
import { useEffect } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useSnapshot } from 'valtio';
|
||||
|
||||
|
||||
export interface ModalPeminjamanProps {
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
buku: {
|
||||
id: string;
|
||||
judul: string;
|
||||
deskripsi?: string;
|
||||
image?: { link?: string };
|
||||
kategori?: { name?: string };
|
||||
} | null;
|
||||
}
|
||||
|
||||
export default function ModalPeminjaman({
|
||||
opened,
|
||||
onClose,
|
||||
buku,
|
||||
}: ModalPeminjamanProps) {
|
||||
const snap = useSnapshot(perpustakaanDigitalState.peminjamanBuku);
|
||||
|
||||
// reset form setiap modal dibuka
|
||||
useEffect(() => {
|
||||
if (opened && buku) {
|
||||
perpustakaanDigitalState.peminjamanBuku.create.form = {
|
||||
...perpustakaanDigitalState.peminjamanBuku.create.form,
|
||||
bukuId: buku.id,
|
||||
nama: '',
|
||||
noTelp: '',
|
||||
alamat: '',
|
||||
tanggalPinjam: '',
|
||||
batasKembali: '',
|
||||
tanggalKembali: '',
|
||||
catatan: '',
|
||||
};
|
||||
}
|
||||
}, [opened, buku]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!buku) return toast.error('Data buku tidak ditemukan');
|
||||
await perpustakaanDigitalState.peminjamanBuku.create.create();
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
centered
|
||||
size="lg"
|
||||
radius="xl"
|
||||
title={<Text fw={700}>Formulir Peminjaman Buku</Text>}
|
||||
>
|
||||
{buku ? (
|
||||
<Stack>
|
||||
{/* --- Info Buku --- */}
|
||||
<Group align="flex-start">
|
||||
<Image
|
||||
src={buku.image?.link || '/placeholder-book.jpg'}
|
||||
alt={buku.judul}
|
||||
w={100}
|
||||
radius="md"
|
||||
/>
|
||||
<Stack gap={4}>
|
||||
<Group>
|
||||
<IconBook2 size={18} color={colors['blue-button']} />
|
||||
<Text fw={700}>{buku.judul}</Text>
|
||||
</Group>
|
||||
|
||||
{buku.kategori?.name && (
|
||||
<Badge color="cyan" variant="light">
|
||||
{buku.kategori.name}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
<Text fz="sm" c="dimmed" lineClamp={3} dangerouslySetInnerHTML={{ __html: buku.deskripsi || 'Tidak ada deskripsi' }} />
|
||||
</Stack>
|
||||
</Group>
|
||||
|
||||
<Divider my="sm" />
|
||||
|
||||
{/* --- Form Input --- */}
|
||||
<TextInput
|
||||
label="Nama Peminjam"
|
||||
placeholder="Masukkan nama lengkap"
|
||||
leftSection={<IconUser size={16} />}
|
||||
value={snap.create.form.nama}
|
||||
onChange={(e) =>
|
||||
(perpustakaanDigitalState.peminjamanBuku.create.form.nama = e.currentTarget.value)
|
||||
}
|
||||
required
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label="No Telp"
|
||||
placeholder="Masukkan no telp"
|
||||
leftSection={<IconUser size={16} />}
|
||||
value={snap.create.form.noTelp}
|
||||
onChange={(e) =>
|
||||
(perpustakaanDigitalState.peminjamanBuku.create.form.noTelp = e.currentTarget.value)
|
||||
}
|
||||
required
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label="Alamat"
|
||||
placeholder="Masukkan alamat"
|
||||
leftSection={<IconUser size={16} />}
|
||||
value={snap.create.form.alamat}
|
||||
onChange={(e) =>
|
||||
(perpustakaanDigitalState.peminjamanBuku.create.form.alamat = e.currentTarget.value)
|
||||
}
|
||||
required
|
||||
/>
|
||||
|
||||
<DateInput
|
||||
label="Tanggal Pinjam"
|
||||
placeholder="Pilih tanggal pinjam"
|
||||
value={
|
||||
snap.create.form.tanggalPinjam
|
||||
? new Date(snap.create.form.tanggalPinjam)
|
||||
: null
|
||||
}
|
||||
onChange={(date) => {
|
||||
perpustakaanDigitalState.peminjamanBuku.create.form.tanggalPinjam =
|
||||
date ? new Date(date).toISOString() : '';
|
||||
}}
|
||||
required
|
||||
/>
|
||||
|
||||
<Box>
|
||||
<Text>Catatan</Text>
|
||||
<CreateEditor
|
||||
value={snap.create.form.catatan}
|
||||
onChange={(e) =>
|
||||
(perpustakaanDigitalState.peminjamanBuku.create.form.catatan = e)
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<DateInput
|
||||
label="Tanggal Kembali"
|
||||
placeholder="Pilih tanggal kembali"
|
||||
value={
|
||||
snap.create.form.tanggalKembali
|
||||
? new Date(snap.create.form.tanggalKembali)
|
||||
: null
|
||||
}
|
||||
onChange={(date) => {
|
||||
perpustakaanDigitalState.peminjamanBuku.create.form.tanggalKembali =
|
||||
date ? new Date(date).toISOString() : '';
|
||||
}}
|
||||
required
|
||||
/>
|
||||
|
||||
<DateInput
|
||||
label="Batas Pengembalian"
|
||||
placeholder="Pilih tanggal kembali"
|
||||
value={
|
||||
snap.create.form.batasKembali
|
||||
? new Date(snap.create.form.batasKembali)
|
||||
: null
|
||||
}
|
||||
onChange={(date) => {
|
||||
perpustakaanDigitalState.peminjamanBuku.create.form.batasKembali =
|
||||
date ? new Date(date).toISOString() : '';
|
||||
}}
|
||||
required
|
||||
/>
|
||||
|
||||
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
loading={snap.create.loading}
|
||||
disabled={
|
||||
!snap.create.form.nama ||
|
||||
!snap.create.form.tanggalPinjam ||
|
||||
!snap.create.form.batasKembali ||
|
||||
!snap.create.form.tanggalKembali
|
||||
}
|
||||
rightSection={<IconArrowRight size={16} />}
|
||||
radius="xl"
|
||||
>
|
||||
Pinjam Buku
|
||||
</Button>
|
||||
</Stack>
|
||||
) : (
|
||||
<Text c="dimmed" ta="center">
|
||||
Tidak ada data buku yang dipilih
|
||||
</Text>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,45 +1,89 @@
|
||||
'use client'
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client';
|
||||
|
||||
import perpustakaanDigitalState from '@/app/admin/(dashboard)/_state/pendidikan/perpustakaan-digital';
|
||||
import colors from '@/con/colors';
|
||||
import { Badge, Box, Center, Group, Image, Paper, SimpleGrid, Skeleton, Spoiler, Stack, Text, Tooltip } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Group,
|
||||
Image,
|
||||
Paper,
|
||||
SimpleGrid,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Text,
|
||||
Tooltip
|
||||
} from '@mantine/core';
|
||||
import { IconBook2, IconInfoCircle } from '@tabler/icons-react';
|
||||
import { Pagination } from '@mantine/core';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
type ContentProps = {
|
||||
searchQuery: string;
|
||||
};
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import ModalPeminjaman from '../_lib/modalPeminjaman';
|
||||
import { useTransitionRouter } from 'next-view-transitions';
|
||||
|
||||
function Content({ searchQuery }: ContentProps) {
|
||||
export default function Content() {
|
||||
const state = useProxy(perpustakaanDigitalState);
|
||||
const [expandedId, setExpandedId] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const router = useTransitionRouter()
|
||||
const [opened, setOpened] = useState(false);
|
||||
const searchParams = useSearchParams();
|
||||
const searchQuery = searchParams.get('search') || '';
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
||||
const { data: books = [], loading, totalPages } = state.dataPerpustakaan.findMany;
|
||||
|
||||
const loadData = useCallback(
|
||||
async (query: string = '') => {
|
||||
const [selectedBook, setSelectedBook] = useState<{
|
||||
id: string;
|
||||
judul: string;
|
||||
deskripsi?: string;
|
||||
image?: { link?: string };
|
||||
kategori?: { name?: string };
|
||||
} | null>(null);
|
||||
|
||||
// Handle data loading and search
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
const controller = new AbortController();
|
||||
|
||||
const loadData = async () => {
|
||||
if (!isMounted) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await state.dataPerpustakaan.findMany.load(1, 100, query, '');
|
||||
await state.dataPerpustakaan.findMany.load(
|
||||
currentPage,
|
||||
10,
|
||||
searchQuery,
|
||||
''
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Gagal memuat data:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
if (!controller.signal.aborted) {
|
||||
console.error('Gagal memuat data:', error);
|
||||
}
|
||||
}
|
||||
},
|
||||
[state.dataPerpustakaan.findMany]
|
||||
);
|
||||
};
|
||||
|
||||
useShallowEffect(() => {
|
||||
loadData(searchQuery);
|
||||
}, [searchQuery, loadData]);
|
||||
const timer = setTimeout(loadData, 300);
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
controller.abort();
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [searchQuery, currentPage]);
|
||||
|
||||
if (
|
||||
isLoading ||
|
||||
!state.dataPerpustakaan.findMany.load ||
|
||||
!state.dataPerpustakaan.findMany.data
|
||||
) {
|
||||
// Handle page change
|
||||
const handlePageChange = (newPage: number) => {
|
||||
setCurrentPage(newPage);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
};
|
||||
|
||||
// Loading state
|
||||
if (loading || !books) {
|
||||
return (
|
||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
|
||||
<Box px={{ base: 'md', md: 100 }} pb={50}>
|
||||
@@ -53,6 +97,7 @@ function Content({ searchQuery }: ContentProps) {
|
||||
return (
|
||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
|
||||
<Box px={{ base: 'md', md: 100 }} pb={20}>
|
||||
{/* 🔹 Header */}
|
||||
<Group justify="space-between" mb="lg">
|
||||
<Group gap="xs">
|
||||
<IconBook2 size={28} color={colors['blue-button']} />
|
||||
@@ -65,101 +110,152 @@ function Content({ searchQuery }: ContentProps) {
|
||||
</Tooltip>
|
||||
</Group>
|
||||
|
||||
{!state.dataPerpustakaan.findMany.data ||
|
||||
state.dataPerpustakaan.findMany.data.length === 0 ? (
|
||||
{/* 📚 Empty State */}
|
||||
{books.length === 0 ? (
|
||||
<Center py="xl">
|
||||
<Stack gap="xs" align="center">
|
||||
<Image loading="lazy" src="/empty-books.svg" alt="Kosong" w={140} h="auto" />
|
||||
<Text c="dimmed" fz="sm">Belum ada buku yang tersedia</Text>
|
||||
<Image
|
||||
loading="lazy"
|
||||
src="/empty-books.svg"
|
||||
alt="Kosong"
|
||||
w={140}
|
||||
h="auto"
|
||||
/>
|
||||
<Text c="dimmed" fz="sm">
|
||||
Belum ada buku yang tersedia
|
||||
</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
) : (
|
||||
// 📘 Daftar Buku
|
||||
<SimpleGrid
|
||||
cols={{ base: 1, sm: 2, md: 3 }}
|
||||
spacing="lg"
|
||||
verticalSpacing="lg"
|
||||
style={{ alignItems: 'stretch' }}
|
||||
>
|
||||
{state.dataPerpustakaan.findMany.data?.map((v, k) => (
|
||||
{books.map((v) => (
|
||||
<motion.div
|
||||
key={k}
|
||||
key={v.id}
|
||||
whileHover={{ scale: 1.03 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
style={{ height: '100%' }}
|
||||
>
|
||||
<Paper
|
||||
p="lg"
|
||||
<Paper
|
||||
p="lg"
|
||||
radius="2xl"
|
||||
shadow="md"
|
||||
bg="white"
|
||||
style={{
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between'
|
||||
justifyContent: 'space-between',
|
||||
height: '100%',
|
||||
minHeight: 440,
|
||||
}}
|
||||
>
|
||||
<Stack gap="md" style={{ flex: 1 }}>
|
||||
<Stack gap="md" style={{ flexGrow: 1 }}>
|
||||
{/* 🖼 Gambar Buku */}
|
||||
<Center>
|
||||
<Image
|
||||
src={v.image?.link}
|
||||
alt={v.judul}
|
||||
<Image
|
||||
src={v.image?.link}
|
||||
alt={v.judul}
|
||||
h={180}
|
||||
w="auto"
|
||||
fit="contain"
|
||||
fallbackSrc="/placeholder-book.jpg"
|
||||
radius="md"
|
||||
fallbackSrc="/placeholder-book.jpg"
|
||||
loading="lazy"
|
||||
/>
|
||||
</Center>
|
||||
<Stack gap={4} align="center">
|
||||
<Text
|
||||
c={colors["blue-button"]}
|
||||
ta="center"
|
||||
fw={700}
|
||||
fz={{ base: "md", md: "lg" }}
|
||||
|
||||
{/* 📖 Judul & Kategori */}
|
||||
<Stack gap={4} align="center" px="sm">
|
||||
<Text
|
||||
c={colors['blue-button']}
|
||||
ta="center"
|
||||
fw={700}
|
||||
fz={{ base: 'md', md: 'lg' }}
|
||||
lineClamp={2}
|
||||
>
|
||||
{v.judul}
|
||||
</Text>
|
||||
{v.kategori && (
|
||||
<Badge color="cyan" radius="sm" variant="light" size="sm">
|
||||
<Badge
|
||||
color="cyan"
|
||||
radius="sm"
|
||||
variant="light"
|
||||
size="sm"
|
||||
>
|
||||
{v.kategori.name}
|
||||
</Badge>
|
||||
)}
|
||||
</Stack>
|
||||
<Spoiler
|
||||
maxHeight={80}
|
||||
showLabel={
|
||||
<Text fw={600} fz="sm" c={colors['blue-button']} ta="center" mt="xs">
|
||||
Lihat deskripsi
|
||||
</Text>
|
||||
}
|
||||
hideLabel={
|
||||
<Text fw={600} fz="sm" c={colors['blue-button']} ta="center" mt="xs">
|
||||
Sembunyikan deskripsi
|
||||
</Text>
|
||||
}
|
||||
expanded={expandedId === v.id}
|
||||
onExpandedChange={(isExpanded) => setExpandedId(isExpanded ? v.id : null)}
|
||||
>
|
||||
<Text
|
||||
ta="justify"
|
||||
fz="sm"
|
||||
lh={1.5}
|
||||
c="dimmed"
|
||||
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
|
||||
style={{wordBreak: "break-word", whiteSpace: "normal"}}
|
||||
/>
|
||||
</Spoiler>
|
||||
|
||||
{/* 📝 Deskripsi */}
|
||||
<Text
|
||||
ta="justify"
|
||||
fz="sm"
|
||||
lh={1.5}
|
||||
lineClamp={5}
|
||||
truncate="end"
|
||||
c="dimmed"
|
||||
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
|
||||
style={{
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: 'normal',
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
{/* 📗 Tombol Detail */}
|
||||
<Button variant='light' color='blue' onClick={() => router.push(`/darmasaba/pendidikan/perpustakaan-digital/${v.kategori?.name}/${v.id}`)}>
|
||||
Lihat Detail
|
||||
</Button>
|
||||
|
||||
{/* 📗 Tombol Peminjaman */}
|
||||
<Button
|
||||
mt="md"
|
||||
variant="outline"
|
||||
color="blue"
|
||||
radius="xl"
|
||||
size="md"
|
||||
fullWidth
|
||||
leftSection={<IconBook2 size={20} />}
|
||||
onClick={() => {
|
||||
setSelectedBook(v);
|
||||
setOpened(true);
|
||||
}}
|
||||
>
|
||||
Peminjaman
|
||||
</Button>
|
||||
</Paper>
|
||||
</motion.div>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
)}
|
||||
|
||||
|
||||
<Center mt="lg">
|
||||
<Pagination
|
||||
value={currentPage}
|
||||
onChange={handlePageChange}
|
||||
total={totalPages}
|
||||
color="blue"
|
||||
radius="md"
|
||||
/>
|
||||
</Center>
|
||||
</Box>
|
||||
|
||||
{/* 🔸 Modal Peminjaman */}
|
||||
<ModalPeminjaman
|
||||
opened={opened}
|
||||
onClose={() => {
|
||||
setOpened(false);
|
||||
setSelectedBook(null);
|
||||
}}
|
||||
buku={selectedBook}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default Content;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// src/app/darmasaba/(pages)/desa/berita/[kategori]/page.tsx
|
||||
import { Suspense } from "react";
|
||||
import Content from "./content";
|
||||
import Content from "../[kategoriBuku]/content";
|
||||
|
||||
|
||||
export default async function Page() {
|
||||
|
||||
return (
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<Content searchQuery="" />
|
||||
<Content kategoriBuku="semua" />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -62,7 +62,7 @@ function Page() {
|
||||
<Group gap="sm">
|
||||
<IconTargetArrow size={28} color={colors['blue-button']} />
|
||||
<Title order={2} fw="bold" c={colors['blue-button']}>
|
||||
Tujuan Program
|
||||
{stateTujuan.findById.data?.judul}
|
||||
</Title>
|
||||
</Group>
|
||||
<Tooltip label="Detail tujuan program pendidikan anak" position="top-start" withArrow>
|
||||
@@ -83,7 +83,7 @@ function Page() {
|
||||
<Group gap="sm">
|
||||
<IconBook2 size={28} color={colors['blue-button']} />
|
||||
<Title order={2} fw="bold" c={colors['blue-button']}>
|
||||
Program Unggulan
|
||||
{stateUnggulan.findById.data?.judul}
|
||||
</Title>
|
||||
</Group>
|
||||
<Tooltip label="Detail program unggulan yang sedang berjalan" position="top-start" withArrow>
|
||||
|
||||
@@ -12,10 +12,9 @@ import {
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Tooltip,
|
||||
TextInput
|
||||
} from '@mantine/core';
|
||||
import { IconDownload, IconSend2 } from '@tabler/icons-react';
|
||||
import { IconSend2 } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||
@@ -150,23 +149,6 @@ function Page() {
|
||||
</Paper>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
|
||||
<Center pb={30}>
|
||||
<Tooltip label="Unduh dokumen tata cara permohonan" withArrow>
|
||||
<Button
|
||||
fz="sm"
|
||||
size="md"
|
||||
radius="md"
|
||||
bg={colors['blue-button']}
|
||||
leftSection={
|
||||
<IconDownload size={20} color={colors['white-1']} />
|
||||
}
|
||||
>
|
||||
Unduh Tata Cara
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Center>
|
||||
|
||||
<Group justify="center">
|
||||
<Paper
|
||||
p="xl"
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconBuildingCommunity, IconTargetArrow, IconTimeline, IconUser } from '@tabler/icons-react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||
import ScrollToTopButton from '@/app/darmasaba/_com/scrollToTopButton';
|
||||
|
||||
function Page() {
|
||||
const allList = useProxy(stateProfilePPID)
|
||||
@@ -36,99 +37,103 @@ function Page() {
|
||||
: [allList.profile.data]
|
||||
|
||||
return (
|
||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Text ta="center" fz={{ base: "2rem", md: "2.5rem", lg: "3rem", xl: "3.4rem" }} c={colors["blue-button"]} fw="bold">
|
||||
Profil PPID Desa Darmasaba
|
||||
</Text>
|
||||
</Box>
|
||||
{dataArray.map((item) => (
|
||||
<Box key={item.id} px={{ base: "md", md: 100 }}>
|
||||
<Paper p="xl" bg={colors['white-trans-1']} radius="lg" shadow="xl">
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Flex align="center" gap={40} justify="center">
|
||||
<Image loading='lazy' src="/darmasaba-icon.png" h={{ base: 70, md: 120 }} alt="Logo Desa" />
|
||||
<Text fz={{ base: "1.5rem", md: "2rem", lg: "2.5rem", xl: "3rem" }} fw="bold">
|
||||
Pejabat Pengelola Informasi Publik
|
||||
</Text>
|
||||
</Flex>
|
||||
</Box>
|
||||
<Divider my="lg" />
|
||||
|
||||
<Box px={{ base: 0, md: 50 }} pb={40}>
|
||||
<SimpleGrid cols={{ base: 1, xl: 2 }} spacing="xl">
|
||||
<Box px={{ base: 0, md: 50 }}>
|
||||
<Paper bg={colors['white-trans-1']} radius="lg" shadow="sm">
|
||||
<Stack gap="md">
|
||||
<Center>
|
||||
<Image
|
||||
loading='lazy'
|
||||
src={item.image?.link ? `${item.image.link}?t=${Date.now()}` : "/perbekel.png"}
|
||||
w={{ base: 220, md: 330 }}
|
||||
alt="Foto Pimpinan"
|
||||
radius="md"
|
||||
/>
|
||||
</Center>
|
||||
<Paper bg={colors['blue-button']} py={25} radius="lg" className="glass3">
|
||||
<Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "1.5rem", md: "2rem" }}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Stack gap="xl">
|
||||
<Box>
|
||||
<Flex align="center" gap="sm" mb="sm">
|
||||
<IconUser size={28} />
|
||||
<Text fz={{ base: "1.25rem", md: "1.5rem" }} fw="bold">Biografi</Text>
|
||||
</Flex>
|
||||
<Text fz={{ base: "1rem", md: "1.125rem", lg: "1.25rem" }} ta="justify" dangerouslySetInnerHTML={{ __html: item.biodata }} style={{wordBreak: "break-word", whiteSpace: "normal"}} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Flex align="center" gap="sm" mb="sm">
|
||||
<IconTimeline size={28} />
|
||||
<Text fz={{ base: "1.25rem", md: "1.5rem" }} fw="bold">Riwayat Karir</Text>
|
||||
</Flex>
|
||||
<Text fz={{ base: "1rem", md: "1.125rem", lg: "1.25rem" }} dangerouslySetInnerHTML={{ __html: item.riwayat }} style={{wordBreak: "break-word", whiteSpace: "normal"}} />
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
|
||||
<Box pb={40}>
|
||||
<Flex align="center" gap="sm" mb="sm">
|
||||
<IconBuildingCommunity size={28} />
|
||||
<Text fz={{ base: "1.25rem", md: "1.5rem" }} fw="bold">Pengalaman Organisasi</Text>
|
||||
</Flex>
|
||||
<List spacing="xs" size="sm">
|
||||
<Box px={20}>
|
||||
<Text fz={{ base: "1rem", md: "1.125rem" }} ta="justify" dangerouslySetInnerHTML={{ __html: item.pengalaman }} style={{wordBreak: "break-word", whiteSpace: "normal"}} />
|
||||
</Box>
|
||||
</List>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Flex align="center" gap="sm" mb="sm">
|
||||
<IconTargetArrow size={28} />
|
||||
<Text fz={{ base: "1.25rem", md: "1.5rem" }} fw="bold">Program Unggulan</Text>
|
||||
</Flex>
|
||||
<List spacing="xs" size="sm">
|
||||
<Box px={20}>
|
||||
<Text fz={{ base: "1rem", md: "1.125rem" }} ta="justify" dangerouslySetInnerHTML={{ __html: item.unggulan }} style={{wordBreak: "break-word", whiteSpace: "normal"}} />
|
||||
</Box>
|
||||
</List>
|
||||
</Box>
|
||||
</Paper>
|
||||
<Box>
|
||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Text ta="center" fz={{ base: "2rem", md: "2.5rem", lg: "3rem", xl: "3.4rem" }} c={colors["blue-button"]} fw="bold">
|
||||
Profil PPID Desa Darmasaba
|
||||
</Text>
|
||||
</Box>
|
||||
{dataArray.map((item) => (
|
||||
<Box key={item.id} px={{ base: "md", md: 100 }}>
|
||||
<Paper p="xl" bg={colors['white-trans-1']} radius="lg" shadow="xl">
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Flex align="center" gap={40} justify="center">
|
||||
<Image loading='lazy' src="/darmasaba-icon.png" h={{ base: 70, md: 120 }} alt="Logo Desa" />
|
||||
<Text fz={{ base: "1.5rem", md: "2rem", lg: "2.5rem", xl: "3rem" }} fw="bold">
|
||||
Pejabat Pengelola Informasi Publik
|
||||
</Text>
|
||||
</Flex>
|
||||
</Box>
|
||||
<Divider my="lg" />
|
||||
|
||||
<Box px={{ base: 0, md: 50 }} pb={40}>
|
||||
<SimpleGrid cols={{ base: 1, xl: 2 }} spacing="xl">
|
||||
<Box px={{ base: 0, md: 50 }}>
|
||||
<Paper bg={colors['white-trans-1']} radius="lg" shadow="sm">
|
||||
<Stack gap="md">
|
||||
<Center>
|
||||
<Image
|
||||
loading='lazy'
|
||||
src={item.image?.link ? `${item.image.link}?t=${Date.now()}` : "/perbekel.png"}
|
||||
w={{ base: 220, md: 330 }}
|
||||
alt="Foto Pimpinan"
|
||||
radius="md"
|
||||
/>
|
||||
</Center>
|
||||
<Paper bg={colors['blue-button']} py={25} radius="lg" className="glass3">
|
||||
<Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "1.5rem", md: "2rem" }}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Stack gap="xl">
|
||||
<Box>
|
||||
<Flex align="center" gap="sm" mb="sm">
|
||||
<IconUser size={28} />
|
||||
<Text fz={{ base: "1.25rem", md: "1.5rem" }} fw="bold">Biografi</Text>
|
||||
</Flex>
|
||||
<Text fz={{ base: "1rem", md: "1.125rem", lg: "1.25rem" }} ta="justify" dangerouslySetInnerHTML={{ __html: item.biodata }} style={{ wordBreak: "break-word", whiteSpace: "normal" }} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Flex align="center" gap="sm" mb="sm">
|
||||
<IconTimeline size={28} />
|
||||
<Text fz={{ base: "1.25rem", md: "1.5rem" }} fw="bold">Riwayat Karir</Text>
|
||||
</Flex>
|
||||
<Text fz={{ base: "1rem", md: "1.125rem", lg: "1.25rem" }} dangerouslySetInnerHTML={{ __html: item.riwayat }} style={{ wordBreak: "break-word", whiteSpace: "normal" }} />
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
|
||||
<Box pb={40}>
|
||||
<Flex align="center" gap="sm" mb="sm">
|
||||
<IconBuildingCommunity size={28} />
|
||||
<Text fz={{ base: "1.25rem", md: "1.5rem" }} fw="bold">Pengalaman Organisasi</Text>
|
||||
</Flex>
|
||||
<List spacing="xs" size="sm">
|
||||
<Box px={20}>
|
||||
<Text fz={{ base: "1rem", md: "1.125rem" }} ta="justify" dangerouslySetInnerHTML={{ __html: item.pengalaman }} style={{ wordBreak: "break-word", whiteSpace: "normal" }} />
|
||||
</Box>
|
||||
</List>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Flex align="center" gap="sm" mb="sm">
|
||||
<IconTargetArrow size={28} />
|
||||
<Text fz={{ base: "1.25rem", md: "1.5rem" }} fw="bold">Program Unggulan</Text>
|
||||
</Flex>
|
||||
<List spacing="xs" size="sm">
|
||||
<Box px={20}>
|
||||
<Text fz={{ base: "1rem", md: "1.125rem" }} ta="justify" dangerouslySetInnerHTML={{ __html: item.unggulan }} style={{ wordBreak: "break-word", whiteSpace: "normal" }} />
|
||||
</Box>
|
||||
</List>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
{/* Tombol Scroll ke Atas */}
|
||||
<ScrollToTopButton />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
157
src/app/darmasaba/(pages)/ppid/struktur-ppid/[id]/page.tsx
Normal file
157
src/app/darmasaba/(pages)/ppid/struktur-ppid/[id]/page.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
'use client';
|
||||
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID';
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
Box,
|
||||
Divider,
|
||||
Group,
|
||||
Image,
|
||||
Paper,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
function DetailPegawaiUser() {
|
||||
const statePegawai = useProxy(stateStrukturPPID.pegawai);
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
|
||||
useShallowEffect(() => {
|
||||
stateStrukturPPID.posisiOrganisasi.findMany.load();
|
||||
statePegawai.findUnique.load(params?.id as string);
|
||||
}, []);
|
||||
|
||||
|
||||
if (!statePegawai.findUnique.data) {
|
||||
return (
|
||||
<Stack py="lg">
|
||||
<Skeleton height={500} radius="md" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
const data = statePegawai.findUnique.data;
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="xl">
|
||||
{/* Back button */}
|
||||
<Group mb="lg">
|
||||
<Box
|
||||
onClick={() => router.back()}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<IconArrowBack size={22} color={colors['blue-button']} />
|
||||
<Text c={colors['blue-button']} fw={500}>
|
||||
Kembali
|
||||
</Text>
|
||||
</Box>
|
||||
</Group>
|
||||
|
||||
<Paper
|
||||
w={{ base: '100%', md: '70%' }}
|
||||
mx="auto"
|
||||
p="xl"
|
||||
radius="lg"
|
||||
shadow="sm"
|
||||
bg="white"
|
||||
style={{
|
||||
border: '1px solid #eaeaea',
|
||||
}}
|
||||
>
|
||||
<Stack align="center" gap="md">
|
||||
{/* Foto Profil */}
|
||||
<Image
|
||||
src={data.image?.link || '/placeholder-profile.png'}
|
||||
alt={data.namaLengkap || 'Foto Profil'}
|
||||
w={160}
|
||||
h={160}
|
||||
radius={100}
|
||||
fit="cover"
|
||||
style={{ border: `2px solid ${colors['blue-button']}` }}
|
||||
loading="lazy"
|
||||
/>
|
||||
|
||||
{/* Nama & Jabatan */}
|
||||
<Stack align="center" gap={2}>
|
||||
<Title order={3} fw={700} c={colors['blue-button']}>
|
||||
{data.namaLengkap || '-'} {data.gelarAkademik || ''}
|
||||
</Title>
|
||||
<Text fz="sm" c="dimmed">
|
||||
{data.posisi?.nama || 'Posisi tidak tersedia'}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Divider my="lg" />
|
||||
|
||||
{/* Informasi Detail */}
|
||||
<Stack gap="md">
|
||||
<InfoRow label="Email" value={data.email} />
|
||||
<InfoRow label="Telepon" value={data.telepon} />
|
||||
<InfoRow label="Alamat" value={data.alamat} multiline />
|
||||
<InfoRow
|
||||
label="Tanggal Masuk"
|
||||
value={
|
||||
data.tanggalMasuk
|
||||
? new Date(data.tanggalMasuk).toLocaleDateString('id-ID', {
|
||||
day: '2-digit',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})
|
||||
: '-'
|
||||
}
|
||||
/>
|
||||
<InfoRow
|
||||
label="Status"
|
||||
value={data.isActive ? 'Aktif' : 'Tidak Aktif'}
|
||||
valueColor={data.isActive ? 'green' : 'red'}
|
||||
/>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
/* Komponen kecil untuk menampilkan baris informasi */
|
||||
function InfoRow({
|
||||
label,
|
||||
value,
|
||||
valueColor,
|
||||
multiline = false,
|
||||
}: {
|
||||
label: string;
|
||||
value?: string | null;
|
||||
valueColor?: string;
|
||||
multiline?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} c="dark">
|
||||
{label}
|
||||
</Text>
|
||||
<Text
|
||||
fz="sm"
|
||||
c={valueColor || 'dimmed'}
|
||||
style={{
|
||||
whiteSpace: multiline ? 'normal' : 'nowrap',
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
>
|
||||
{value || '-'}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default DetailPegawaiUser;
|
||||
@@ -1,129 +1,8 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
// /* 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 { Box, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||
// import { OrganizationChart } from 'primereact/organizationchart';
|
||||
// import { useEffect } from 'react';
|
||||
// import { useProxy } from 'valtio/utils';
|
||||
// import BackButton from '../../desa/layanan/_com/BackButto';
|
||||
|
||||
// function Page() {
|
||||
// return (
|
||||
// <Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
||||
// <Box px={{ base: 'md', md: 100 }}>
|
||||
// <BackButton />
|
||||
// </Box>
|
||||
// <Title ta={"center"} fz={{ base: "2rem", md: "2.5rem", lg: "3rem", xl: "3.5rem" }} c={colors["blue-button"]} fw={"bold"}>Struktur PPID</Title>
|
||||
// <StrukturOrganisasiPPID />
|
||||
|
||||
// </Stack>
|
||||
// );
|
||||
// }
|
||||
|
||||
// function StrukturOrganisasiPPID() {
|
||||
// const stateOrganisasi = useProxy(stateStrukturPPID.pegawai)
|
||||
|
||||
// useEffect(() => {
|
||||
// stateOrganisasi.findMany.load()
|
||||
// }, [])
|
||||
|
||||
// if (!stateOrganisasi.findMany.data || stateOrganisasi.findMany.data.length === 0) {
|
||||
// return (
|
||||
// <Stack py={10}>
|
||||
// <Skeleton h={500} />
|
||||
// </Stack>
|
||||
// );
|
||||
// }
|
||||
|
||||
// // Step 1: Group pegawai berdasarkan posisiId
|
||||
// const posisiMap = new Map<string, any>();
|
||||
|
||||
// for (const pegawai of stateOrganisasi.findMany.data) {
|
||||
// const posisiId = pegawai.posisi.id;
|
||||
// if (!posisiMap.has(posisiId)) {
|
||||
// posisiMap.set(posisiId, {
|
||||
// ...pegawai.posisi,
|
||||
// pegawaiList: [],
|
||||
// children: []
|
||||
// });
|
||||
// }
|
||||
// posisiMap.get(posisiId)!.pegawaiList.push(pegawai);
|
||||
// }
|
||||
|
||||
|
||||
// // Step 2: Buat struktur pohon berdasarkan parentId
|
||||
// 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);
|
||||
// }
|
||||
// });
|
||||
|
||||
// // Step 3: Ubah struktur ke format OrganizationChart
|
||||
// function toOrgChartFormat(node: any): any {
|
||||
// return {
|
||||
// expanded: true,
|
||||
// type: 'person',
|
||||
// styleClass: 'p-person',
|
||||
// data: {
|
||||
// name: node.pegawaiList?.[0]?.namaLengkap || 'Tidak ada pegawai',
|
||||
// status: node.nama,
|
||||
// image: node.pegawaiList?.[0]?.image?.link || '/img/default.png'
|
||||
// },
|
||||
// children: node.children.map(toOrgChartFormat)
|
||||
// };
|
||||
// }
|
||||
|
||||
|
||||
// const chartData = root.map(toOrgChartFormat);
|
||||
|
||||
// return (
|
||||
// <Box py={10}>
|
||||
// <Paper bg={colors.grey} p="md" style={{ overflowX: 'auto' }}>
|
||||
// <OrganizationChart style={{ color: colors['blue-button'] }} value={chartData} nodeTemplate={nodeTemplate} />
|
||||
// </Paper>
|
||||
// </Box>
|
||||
// );
|
||||
// }
|
||||
|
||||
|
||||
// function nodeTemplate(node: any) {
|
||||
// const imageSrc = node?.data?.image || '/img/default.png';
|
||||
// const name = node?.data?.name || 'Tanpa Nama';
|
||||
// const status = node?.data?.status || 'Tidak ada deskripsi';
|
||||
|
||||
// return (
|
||||
// <Stack pos={"relative"} py={"xl"} gap={"22"}>
|
||||
// <Stack align="center" gap={4}>
|
||||
// <Image
|
||||
// src={imageSrc}
|
||||
// alt={name}
|
||||
// radius="xl"
|
||||
// w={120}
|
||||
// h={120}
|
||||
// fit="cover"
|
||||
// />
|
||||
// <Text fw={600} ta="center">{name}</Text>
|
||||
// <Text size="sm" c="dimmed" ta="center">{status}</Text>
|
||||
// </Stack>
|
||||
// </Stack>
|
||||
// );
|
||||
// }
|
||||
|
||||
// export default Page;
|
||||
|
||||
'use client'
|
||||
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID'
|
||||
import colors from '@/con/colors'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
@@ -145,7 +24,8 @@ import { OrganizationChart } from 'primereact/organizationchart'
|
||||
import { useEffect } from 'react'
|
||||
import { useProxy } from 'valtio/utils'
|
||||
import BackButton from '../../desa/layanan/_com/BackButto'
|
||||
import colors from '@/con/colors'
|
||||
import { useTransitionRouter } from 'next-view-transitions'
|
||||
import ScrollToTopButton from '@/app/darmasaba/_com/scrollToTopButton'
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
@@ -180,12 +60,16 @@ export default function Page() {
|
||||
<StrukturOrganisasiPPID />
|
||||
</Box>
|
||||
</Container>
|
||||
|
||||
{/* Tombol Scroll ke Atas */}
|
||||
<ScrollToTopButton />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function StrukturOrganisasiPPID() {
|
||||
const stateOrganisasi: any = useProxy(stateStrukturPPID.pegawai)
|
||||
const router = useTransitionRouter()
|
||||
|
||||
useEffect(() => {
|
||||
void stateOrganisasi.findMany.load()
|
||||
@@ -292,19 +176,21 @@ function StrukturOrganisasiPPID() {
|
||||
})
|
||||
|
||||
function toOrgChartFormat(node: any): any {
|
||||
const pegawai = node.pegawaiList?.[0];
|
||||
return {
|
||||
expanded: true,
|
||||
type: 'person',
|
||||
styleClass: 'p-person',
|
||||
data: {
|
||||
name: node.pegawaiList?.[0]?.namaLengkap || 'Belum ditugaskan',
|
||||
id: pegawai?.id || null, // tambahin ini bro
|
||||
name: pegawai?.namaLengkap || 'Belum ditugaskan',
|
||||
title: node.nama || 'Tanpa jabatan',
|
||||
image: node.pegawaiList?.[0]?.image?.link || '/img/default.png',
|
||||
image: pegawai?.image?.link || '/img/default.png',
|
||||
description: node.deskripsi || '',
|
||||
positionId: node.id || null,
|
||||
},
|
||||
children: node.children?.map(toOrgChartFormat) || [],
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const chartData = root.map(toOrgChartFormat)
|
||||
@@ -322,21 +208,22 @@ function StrukturOrganisasiPPID() {
|
||||
>
|
||||
<OrganizationChart
|
||||
value={chartData}
|
||||
nodeTemplate={nodeTemplate}
|
||||
nodeTemplate={(node) => nodeTemplate(node, router)}
|
||||
/>
|
||||
</Paper>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function nodeTemplate(node: any) {
|
||||
function nodeTemplate(node: any, router: ReturnType<typeof useTransitionRouter>) {
|
||||
const imageSrc = node?.data?.image || '/img/default.png'
|
||||
const name = node?.data?.name || 'Tanpa Nama'
|
||||
const title = node?.data?.title || 'Tanpa Jabatan'
|
||||
const description = node?.data?.description || ''
|
||||
|
||||
|
||||
return (
|
||||
<Transition mounted transition="pop" duration={240}>
|
||||
<Transition mounted transition="pop" duration={240}>
|
||||
{(styles) => (
|
||||
<Card
|
||||
radius="lg"
|
||||
@@ -374,19 +261,17 @@ function nodeTemplate(node: any) {
|
||||
<Text size="xs" c="dimmed" mt={8} lineClamp={3}>
|
||||
{description || 'Belum ada deskripsi.'}
|
||||
</Text>
|
||||
<Tooltip label="Kembali ke struktur organisasi" withArrow position="bottom">
|
||||
<Tooltip label="Lihat Detail" withArrow position="bottom">
|
||||
<Button
|
||||
variant="light"
|
||||
size="xs"
|
||||
mt="md"
|
||||
onClick={() => {
|
||||
const id = node?.data?.positionId
|
||||
if (id && (window as any).scrollTo) {
|
||||
;(window as any).scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
const id = node?.data?.id
|
||||
router.push(`/darmasaba/ppid/struktur-ppid/${id}`)
|
||||
}}
|
||||
>
|
||||
Kembali
|
||||
Lihat Detail
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Card>
|
||||
@@ -394,6 +279,3 @@ function nodeTemplate(node: any) {
|
||||
</Transition>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ function Page() {
|
||||
<Text
|
||||
fz={{ base: 'md', md: 'lg' }}
|
||||
lh={1.7}
|
||||
ta="justify"
|
||||
ta="center"
|
||||
dangerouslySetInnerHTML={{ __html: item.visi }}
|
||||
style={{wordBreak: "break-word", whiteSpace: "normal"}}
|
||||
/>
|
||||
@@ -88,7 +88,7 @@ function Page() {
|
||||
<Text
|
||||
fz={{ base: 'md', md: 'lg' }}
|
||||
lh={1.7}
|
||||
ta="justify"
|
||||
ta="center"
|
||||
dangerouslySetInnerHTML={{ __html: item.misi }}
|
||||
style={{wordBreak: "break-word", whiteSpace: "normal"}}
|
||||
/>
|
||||
|
||||
@@ -1,27 +1,63 @@
|
||||
import { useRef, useState, useEffect } from 'react';
|
||||
import stateNav from "@/state/state-nav";
|
||||
import { Container, Stack, TextInput, Tooltip } from "@mantine/core";
|
||||
import { IconSearch } from "@tabler/icons-react";
|
||||
import { Container, Stack, ActionIcon, Box } from "@mantine/core";
|
||||
import { IconX } from '@tabler/icons-react';
|
||||
import GlobalSearch from "./globalSearch";
|
||||
|
||||
export function NavbarSearch() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Close when clicking outside
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false);
|
||||
stateNav.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Add event listener
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
// Clean up
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Container
|
||||
w={{ base: "100%", md: "80%" }}
|
||||
fluid
|
||||
py="xl"
|
||||
onMouseLeave={stateNav.clear}
|
||||
<Box
|
||||
ref={containerRef}
|
||||
style={{ position: 'relative' }}
|
||||
>
|
||||
<Stack pt="xl">
|
||||
<Tooltip label="Type to search across the site" position="bottom-start" withArrow>
|
||||
<TextInput
|
||||
autoFocus
|
||||
size="lg"
|
||||
variant="filled"
|
||||
radius="xl"
|
||||
placeholder="Search anything..."
|
||||
leftSection={<IconSearch size={20} />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</Container>
|
||||
<Container
|
||||
w={{ base: "100%", md: "80%" }}
|
||||
fluid
|
||||
py="xl"
|
||||
>
|
||||
<Stack pt="xl">
|
||||
<Box style={{ position: 'relative' }}>
|
||||
<GlobalSearch />
|
||||
{isOpen && (
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
stateNav.clear();
|
||||
}}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 10,
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
zIndex: 1000
|
||||
}}
|
||||
>
|
||||
<IconX size={16} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
91
src/app/darmasaba/_com/globalSearch.tsx
Normal file
91
src/app/darmasaba/_com/globalSearch.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
'use client';
|
||||
import searchState, { debouncedFetch } from '@/app/api/[[...slugs]]/_lib/search/searchState';
|
||||
import { Box, Center, Loader, Stack, Text, TextInput } from '@mantine/core';
|
||||
import { IconX } from '@tabler/icons-react';
|
||||
import { useEffect } from 'react';
|
||||
import { useSnapshot } from 'valtio';
|
||||
import getDetailUrl from './searchUrl';
|
||||
|
||||
|
||||
|
||||
export default function GlobalSearch() {
|
||||
const snap = useSnapshot(searchState);
|
||||
|
||||
// Infinite scroll
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
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
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 📄 Hasil pencarian */}
|
||||
<div style={{ maxHeight: '400px', overflowY: 'auto' }}>
|
||||
{snap.results.map((item, i) => (
|
||||
<Box
|
||||
key={i}
|
||||
p="sm"
|
||||
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;
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ function Penghargaan() {
|
||||
variant="gradient"
|
||||
gradient={{ from: "cyan", to: "blue", deg: 60 }}
|
||||
>
|
||||
Penghargaan & Prestasi Desa
|
||||
Penghargaan Desa
|
||||
</Text>
|
||||
|
||||
{loading ? (
|
||||
|
||||
36
src/app/darmasaba/_com/scrollToTopButton.tsx
Normal file
36
src/app/darmasaba/_com/scrollToTopButton.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
'use client'
|
||||
import { useWindowScroll } from '@mantine/hooks';
|
||||
import { ActionIcon, Transition } from '@mantine/core';
|
||||
import { IconArrowUp } from '@tabler/icons-react';
|
||||
import colors from '@/con/colors';
|
||||
|
||||
function ScrollToTopButton() {
|
||||
const [scroll, scrollTo] = useWindowScroll();
|
||||
|
||||
return (
|
||||
<Transition
|
||||
mounted={scroll.y > 300}
|
||||
transition="slide-up"
|
||||
duration={300}
|
||||
timingFunction="ease"
|
||||
>
|
||||
{(styles) => (
|
||||
<ActionIcon
|
||||
style={styles}
|
||||
size="xl"
|
||||
radius="xl"
|
||||
variant="filled"
|
||||
color={colors['blue-button']}
|
||||
onClick={() => scrollTo({ y: 0 })}
|
||||
pos="fixed"
|
||||
bottom={24}
|
||||
right={24}
|
||||
aria-label="Kembali ke atas"
|
||||
>
|
||||
<IconArrowUp size={20} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
export default ScrollToTopButton
|
||||
60
src/app/darmasaba/_com/searchUrl.tsx
Normal file
60
src/app/darmasaba/_com/searchUrl.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
const getDetailUrl = (item: { type?: string; id: string | number; [key: string]: unknown }) => {
|
||||
const { type, id, kategori } = item;
|
||||
const typeUrlMap: Record<string, string> = {
|
||||
programinovasi: `/darmasaba/program-inovasi/${id}`,
|
||||
desaantikorupsi: '/darmasaba/desa-anti-korupsi',
|
||||
sdgsdesa: '/darmasaba/sdgs-desa',
|
||||
apbdes: '/darmasaba/apbdes',
|
||||
prestasidesa: '/darmasaba/prestasi-desa',
|
||||
pejabatdesa: '/darmasaba/profile/pejabat-desa',
|
||||
strukturppid: '/darmasaba/ppid/struktur-ppid',
|
||||
visimisippid: '/darmasaba/ppid/visi-misi',
|
||||
dasarhukumppid: '/darmasaba/ppid/dasar-hukum',
|
||||
profileppid: '/darmasaba/ppid/profile',
|
||||
daftarinformasipublik: '/darmasaba/ppid/daftar-informasi-publik',
|
||||
perbekeldarmasaba: '/darmasaba/desa/profile',
|
||||
berita: `/darmasaba/desa/berita/${kategori}/${id}`,
|
||||
pengumuman: `/darmasaba/desa/pengumuman/${kategori}/${id}`,
|
||||
sejarahdesa: '/darmasaba/desa/profile',
|
||||
visimisidesa: '/darmasaba/desa/profile',
|
||||
lambangdesa: '/darmasaba/desa/profile',
|
||||
maskotdesa: '/darmasaba/desa/profile',
|
||||
profilperbekel: '/darmasaba/desa/profile',
|
||||
potensi: '/darmasaba/desa/potensi-desa',
|
||||
galleryFoto: '/darmasaba/desa/gallery/foto',
|
||||
galleryVideo: '/darmasaba/desa/gallery/video',
|
||||
pelayananSuratKeterangan: '/darmasaba/desa/layanan',
|
||||
pelayananPerizinanBerusaha: '/darmasaba/desa/layanan',
|
||||
pelayananTelunjukSaktiDesa: '/darmasaba/desa/layanan',
|
||||
pelayananPendudukNonPermanent: '/darmasaba/desa/layanan',
|
||||
penghargaan: '/darmasaba/desa/penghargaan',
|
||||
posyandu: '/darmasaba/kesehatan/posyandu',
|
||||
fasilitasKesehatan: '/darmasaba/kesehatan/data-kesehatan-warga',
|
||||
jadwalKegiatan: '/darmasaba/kesehatan/data-kesehatan-warga',
|
||||
artikelKesehatan: '/darmasaba/kesehatan/data-kesehatan-warga',
|
||||
puskesmas: '/darmasaba/kesehatan/puskesmas',
|
||||
programKesehatan: '/darmasaba/kesehatan/program-kesehatan',
|
||||
penangananDarurat: '/darmasaba/kesehatan/penanganan-darurat',
|
||||
kontakDarurat: '/darmasaba/kesehatan/kontak-darurat',
|
||||
infoWabahPenyakit: '/darmasaba/kesehatan/info-wabah-penyakit',
|
||||
keamananLingkungan: '/darmasaba/keamanan/keamanan-lingkungan-pecalang-patwal',
|
||||
polsekTerdekat: '/darmasaba/keamanan/polsek-terdekat',
|
||||
kontakDaruratKeamanan: '/darmasaba/keamanan/kontak-darurat',
|
||||
pencegahanKriminalitas: '/darmasaba/keamanan/pencegahan-kriminalitas',
|
||||
laporanPublik: '/darmasaba/keamanan/laporan-publik',
|
||||
tipsKeamanan: '/darmasaba/keamanan/tips-keamanan',
|
||||
pasarDesa: '/darmasaba/ekonomi/pasar-desa',
|
||||
lowonganKerjaLokal: '/darmasaba/ekonomi/lowongan-kerja-lokal',
|
||||
strukturOrganisasi: '/darmasaba/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa',
|
||||
jumlahPendudukUsiaKerjaYangMenganggurUsia: '/darmasaba/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur',
|
||||
jumlahPendudukUsiaKerjaYangMenganggurPendidikan: '/darmasaba/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur',
|
||||
jumlahPendudukMiskin: '/darmasaba/ekonomi/jumlah-penduduk-miskin',
|
||||
programKemiskinan: '/darmasaba/ekonomi/program-kemiskinan',
|
||||
sektorUnggulanDesa: '/darmasaba/ekonomi/sektor-unggulan-desa',
|
||||
demografiPekerjaan: '/darmasaba/ekonomi/demografi-pekerjaan',
|
||||
};
|
||||
|
||||
return typeUrlMap[type || ''] || '/darmasaba';
|
||||
};
|
||||
|
||||
export default getDetailUrl;
|
||||
@@ -8,23 +8,28 @@ import colors from "@/con/colors";
|
||||
import SDGS from "./_com/main-page/sdgs";
|
||||
// import ApiFetch from "@/lib/api-fetch";
|
||||
|
||||
import { Stack } from "@mantine/core";
|
||||
import { Box, Stack } from "@mantine/core";
|
||||
import Apbdes from "./_com/main-page/apbdes";
|
||||
import Prestasi from "./_com/main-page/prestasi";
|
||||
import ScrollToTopButton from "./_com/scrollToTopButton";
|
||||
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Stack bg={colors.grey[1]} gap={"4rem"}>
|
||||
<LandingPage />
|
||||
<Penghargaan />
|
||||
<Layanan />
|
||||
<Potensi />
|
||||
<DesaAntiKorupsi />
|
||||
<Kepuasan />
|
||||
<SDGS />
|
||||
<Apbdes />
|
||||
<Prestasi/>
|
||||
</Stack>
|
||||
<Box>
|
||||
<Stack bg={colors.grey[1]} gap={"4rem"}>
|
||||
<LandingPage />
|
||||
<Penghargaan />
|
||||
<Layanan />
|
||||
<Potensi />
|
||||
<DesaAntiKorupsi />
|
||||
<Kepuasan />
|
||||
<SDGS />
|
||||
<Apbdes />
|
||||
<Prestasi />
|
||||
</Stack>
|
||||
{/* Tombol Scroll ke Atas */}
|
||||
<ScrollToTopButton />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user