Compare commits

...

13 Commits

Author SHA1 Message Date
f82c7b86e0 27 Oct 2025-10-27 10:54:50 +08:00
b5d6585cd5 27 Oct 2025-10-27 10:54:01 +08:00
aa98359ef7 Fix Revisi Kak Inno 22 Oktober && Fix Revisi Kak Ayu 22 Oktober 2025-10-23 17:45:45 +08:00
0ff0d5234a Fix QC Kak Inno 21 Oktober, QC Kak Ayu 21 Oktober, QC Keano, && QC Pak Jun 21 Oktober 2025-10-22 17:00:12 +08:00
827c1c191a Revisi QC Kak Inno tanggal 20 2025-10-22 09:58:16 +08:00
fb596f9033 Fix QC Kak Inno 17 Okt 25, Fix QC Kak Ayu 17 Okt 25, & Fix Qc Pak Jun 17 Okt 25 2025-10-21 12:17:30 +08:00
9055b40769 Fix navbar mobile add active page 2025-10-19 18:08:49 +08:00
bbf13c1cf7 Mengerjakan QC Kak Inno & Kak Ayu Tanggal 16 Oktober
Fix Search
2025-10-17 17:45:56 +08:00
75bf0652b1 Fix QC Kak Inno & Kak Ayu Tanggal 15 Oct 2025-10-17 10:03:03 +08:00
0b574406e2 Fix QC Kak Inno : tanggal 14 Oktober
Fitur Search bisa digunakan di 6 Menu, sisa 3 Menu Lagi
2025-10-15 17:29:57 +08:00
ccf39bc778 Penambahan fungsi search disetiap menu & submenu,
Menu Landing Page
Menu PPID
Menu Desa
2025-10-15 10:13:02 +08:00
3c21f7742c Yang sudh dikerjakan:
- Saat Mau minjam muncul modal data diri peminjam buku V
- Ada Status Peminjamannya ( status buku bisa engga otomatis dipinjemnya), kalau dikembalikan statusnya otomatis
)
Yang Mau Dikerjakan:
Cek fungsi menu yang kompleks
2025-10-14 10:38:55 +08:00
a158241c0b - QC User & Admin Menu Pendidikan V
- Fix SubMenu :
- Beasiswa Desa ( Baca Selengkapnya terdapatkan konten ) V
- Info Sekolah ( Kategori Menyesuaikan Dengan Datanya ) V
- Perpustakaan Digital (  V
- Kategori Menyesuaikan Dengan Datanya V
- Saat Mau minjam muncul modal data diri peminjam buku V
- Ada Status Peminjamannya V
)
2025-10-13 11:20:38 +08:00
127 changed files with 7703 additions and 1969 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -3,7 +3,7 @@
"version": "0.1.5",
"private": true,
"scripts": {
"dev": "bun --bun next dev --hostname 0.0.0.0",
"dev": "bun --bun next dev",
"build": "bun --bun next build",
"start": "bun --bun next start"
},
@@ -43,6 +43,7 @@
"@types/bun": "^1.2.2",
"@types/leaflet": "^1.9.20",
"@types/lodash": "^4.17.16",
"@types/nodemailer": "^7.0.2",
"add": "^2.0.6",
"adm-zip": "^0.5.16",
"animate.css": "^4.1.1",
@@ -52,6 +53,7 @@
"classnames": "^2.5.1",
"colors": "^1.4.0",
"dayjs": "^1.11.13",
"dotenv": "^17.2.3",
"elysia": "^1.3.5",
"embla-carousel-autoplay": "^8.5.2",
"embla-carousel-react": "^7.1.0",
@@ -71,6 +73,7 @@
"next": "^15.5.2",
"next-view-transitions": "^0.3.4",
"node-fetch": "^3.3.2",
"nodemailer": "^7.0.10",
"p-limit": "^6.2.0",
"primeicons": "^7.0.0",
"primereact": "^10.9.6",
@@ -82,6 +85,7 @@
"react-simple-toasts": "^6.1.0",
"react-toastify": "^11.0.5",
"react-transition-group": "^4.4.5",
"react-zoom-pan-pinch": "^3.7.0",
"readdirp": "^4.1.1",
"recharts": "^2.15.3",
"sharp": "^0.34.3",

View File

@@ -1,16 +1,4 @@
[
{
"id": "cmds8w2q60002vnbe6i8qhkuo",
"name": "Telephone Desa Darmasaba",
"iconUrl": "081239580000",
"imageId": "cmff3nv180003vn6h5jvedidq"
},
{
"id": "cmds8z7u20005vnbegyyvnbk0",
"name": "Email Desa Darmasaba",
"iconUrl": "desadarmasaba@badungkab.go.id",
"imageId": "cmff3ll130001vn6hkhls3f5y"
},
{
"id": "cmds9023u0008vnbe3oxmhwyf",
"name": "Desa Darmasaba",

View File

@@ -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())
@@ -1606,7 +1606,7 @@ model Pembiayaan {
ApbDesa ApbDesa[] @relation("ApbDesaPembiayaan")
}
// ========================================= INOVASI ========================================= //
// ========================================= MENU INOVASI ========================================= //
// ========================================= DESA DIGITAL / SMART VILLAGE ========================================= //
model DesaDigital {
id String @id @default(cuid())
@@ -1820,7 +1820,7 @@ model KategoriKegiatan {
isActive Boolean @default(true)
KegiatanDesa KegiatanDesa[]
}
// ========================================= EDUKASI LINGKUNGAN ========================================= //
model TujuanEdukasiLingkungan {
id String @id @default(cuid())
@@ -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 {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 266 KiB

After

Width:  |  Height:  |  Size: 378 KiB

View File

@@ -75,17 +75,18 @@ const berita = proxy({
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "", kategori = "") => {
berita.findMany.loading = true; // ✅ Akses langsung via nama path
const startTime = Date.now();
berita.findMany.loading = true;
berita.findMany.page = page;
berita.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
if (kategori) query.kategori = kategori;
const res = await ApiFetch.api.desa.berita["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
berita.findMany.data = res.data.data ?? [];
berita.findMany.totalPages = res.data.totalPages ?? 1;
@@ -98,9 +99,16 @@ const berita = proxy({
berita.findMany.data = [];
berita.findMany.totalPages = 1;
} finally {
berita.findMany.loading = false;
// pastikan minimal 300ms sebelum loading = false (biar UX smooth)
const elapsed = Date.now() - startTime;
const minDelay = 300;
const delay = elapsed < minDelay ? minDelay - elapsed : 0;
setTimeout(() => {
berita.findMany.loading = false;
}, delay);
}
},
},
},
findUnique: {

View File

@@ -55,46 +55,95 @@ const dataPerpustakaan = proxy({
},
},
findMany: {
data: null as
| Prisma.DataPerpustakaanGetPayload<{
include: {
image: true;
kategori: true;
};
}>[]
| null,
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "", kategori = "") => {
dataPerpustakaan.findMany.loading = true; // ✅ Akses langsung via nama path
dataPerpustakaan.findMany.page = page;
dataPerpustakaan.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
if (kategori) query.kategori = kategori;
const res = await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan["findMany"].get({ query });
if (res.status === 200 && res.data?.success) {
dataPerpustakaan.findMany.data = res.data.data ?? [];
dataPerpustakaan.findMany.totalPages = res.data.totalPages ?? 1;
} else {
dataPerpustakaan.findMany.data = [];
dataPerpustakaan.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch data perpustakaan paginated:", err);
data: null as
| Prisma.DataPerpustakaanGetPayload<{
include: {
image: true;
kategori: true;
};
}>[]
| null,
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "", kategori = "") => {
const startTime = Date.now();
dataPerpustakaan.findMany.loading = true; // ✅ Akses langsung via nama path
dataPerpustakaan.findMany.page = page;
dataPerpustakaan.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
if (kategori) query.kategori = kategori;
const res =
await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan[
"findMany"
].get({ query });
if (res.status === 200 && res.data?.success) {
dataPerpustakaan.findMany.data = res.data.data ?? [];
dataPerpustakaan.findMany.totalPages = res.data.totalPages ?? 1;
} else {
dataPerpustakaan.findMany.data = [];
dataPerpustakaan.findMany.totalPages = 1;
} finally {
dataPerpustakaan.findMany.loading = false;
}
},
} catch (err) {
console.error("Gagal fetch data perpustakaan paginated:", err);
dataPerpustakaan.findMany.data = [];
dataPerpustakaan.findMany.totalPages = 1;
} finally {
// pastikan minimal 300ms sebelum loading = false (biar UX smooth)
const elapsed = Date.now() - startTime;
const minDelay = 300;
const delay = elapsed < minDelay ? minDelay - elapsed : 0;
setTimeout(() => {
dataPerpustakaan.findMany.loading = false;
}, delay);
}
},
},
findManyAll: {
data: null as
| Prisma.DataPerpustakaanGetPayload<{
include: {
image: true;
kategori: true;
};
}>[]
| null,
loading: false,
search: "",
load: async (search = "", kategori = "") => {
dataPerpustakaan.findMany.loading = true; // ✅ Akses langsung via nama path
dataPerpustakaan.findMany.search = search;
try {
const query: any = {};
if (search) query.search = search;
if (kategori) query.kategori = kategori;
const res =
await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan[
"findManyAll"
].get({ query });
if (res.status === 200 && res.data?.success) {
dataPerpustakaan.findManyAll.data = res.data.data ?? [];
} else {
dataPerpustakaan.findManyAll.data = [];
}
} catch (err) {
console.error("Gagal fetch data perpustakaan paginated:", err);
dataPerpustakaan.findManyAll.data = [];
} finally {
dataPerpustakaan.findManyAll.loading = false;
}
},
},
findUnique: {
data: null as Prisma.DataPerpustakaanGetPayload<{
include: {
@@ -321,17 +370,20 @@ const kategoriBuku = proxy({
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
load: async (page = 1, limit = 10, search = "") => {
kategoriBuku.findMany.loading = true; // ✅ Akses langsung via nama path
kategoriBuku.findMany.page = page;
kategoriBuku.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.pendidikan.perpustakaandigital.kategoribuku["findMany"].get({ query });
const res =
await ApiFetch.api.pendidikan.perpustakaandigital.kategoribuku[
"findMany"
].get({ query });
if (res.status === 200 && res.data?.success) {
kategoriBuku.findMany.data = res.data.data ?? [];
kategoriBuku.findMany.totalPages = res.data.totalPages ?? 1;
@@ -514,9 +566,319 @@ 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;

View File

@@ -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<{

View File

@@ -27,7 +27,7 @@ function PelayananPendudukNonPermanent() {
);
useShallowEffect(() => {
pelayananPendudukNonPermanen.findById.load('1');
pelayananPendudukNonPermanen.findById.load('edit');
}, []);
if (!pelayananPendudukNonPermanen.findById.data) {

View File

@@ -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');

View File

@@ -55,9 +55,9 @@ function EditProgramKemiskinan() {
useEffect(() => {
if (!id) return;
stateProgram.findUnique
.load(id)
.then(() => {
const loadData = async () => {
try {
await stateProgram.findUnique.load(id);
const data = stateProgram.findUnique.data;
if (data) {
setFormData({
@@ -70,12 +70,16 @@ function EditProgramKemiskinan() {
},
});
}
})
.catch((err) => {
} catch (err) {
console.error('Error load data:', err);
toast.error('Gagal mengambil data program');
});
}, [id, stateProgram.findUnique]);
}
};
loadData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]); // ✅ hanya trigger saat id berubah
// generic handler untuk field top-level
const handleChange = useCallback(

View File

@@ -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"

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -65,7 +65,11 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
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

View File

@@ -72,7 +72,11 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
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

View File

@@ -79,7 +79,11 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
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 }}>

View File

@@ -73,7 +73,11 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
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

View File

@@ -4,7 +4,7 @@ import colors from '@/con/colors';
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);
@@ -65,7 +72,11 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -66,7 +66,11 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
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

View File

@@ -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}>

View File

@@ -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;

View File

@@ -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"
},
]
},
}
]

View File

@@ -132,7 +132,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
<Tooltip label="Keluar" position="bottom" withArrow>
<ActionIcon
onClick={() => {
router.push("/login");
router.push("/darmasaba");
}}
color={colors["blue-button"]}
radius="xl"

View File

@@ -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",

View File

@@ -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: [],
};
}
}

View File

@@ -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(

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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",
};
}

View File

@@ -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;

View File

@@ -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'),
}
}
}

View File

@@ -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;

View File

@@ -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
);
}
}

View File

@@ -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,

View File

@@ -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: [],
};
}
}

View File

@@ -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);

File diff suppressed because it is too large Load Diff

View 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;

View File

@@ -0,0 +1,162 @@
/* 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;
try {
const res = await ApiFetch.api.search.findMany.get({
query: {
query: searchState.query,
page: searchState.page,
limit: searchState.limit,
type: searchState.type,
},
});
console.log("Search API Response:", res);
const rawItems = res.data?.data || [];
const parsedItems = structuredClone(rawItems); // ✅ penting!
console.log("✅ Parsed items:", parsedItems);
if (searchState.page === 1) {
searchState.results = parsedItems;
} else {
searchState.results.push(...parsedItems);
}
console.log("Search results render:", searchState.results);
searchState.nextPage = res.data?.nextPage || null;
} catch (error) {
console.error("Search fetch error:", error);
} finally {
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;

View File

@@ -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") {

View File

@@ -0,0 +1,54 @@
import { NextResponse } from 'next/server';
import nodemailer from 'nodemailer';
export async function POST(request: Request) {
try {
const { email } = await request.json();
// Input validation
if (!email) {
return NextResponse.json(
{ success: false, message: 'Email is required' },
{ status: 400 }
);
}
// Email regex validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return NextResponse.json(
{ success: false, message: 'Invalid email format' },
{ status: 400 }
);
}
// Configure nodemailer
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS,
},
});
// Send email
await transporter.sendMail({
from: `"Tim Info" <${process.env.EMAIL_USER}>`,
to: email,
subject: '✅ Berhasil Berlangganan!',
html: `<p>Terima kasih telah berlangganan info terbaru dari kami!</p>`,
});
return NextResponse.json({
success: true,
message: 'Subscription successful! Please check your email.',
});
} catch (error) {
console.error('Error in subscribe API:', error);
return NextResponse.json(
{ success: false, message: 'Internal server error' },
{ status: 500 }
);
}
}

View File

@@ -62,11 +62,23 @@ function Page() {
Informasi dan Pelayanan Administrasi Digital
</Text>
</Box>
<Image src={state.findUnique.data?.image?.link || ''} alt='' w={"100%"} loading="lazy"/>
<Image src={state.findUnique.data?.image?.link || ''} alt='' w={"100%"} loading="lazy" />
</Container>
<Box px={{ base: "md", md: 100 }}>
<Stack gap={"xs"}>
<Text py={20} fz={{ base: "sm", md: "lg" }} ta={"justify"} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: state.findUnique.data?.content || '' }} />
<Text
py={20}
fz={{ base: "sm", md: "lg" }}
lh={{ base: 1.6, md: 1.8 }} // ✅ line-height lebih rapat dan responsif
ta="justify"
style={{
wordBreak: "break-word",
whiteSpace: "normal",
}}
dangerouslySetInnerHTML={{
__html: state.findUnique.data?.content || "",
}}
/>
</Stack>
</Box>
</Stack>

View File

@@ -6,122 +6,83 @@ import { useCallback, useEffect, useState } from 'react';
import ApiFetch from '@/lib/api-fetch';
interface FileItem {
id: string;
name: string;
link: string;
realName: string;
createdAt: string | Date;
category: string;
path: string;
mimeType: string;
}
export default function FotoContent() {
const [files, setFiles] = useState<FileItem[]>([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState('');
const [page, setPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
// Handle search and pagination changes
const loadData = useCallback((pageNum: number, searchTerm: string) => {
id: string;
name: string;
link: string;
realName: string;
createdAt: string | Date;
category: string;
path: string;
mimeType: string;
}
export default function FotoContent() {
const [files, setFiles] = useState<FileItem[]>([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState('');
const [page, setPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const limit = 9; // ✅ ambil 12 data per page
const loadData = useCallback(async (pageNum: number, searchTerm: string) => {
setLoading(true);
// Using the load function from the component's scope
const loadFn = async () => {
try {
const response = await ApiFetch.api.fileStorage.findMany.get({
query: {
category: 'image',
page: pageNum.toString(),
limit: '10',
...(searchTerm && { search: searchTerm })
}
});
try {
const query: Record<string, string> = {
category: 'image',
page: pageNum.toString(),
limit: limit.toString(),
};
if (searchTerm) query.search = searchTerm;
if (response.status === 200 && response.data) {
setFiles(response.data.data || []);
setTotalPages(response.data.meta?.totalPages || 1);
} else {
setFiles([]);
}
} catch (err) {
console.error('Load error:', err);
const response = await ApiFetch.api.fileStorage.findMany.get({ query });
if (response.status === 200 && response.data) {
setFiles(response.data.data || []);
setTotalPages(response.data.meta?.totalPages || 1);
} else {
setFiles([]);
} finally {
setLoading(false);
}
};
loadFn();
} catch (err) {
console.error('Load error:', err);
setFiles([]);
} finally {
setLoading(false);
}
}, []);
// Initial load and URL change handler
// Initial load + update when URL/search changes
useEffect(() => {
const handleRouteChange = () => {
const urlParams = new URLSearchParams(window.location.search);
const urlSearch = urlParams.get('search') || '';
const urlPage = parseInt(urlParams.get('page') || '1');
setSearch(urlSearch);
setPage(urlPage);
loadData(urlPage, urlSearch);
};
// Handle search updates from the search bar
const handleSearchUpdate = (e: Event) => {
const { search } = (e as CustomEvent).detail;
setSearch(search);
setPage(1); // Reset to first page on new search
setPage(1);
loadData(1, search);
};
// Initial load
handleRouteChange();
// Set up event listeners
window.addEventListener('popstate', handleRouteChange);
window.addEventListener('searchUpdate', handleSearchUpdate as EventListener);
// Cleanup
return () => {
window.removeEventListener('popstate', handleRouteChange);
window.removeEventListener('searchUpdate', handleSearchUpdate as EventListener);
};
}, [loadData]);
// ✅ Fetch data
// ✅ Update when page/search changes
useEffect(() => {
const fetchFiles = async () => {
setLoading(true);
try {
const query: Record<string, string> = {
category: 'image',
page: page.toString(),
limit: '10',
};
if (search) query.search = search;
loadData(page, search);
}, [page, search, loadData]);
const response = await ApiFetch.api.fileStorage.findMany.get({ query });
if (response.status === 200 && response.data) {
setFiles(response.data.data || []);
setTotalPages(response.data.meta?.totalPages || 1);
} else {
setFiles([]);
}
} catch (err) {
console.error('Fetch error:', err);
setFiles([]);
} finally {
setLoading(false);
}
};
if (page > 0) fetchFiles(); // jangan fetch jika page belum valid
}, [search, page]);
// ✅ Update URL
const updateURL = (newSearch: string, newPage: number) => {
const url = new URL(window.location.href);
if (newSearch) url.searchParams.set('search', newSearch);
@@ -148,7 +109,14 @@ interface FileItem {
<Box pt={20} px={{ base: 'md', md: 100 }}>
<SimpleGrid cols={{ base: 1, md: 3 }}>
{files.map((file) => (
<Paper key={file.id} mb={50} p="md" radius={26} bg={colors['white-trans-1']} style={{ height: '100%' }}>
<Paper
key={file.id}
mb={50}
p="md"
radius={26}
bg={colors['white-trans-1']}
style={{ height: '100%' }}
>
<Box style={{ height: '250px', overflow: 'hidden', borderRadius: '12px' }}>
<Image
src={file.link}
@@ -159,20 +127,18 @@ interface FileItem {
loading="lazy"
/>
</Box>
<Box>
<Stack gap="sm" py={10}>
<Text fw="bold" fz={{ base: 'h4', md: 'h3' }}>
{file.realName || file.name}
</Text>
<Text fz="sm" c="dimmed">
{new Date(file.createdAt).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'long',
year: 'numeric',
})}
</Text>
</Stack>
</Box>
<Stack gap="sm" py={10}>
<Text fw="bold" fz={{ base: 'h4', md: 'h3' }}>
{file.realName || file.name}
</Text>
<Text fz="sm" c="dimmed">
{new Date(file.createdAt).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'long',
year: 'numeric',
})}
</Text>
</Stack>
</Paper>
))}
</SimpleGrid>
@@ -181,4 +147,4 @@ interface FileItem {
</Center>
</Box>
);
}
}

View File

@@ -146,24 +146,24 @@ function Page() {
<Title order={3}>Ajukan Permohonan</Title>
<TextInput
label={<Text fz="sm" fw="bold">Nama</Text>}
placeholder="masukkan nama"
placeholder="Masukkan nama"
onChange={(val) => (stateCreate.create.form.nama = val.target.value)}
/>
<TextInput
type="number"
label={<Text fz="sm" fw="bold">NIK</Text>}
placeholder="masukkan NIK"
placeholder="Masukkan NIK"
onChange={(val) => (stateCreate.create.form.nik = val.target.value)}
/>
<TextInput
label={<Text fz="sm" fw="bold">Alamat</Text>}
placeholder="masukkan alamat"
placeholder="Masukkan alamat"
onChange={(val) => (stateCreate.create.form.alamat = val.target.value)}
/>
<TextInput
type="number"
label={<Text fz="sm" fw="bold">Nomor KK</Text>}
placeholder="masukkan Nomor KK"
placeholder="Masukkan Nomor KK"
onChange={(val) => (stateCreate.create.form.nomorKk = val.target.value)}
/>
<Select
@@ -186,12 +186,11 @@ function Page() {
stateCreate.create.form.kategoriId = '';
}
}}
searchable
// searchable
clearable
nothingFoundMessage="Tidak ditemukan"
required
/>
<Button bg={colors['blue-button']} onClick={handleSubmit}>
Simpan
</Button>

View File

@@ -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 {

View File

@@ -7,34 +7,52 @@ import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
function PelayananPerizinanBerusaha() {
const state = useProxy(stateLayananDesa)
const [loading, setLoading] = useState(false)
const [active, setActive] = useState(1);
const nextStep = () => setActive((current) => (current < 6 ? current + 1 : current));
const prevStep = () => setActive((current) => (current > 0 ? current - 1 : current));
const state = useProxy(stateLayananDesa);
const [loading, setLoading] = useState(false);
const [active, setActive] = useState(0);
const totalSteps = 6;
const nextStep = () => {
if (active < totalSteps - 1) {
setActive(active + 1);
} else if (active === totalSteps - 1) {
setActive(totalSteps); // Mark as completed
}
};
const prevStep = () => {
if (active > 0) {
setActive(active - 1);
}
};
useEffect(() => {
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 {
setLoading(false);
}
}
loadData()
}, [])
};
loadData();
}, []);
const data = state.pelayananPerizinanBerusaha.findById.data;
if (!data && !loading) {
return (
<Center mih={300}>
<Stack align="center" gap="sm">
<Text fz="lg" fw={500} c="dimmed">Belum ada informasi layanan yang tersedia</Text>
<Button component="a" href="https://oss.go.id" target="_blank" radius="xl">Kunjungi OSS</Button>
<Text fz="lg" fw={500} c="dimmed">
Belum ada informasi layanan yang tersedia
</Text>
<Button component="a" href="https://oss.go.id" target="_blank" radius="xl">
Kunjungi OSS
</Button>
</Stack>
</Center>
);
@@ -47,72 +65,111 @@ function PelayananPerizinanBerusaha() {
<Loader size="lg" color="blue" />
</Center>
) : (
<Stack gap="lg">
<Box>
<Title order={2} fw={700} fz={{ base: 22, md: 32 }} mb="sm">
Perizinan Berusaha Berbasis Risiko melalui OSS
</Title>
<Text fz={{ base: 'sm', md: 'md' }} c="dimmed">
Sistem Online Single Submission (OSS) untuk pendaftaran NIB
</Text>
</Box>
<Stack gap="lg">
<Box>
<Title order={2} fw={700} fz={{ base: 22, md: 32 }} mb="sm">
Perizinan Berusaha Berbasis Risiko melalui OSS
</Title>
<Text fz={{ base: 'sm', md: 'md' }} c="dimmed">
Sistem Online Single Submission (OSS) untuk pendaftaran NIB
</Text>
</Box>
<Text fz={{ base: 'sm', md: 'md' }} ta="justify" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: data?.deskripsi || '' }} />
<Text
fz={{ base: 'sm', md: 'md' }}
ta="justify"
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
dangerouslySetInnerHTML={{ __html: data?.deskripsi || '' }}
/>
<Box>
<Text fw={600} mb="sm" fz={{ base: 'sm', md: 'lg' }}>Alur pendaftaran NIB:</Text>
<Stepper active={active} onStepClick={setActive} orientation="vertical" color="blue" radius="md"
styles={{
step: { padding: '14px 0' },
stepBody: { marginLeft: 8 }
}}
>
<StepperStep label="Langkah 1" description="Daftar Akun">
<Text fz="sm">Membuat akun di portal OSS</Text>
</StepperStep>
<StepperStep label="Langkah 2" description="Isi Data Perusahaan">
<Text fz="sm">Lengkapi informasi perusahaan, data pemegang saham, dan alamat</Text>
</StepperStep>
<StepperStep label="Langkah 3" description="Pilih KBLI">
<Text fz="sm">Menentukan kode KBLI sesuai jenis usaha</Text>
</StepperStep>
<StepperStep label="Langkah 4" description="Unggah Dokumen">
<Text fz="sm">Unggah akta pendirian, surat izin, dan dokumen wajib lainnya</Text>
</StepperStep>
<StepperStep label="Langkah 5" description="Verifikasi Instansi">
<Text fz="sm">Menunggu verifikasi dan persetujuan dari pihak berwenang</Text>
</StepperStep>
<StepperStep label="Langkah 6" description="Terbit NIB">
<Text fz="sm">Menerima NIB sebagai identitas resmi usaha</Text>
</StepperStep>
<StepperCompleted>
<Center>
<Stack align="center" gap="xs">
<IconCheck size={40} color="green" />
<Text fz="sm" fw={500}>Proses pendaftaran selesai</Text>
</Stack>
</Center>
</StepperCompleted>
</Stepper>
<Box>
<Text fw={600} mb="sm" fz={{ base: 'sm', md: 'lg' }}>
Alur pendaftaran NIB:
</Text>
<Stepper
active={active}
onStepClick={(step) => {
if (step <= active) { // Only allow clicking on previous or current steps
setActive(step);
}
}}
orientation="vertical"
color="blue"
radius="md"
styles={{
step: { padding: '14px 0' },
stepBody: { marginLeft: 8 }
}}
>
<StepperStep label="Langkah 1" description="Daftar Akun">
<Text fz="sm">Membuat akun di portal OSS</Text>
</StepperStep>
<StepperStep label="Langkah 2" description="Isi Data Perusahaan">
<Text fz="sm">Lengkapi informasi perusahaan, data pemegang saham, dan alamat</Text>
</StepperStep>
<StepperStep label="Langkah 3" description="Pilih KBLI">
<Text fz="sm">Menentukan kode KBLI sesuai jenis usaha</Text>
</StepperStep>
<StepperStep label="Langkah 4" description="Unggah Dokumen">
<Text fz="sm">Unggah akta pendirian, surat izin, dan dokumen wajib lainnya</Text>
</StepperStep>
<StepperStep label="Langkah 5" description="Verifikasi Instansi">
<Text fz="sm">Menunggu verifikasi dan persetujuan dari pihak berwenang</Text>
</StepperStep>
<StepperStep label="Langkah 6" description="Terbit NIB">
<Text fz="sm">Menerima NIB sebagai identitas resmi usaha</Text>
</StepperStep>
<StepperCompleted>
<Center>
<Stack align="center" gap="xs">
<IconCheck size={40} color="green" />
<Text fz="sm" fw={500}>Proses pendaftaran selesai</Text>
</Stack>
</Center>
</StepperCompleted>
</Stepper>
{active < totalSteps && (
<Group justify="center" mt="lg">
<Button variant="light" leftSection={<IconArrowLeft size={18} />} onClick={prevStep} disabled={active === 0}>
<Button
variant="light"
leftSection={<IconArrowLeft size={18} />}
onClick={prevStep}
disabled={active === 0}
>
Kembali
</Button>
<Button rightSection={<IconArrowRight size={18} />} onClick={nextStep}>
Lanjut
</Button>
</Group>
</Box>
<Text fz="sm" ta="justify" c="dimmed" mt="md">
Catatan: Persyaratan dan prosedur dapat berubah sewaktu-waktu. Untuk informasi resmi terbaru, silakan kunjungi situs{" "}
<a href="https://oss.go.id/" target="_blank" rel="noopener noreferrer">oss.go.id</a> atau hubungi instansi pemerintah terkait.
</Text>
</Stack>
{active < totalSteps ? (
<Button
rightSection={active < totalSteps - 1 ? <IconArrowRight size={18} /> : null}
onClick={nextStep}
>
{active === totalSteps - 1 ? 'Selesai' : 'Lanjut'}
</Button>
) : (
<Button
variant="light"
onClick={() => setActive(0)}
>
Mulai Lagi
</Button>
)}
</Group>
)}
</Box>
<Text fz="sm" ta="justify" c="dimmed" mt="md">
Catatan: Persyaratan dan prosedur dapat berubah sewaktu-waktu. Untuk informasi resmi terbaru, silakan kunjungi situs{' '}
<a href="https://oss.go.id/" target="_blank" rel="noopener noreferrer">
oss.go.id
</a>{' '}
atau hubungi instansi pemerintah terkait.
</Text>
</Stack>
)}
</Box>
);
}
export default PelayananPerizinanBerusaha;
export default PelayananPerizinanBerusaha;

View File

@@ -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>

View File

@@ -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>
);
}

View File

@@ -1,167 +1,3 @@
// 'use client'
// import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
// import colors from '@/con/colors';
// import { Box, Grid, GridCol, Paper, SimpleGrid, Stack, Text, Title } from '@mantine/core';
// import { useProxy } from 'valtio/utils';
// import BackButton from '../../desa/layanan/_com/BackButto';
// import { useShallowEffect } from '@mantine/hooks';
// function Page() {
// const state = useProxy(PendapatanAsliDesa.ApbDesa);
// useShallowEffect(() => {
// state.findMany.load();
// }, []);
// useShallowEffect(() => {
// PendapatanAsliDesa.pembiayaan.findMany.load();
// PendapatanAsliDesa.belanja.findMany.load();
// PendapatanAsliDesa.pendapatan.findMany.load();
// }, []);
// // Get the latest APB data
// const latestApb = state.findMany.data?.[0];
// // Calculate totals
// const totalPendapatan = latestApb?.pendapatan?.reduce((sum, item) => sum + (item?.value || 0), 0) || 0;
// const totalBelanja = latestApb?.belanja?.reduce((sum, item) => sum + (item?.value || 0), 0) || 0;
// const totalPembiayaan = latestApb?.pembiayaan?.reduce((sum, item) => sum + (item?.value || 0), 0) || 0;
// return (
// <Stack pos="relative" bg={colors.Bg} py="xl" gap="lg">
// <Box px={{ base: 'md', md: 100 }}>
// <BackButton />
// </Box>
// <Text ta="center" fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw="bold">
// Pendapatan Asli Desa
// </Text>
// <Box px={{ base: "md", md: 100 }}>
// <Stack gap="lg" justify="center">
// <Paper bg={colors['white-1']} p="xl">
// <SimpleGrid cols={{ base: 1, md: 3 }} spacing="md">
// {/* Pendapatan Card */}
// <Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
// <Stack gap={"xs"}>
// <Title order={3}>Pendapatan</Title>
// {PendapatanAsliDesa.pendapatan.findMany.data?.map((item) => (
// <Box key={item.id}>
// <Grid>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="md" fw={500}>{item.name}</Text>
// </GridCol>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', {
// style: 'currency',
// currency: 'IDR',
// minimumFractionDigits: 0
// }).format(item.value)}</Text>
// </GridCol>
// </Grid>
// </Box>
// ))}
// <Grid>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="lg" fw={600} mb="xs">Total Pendapatan</Text>
// </GridCol>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="xl" fw={700} c={colors['blue-button']}>
// {new Intl.NumberFormat('id-ID', {
// style: 'currency',
// currency: 'IDR',
// minimumFractionDigits: 0
// }).format(totalPendapatan)}
// </Text>
// </GridCol>
// </Grid>
// </Stack>
// </Box>
// {/* Belanja Card */}
// <Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
// <Stack gap={"xs"}>
// <Title order={3}>Belanja</Title>
// {PendapatanAsliDesa.belanja.findMany.data?.map((item) => (
// <Box key={item.id}>
// <Grid>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="md" fw={500}>{item.name}</Text>
// </GridCol>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', {
// style: 'currency',
// currency: 'IDR',
// minimumFractionDigits: 0
// }).format(item.value)}</Text>
// </GridCol>
// </Grid>
// </Box>
// ))}
// <Grid>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="lg" fw={600} mb="xs">Total Belanja</Text>
// </GridCol>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="xl" fw={700} c="orange">
// {new Intl.NumberFormat('id-ID', {
// style: 'currency',
// currency: 'IDR',
// minimumFractionDigits: 0
// }).format(totalBelanja)}
// </Text>
// </GridCol>
// </Grid>
// </Stack>
// </Box>
// {/* Pembiayaan Card */}
// <Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
// <Stack gap={"xs"}>
// <Title order={3}>Pembiayaan</Title>
// {PendapatanAsliDesa.pembiayaan.findMany.data?.map((item) => (
// <Box key={item.id}>
// <Grid>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="md" fw={500}>{item.name}</Text>
// </GridCol>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', {
// style: 'currency',
// currency: 'IDR',
// minimumFractionDigits: 0
// }).format(item.value)}</Text>
// </GridCol>
// </Grid>
// </Box>
// ))}
// <Grid>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="lg" fw={600} mb="xs">Total Pembiayaan</Text>
// </GridCol>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="xl" fw={700} c="green">
// {new Intl.NumberFormat('id-ID', {
// style: 'currency',
// currency: 'IDR',
// minimumFractionDigits: 0
// }).format(totalPembiayaan)}
// </Text>
// </GridCol>
// </Grid>
// </Stack>
// </Box>
// </SimpleGrid>
// </Paper>
// </Stack>
// </Box>
// </Stack>
// );
// }
// export default Page;
'use client'
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
import colors from '@/con/colors';
@@ -206,32 +42,41 @@ function Page() {
<Stack gap="lg" justify="center">
<Paper bg={colors['white-1']} p="xl">
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="md">
{/* Pendapatan Card */}
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
{/* Pendapatan Card */}
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
<Stack gap={"xs"}>
<Title order={3}>Pendapatan</Title>
{PendapatanAsliDesa.pendapatan.findMany.data?.map((item) => (
{latestApb?.pendapatan?.map((item) => (
<Box key={item.id}>
<Grid>
<GridCol span={{ base: 12, md: 6 }}>
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text fz="md" fw={500}>{item.name}</Text>
</GridCol>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(item.value)}</Text>
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text
fz="md"
fw={500}
style={{
wordBreak: 'break-word',
whiteSpace: 'normal',
textAlign: 'right',
}}
>
{new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(item.value)}
</Text>
</GridCol>
</Grid>
</Box>
))}
<Grid>
<GridCol span={{ base: 12, md: 6 }}>
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text fz="lg" fw={600} mb="xs">Total Pendapatan</Text>
</GridCol>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="xl" fw={700} c={colors['blue-button']}>
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text style={{
wordBreak: 'break-word',
whiteSpace: 'normal'
}} fz="xl" fw={700} c={colors['blue-button']}>
{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
@@ -247,18 +92,28 @@ function Page() {
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
<Stack gap={"xs"}>
<Title order={3}>Belanja</Title>
{PendapatanAsliDesa.belanja.findMany.data?.map((item) => (
{latestApb?.belanja?.map((item) => (
<Box key={item.id}>
<Grid>
<GridCol span={{ base: 12, md: 6 }}>
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text fz="md" fw={500}>{item.name}</Text>
</GridCol>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(item.value)}</Text>
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text
fz="md"
fw={500}
style={{
wordBreak: 'break-word',
whiteSpace: 'normal',
textAlign: 'right',
}}
>
{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(item.value)}
</Text>
</GridCol>
</Grid>
</Box>
@@ -284,18 +139,28 @@ function Page() {
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
<Stack gap={"xs"}>
<Title order={3}>Pembiayaan</Title>
{PendapatanAsliDesa.pembiayaan.findMany.data?.map((item) => (
{latestApb?.pembiayaan?.map((item) => (
<Box key={item.id}>
<Grid>
<GridCol span={{ base: 12, md: 6 }}>
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text fz="md" fw={500}>{item.name}</Text>
</GridCol>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(item.value)}</Text>
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text
fz="md"
fw={500}
style={{
wordBreak: 'break-word',
whiteSpace: 'normal',
textAlign: 'right',
}}
>
{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(item.value)}
</Text>
</GridCol>
</Grid>
</Box>
@@ -366,5 +231,4 @@ function Page() {
);
}
export default Page;
export default Page;

View File

@@ -48,7 +48,7 @@ function Page() {
p={10}
mb={50}
h={400}
w={150}
w={Math.max(data.length * 120, 800)} // auto lebar sesuai jumlah data
data={data.map((item) => ({
id: item.id,
Pekerjaan: item.pekerjaan,

View File

@@ -72,21 +72,21 @@ function Page() {
)
}
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}>
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22" style={{ overflow: 'auto' }}>
<Box px={{ base: 'md', md: 50, lg: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }} >
<Box px={{ base: 'md', md: 50, lg: 100 }} >
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
Jumlah Penduduk Usia Kerja Yang Menganggur
</Text>
</Box>
<Box px={{ base: "md", md: 100 }}>
<Box px={{ base: "md", md: 50, lg: 100 }}>
<Stack gap={'lg'} justify='center'>
<Paper p={'lg'}>
<Text fw={'bold'} fz={'h3'}>Pengangguran Berdasarkan Usia</Text>
{mounted && donutGrafikNganggurData.length > 0 ? (<Box style={{ width: '100%', height: 'auto', minHeight: 200 }}>
<Box w="100%" style={{ maxWidth: 400, margin: "0 auto" }}>
<Box w="100%" maw={{ base: '100%', md: 400 }} mx="auto">
<PieChart
w="100%"
h={250} // lebih kecil biar aman di mobile
@@ -133,7 +133,7 @@ function Page() {
<Box w="100%" style={{ maxWidth: 400, margin: "0 auto" }}>
<PieChart
w="100%"
h={250} // lebih kecil biar aman di mobile
h="min(250px, 50vh)" // lebih kecil biar aman di mobile
withLabelsLine
labelsPosition="outside"
labelsType="percent"

View File

@@ -199,7 +199,7 @@ function Page() {
<TableTd ta={'center'}>{item.totalUnemployment}</TableTd>
<TableTd ta={'center'}>{item.educatedUnemployment}</TableTd>
<TableTd ta={'center'}>{item.uneducatedUnemployment}</TableTd>
<TableTd ta={'center'}>{item.percentageChange}</TableTd>
<TableTd ta={'center'}>{item.percentageChange}%</TableTd>
</TableTr>
))}
</TableTbody>

View File

@@ -0,0 +1,155 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, Image, Skeleton, Group, Badge, Divider } from '@mantine/core';
import { IconArrowBack, IconMapPin, IconPhone, IconStar } from '@tabler/icons-react';
import { useRouter, useParams } from 'next/navigation';
import React from 'react';
import { useProxy } from 'valtio/utils';
import { useShallowEffect } from '@mantine/hooks';
import pasarDesaState from '@/app/admin/(dashboard)/_state/ekonomi/pasar-desa/pasar-desa';
function DetailProdukPasarUser() {
const router = useRouter();
const params = useParams();
const statePasar = useProxy(pasarDesaState);
useShallowEffect(() => {
statePasar.pasarDesa.findUnique.load(params?.id as string);
}, []);
const data = statePasar.pasarDesa.findUnique.data;
if (!data) {
return (
<Stack py={10}>
<Skeleton height={400} radius="md" />
</Stack>
);
}
return (
<Box py={20}>
{/* Tombol kembali */}
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={20} color={colors['blue-button']} />}
mb={15}
>
Kembali ke daftar produk
</Button>
<Paper
w={{ base: '100%', md: '70%' }}
mx="auto"
p="lg"
radius="md"
shadow="sm"
bg={colors['white-1']}
>
<Stack gap="lg">
{/* Gambar Produk */}
{data.image?.link ? (
<Image
src={data.image.link}
alt={data.nama}
radius="md"
h={300}
w="100%"
fit="cover"
loading="lazy"
/>
) : (
<Box
h={300}
bg="gray.1"
display="flex"
style={{ alignItems: 'center', justifyContent: 'center', borderRadius: 'md' }}
>
<Text c="dimmed">Tidak ada gambar</Text>
</Box>
)}
{/* Detail Produk */}
<Stack gap="xs">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
{data.nama || 'Produk Tanpa Nama'}
</Text>
<Group>
<Badge color="green" size="lg" radius="md">
Rp {data.harga?.toLocaleString('id-ID')}
</Badge>
{data.rating && (
<Group gap={4}>
<IconStar size={18} color="#FFD43B" />
<Text fz="md" fw={500}>{data.rating}</Text>
</Group>
)}
</Group>
</Stack>
<Divider my="sm" />
{/* Info Tambahan */}
<Stack gap="sm">
<Box>
<Text fz="lg" fw={600}>Kategori</Text>
<Group gap="xs" mt={4}>
{data.KategoriToPasar && data.KategoriToPasar.length > 0 ? (
data.KategoriToPasar.map((kategori) => (
<Badge key={kategori.id} color="blue" variant="light">
{kategori.kategori.nama}
</Badge>
))
) : (
<Text fz="sm" c="dimmed">Tidak ada kategori</Text>
)}
</Group>
</Box>
{data.alamatUsaha && (
<Group gap={6}>
<IconMapPin size={18} color={colors['blue-button']} />
<Text fz="md">{data.alamatUsaha}</Text>
</Group>
)}
{data.kontak && (
<Group gap={6}>
<IconPhone size={18} color={colors['blue-button']} />
<Text fz="md">{data.kontak}</Text>
</Group>
)}
</Stack>
<Divider my="sm" />
{/* Deskripsi */}
<Box>
<Text fz="lg" fw={600}>Deskripsi Produk</Text>
<Text fz="md" c="dimmed" mt={4}>
Tidak ada deskripsi.
</Text>
</Box>
{/* Tombol Aksi User */}
{data.kontak && (
<Button
mt="md"
color="green"
size="lg"
radius="md"
component="a"
href={`https://wa.me/${data.kontak.replace(/[^0-9]/g, '')}`}
target="_blank"
>
Hubungi Penjual via WhatsApp
</Button>
)}
</Stack>
</Paper>
</Box>
);
}
export default DetailProdukPasarUser;

View File

@@ -105,7 +105,7 @@ function Page() {
return (
<Stack key={k}>
<motion.div
onClick={() => router.push(`https://wa.me/${v.kontak?.replace(/\D/g, '')}`)}
onClick={() => router.push(`/darmasaba/ekonomi/pasar-desa/${v.id}`)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.8 }}
>
@@ -117,7 +117,7 @@ function Page() {
h={200}
w='100%'
style={{ objectFit: 'cover' }}
loading="lazy"
loading="lazy"
/>
<Text py={10} fw={'bold'} fz={'lg'}>{v.nama}</Text>
<Text fz={'md'}>Rp {v.harga.toLocaleString('id-ID')}</Text>

View File

@@ -28,6 +28,32 @@ function Page() {
)
}
// Add this check before the return statement
if (data.length === 0) {
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap={22}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
<Text fz={{ base: 'h1', md: '2.5rem' }} c={colors['blue-button']} fw="bold">
Sektor Unggulan Desa Darmasaba
</Text>
<Text c="dimmed" mt="md">
Data sektor unggulan belum tersedia
</Text>
</Box>
</Stack>
);
}
const chartData = data
.filter(item => item?.name && typeof item.value === 'number')
.map((item) => ({
id: item.id,
sektor: item.name,
Ton: item.value,
}));
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}>
@@ -45,27 +71,34 @@ function Page() {
return (
<Paper p={'xl'} key={k}>
<Text fw={'bold'} fz={'h4'}>{v.name}</Text>
<Text fz={'h4'} ta={'justify'} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: v.description || '' }} />
<Text fz={'h4'} ta={'justify'} style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: v.description || '' }} />
</Paper>
)
})}
<Paper p={'xl'}>
<Text pb={10} fw={'bold'} fz={'h4'}>Statistik Sektor Unggulan Darmasaba</Text>
<BarChart
p={10}
h={300}
data={data.map((item) => ({
id: item.id,
sektor: item.name,
Ton: item.value,
}))}
dataKey="sektor"
series={[
{ name: 'Ton', color: colors['blue-button'] },
]}
tickLine="y"
/>
</Paper>
<Box style={{ width: '100%', overflowX: 'auto' }}>
<Paper p="xl">
<Text pb={10} fw="bold" fz="h4">Statistik Sektor Unggulan Darmasaba</Text>
<Box style={{ width: '100%', minWidth: '600px' }}>
<BarChart
p={10}
h={300}
data={chartData}
dataKey="sektor"
series={[
{ name: 'Ton', color: colors['blue-button'] },
]}
tickLine="y"
tooltipAnimationDuration={200}
withTooltip
style={{
fontFamily: 'inherit',
}}
xAxisLabel="Sektor"
yAxisLabel="Ton"
/>
</Box>
</Paper>
</Box>
</Stack>
</Box>
</Stack>

View File

@@ -1,14 +1,14 @@
'use client'
import { IconKey, IconMapper } from '@/app/admin/(dashboard)/_com/iconMap';
import programKreatifState from '@/app/admin/(dashboard)/_state/inovasi/program-kreatif';
import colors from '@/con/colors';
import { Box, Button, Center, Group, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
import { Box, Button, Center, Grid, GridCol, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconSearch } from '@tabler/icons-react';
import { useTransitionRouter } from 'next-view-transitions';
import React, { useState } from 'react';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto';
import { IconSearch } from '@tabler/icons-react';
import { IconKey, IconMapper } from '@/app/admin/(dashboard)/_com/iconMap';
// const data = [
// {
@@ -75,17 +75,23 @@ function Page() {
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }} >
<Group justify="space-between" mb="md" align='center'>
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
Program Kreatif Desa
</Text>
<TextInput
placeholder="Cari program kreatif..."
leftSection={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
</Group>
<Grid align='center'>
<GridCol span={{ base: 12, md: 9 }}>
<Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
Program Kreatif Desa
</Text>
</GridCol>
<GridCol span={{ base: 12, md: 3 }}>
<TextInput
radius={"lg"}
placeholder='Cari Program Kreatif'
value={search}
onChange={(e) => setSearch(e.target.value)}
leftSection={<IconSearch size={20} />}
w={{ base: "50%", md: "100%" }}
/>
</GridCol>
</Grid>
</Box>
<Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'} justify='center'>

View File

@@ -50,10 +50,12 @@ function Page() {
</Text>
</Box>
<TextInput
placeholder='Cari kontak darurat, nama, atau nomor...'
leftSection={<IconSearch size={20} />}
radius={"lg"}
placeholder='Cari Kontak Darurat'
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
onChange={(e) => setSearch(e.target.value)}
leftSection={<IconSearch size={20} />}
w={{ base: "100%", md: "25%" }}
/>
</Group>
<Box px={{ base: "md", md: 100 }}>
@@ -95,10 +97,12 @@ function Page() {
</Text>
</Box>
<TextInput
placeholder='Cari kontak darurat, nama, atau nomor...'
leftSection={<IconSearch size={20} />}
radius={"lg"}
placeholder='Cari Kontak Darurat'
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
onChange={(e) => setSearch(e.target.value)}
leftSection={<IconSearch size={20} />}
w={{ base: "50%", md: "100%" }}
/>
</Group>
<Box px={{ base: "md", md: 100 }}>

View File

@@ -1,15 +1,15 @@
'use client'
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import laporanPublikState from '@/app/admin/(dashboard)/_state/keamanan/laporan-publik';
import colors from '@/con/colors';
import { Box, Button, Center, ColorSwatch, Flex, Group, Modal, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from '@mantine/core';
import { Box, Button, Center, ColorSwatch, Flex, Group, Modal, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
import { DateTimePicker } from '@mantine/dates';
import { useDebouncedValue, useDisclosure, useShallowEffect } from '@mantine/hooks';
import { IconArrowRight, IconPlus } from '@tabler/icons-react';
import { IconArrowRight, IconPlus, IconSearch } from '@tabler/icons-react';
import { useTransitionRouter } from 'next-view-transitions';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto';
import { useTransitionRouter } from 'next-view-transitions';
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
function Page() {
const [search, setSearch] = useState("");
@@ -53,14 +53,17 @@ function Page() {
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}>
<Flex justify="space-between" align="center">
<Group justify="space-between" align="center">
<BackButton />
<TextInput
placeholder="Cari laporan"
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
</Flex>
radius={"lg"}
placeholder='Cari Laporan Publik'
value={search}
onChange={(e) => setSearch(e.target.value)}
leftSection={<IconSearch size={20} />}
w={{ base: "100%", md: "30%" }}
/>
</Group>
</Box>
<Box px={{ base: 'md', md: 100 }}>
<Group justify="space-between">
@@ -115,7 +118,7 @@ function Page() {
return (
<Paper radius={'lg'} key={k} bg={colors['white-trans-1']} p={'xl'}>
<Stack>
<Title c={colors['blue-button']} order={1}>{v.judul}</Title>
<Text c={colors['blue-button']} lineClamp={3} truncate="end" fz="h4" fw="bold">{v.judul}</Text>
<Text fs={'italic'} fz={'xl'}>
{v.tanggalWaktu
? new Date(v.tanggalWaktu).toLocaleString('id-ID')

View File

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

View File

@@ -84,9 +84,8 @@ function Page() {
</Box>
<Box>
<Text fz="h4" fw="bold">Kenali Gejala DBD</Text>
<Text fz="h4" fw="bold">{state.findUnique.data.symptom?.title}</Text>
<Divider my="xs" />
<Text fz="md" fw="semibold">{state.findUnique.data.symptom?.title}</Text>
<Text fz="md" lh={1.6} ta="justify" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: state.findUnique.data.symptom?.content }} />
</Box>

View File

@@ -3,9 +3,9 @@
import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
import colors from '@/con/colors';
import { ActionIcon, Anchor, AspectRatio, Badge, Box, Button, Card, Chip, CopyButton, Divider, Grid, Group, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, ThemeIcon, Title, Tooltip } from '@mantine/core';
import { ActionIcon, AspectRatio, Badge, Box, Button, Card, CopyButton, Divider, Grid, Group, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, ThemeIcon, Title, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconBrandWhatsapp, IconCheck, IconCopy, IconDeviceLandlinePhone, IconHeart, IconInfoCircle, IconMail, IconMapPin, IconMoodEmpty, IconSearch, IconStethoscope, IconUser, IconUsersGroup, IconWallet } from '@tabler/icons-react';
import { IconBrandWhatsapp, IconCheck, IconCopy, IconDeviceLandlinePhone, IconHeart, IconInfoCircle, IconMail, IconMapPin, IconMoodEmpty, IconSearch, IconUser } from '@tabler/icons-react';
import { useParams } from 'next/navigation';
import { useMemo } from 'react';
import { useProxy } from 'valtio/utils';
@@ -149,11 +149,6 @@ function Page() {
</CopyButton>
</Group>
</Group>
<Group gap="xs" mt="sm" wrap="wrap">
<Chip defaultChecked radius="xl" variant="light" icon={<IconStethoscope size={16} />}>Layanan Medis</Chip>
<Chip radius="xl" variant="light" icon={<IconUsersGroup size={16} />}>Ramah Keluarga</Chip>
<Chip radius="xl" variant="light" icon={<IconWallet size={16} />}>Pembayaran Non-Tunai</Chip>
</Group>
</Stack>
</Card>
</Box>
@@ -210,7 +205,6 @@ function Page() {
<Button variant="light" leftSection={<IconBrandWhatsapp size={18} />} component="a" href={`https://wa.me/${kontak.whatsapp.replace(/\D/g, '')}`} target="_blank" aria-label="Hubungi WhatsApp">WhatsApp</Button>
<Button variant="light" leftSection={<IconMail size={18} />} component="a" href={`mailto:${kontak.email}`} aria-label="Kirim Email">Email</Button>
</Group>
<Anchor target="_blank" underline="hover">Kunjungi situs resmi</Anchor>
</Stack>
</Card>
@@ -246,15 +240,8 @@ function Page() {
</Table>
</Stack>
</Card>
</Stack>
</Grid.Col>
</Grid>
</Box>
<Box px={{ base: 'md', md: 100 }}>
<Grid gutter="lg">
<Grid.Col span={{ base: 12, md: 8 }}>
<Card radius="xl" p="lg" withBorder>
<Card radius="xl" p="lg" withBorder>
<Stack gap="md">
<Title order={3}>Fasilitas Pendukung</Title>
<Divider />
@@ -270,8 +257,7 @@ function Page() {
)}
</Stack>
</Card>
</Grid.Col>
<Grid.Col span={{ base: 12, md: 4 }}>
<Card radius="xl" p="lg" withBorder>
<Stack gap="md">
<Title order={3}>Layanan & Tarif</Title>
@@ -309,10 +295,11 @@ function Page() {
)}
</Stack>
</Card>
</Stack>
</Grid.Col>
</Grid>
</Box>
<Box px={{ base: 'md', md: 100 }} pb="xl">
<Paper radius="xl" p="lg" withBorder>
<Stack gap="md">

View File

@@ -4,14 +4,16 @@ import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
import colors from '@/con/colors';
import {
Box,
Button,
Divider,
Group,
Modal,
Paper,
Skeleton,
Stack,
Text
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useDisclosure, useShallowEffect } from '@mantine/hooks';
import { IconMail, IconPhone, IconUser } from '@tabler/icons-react';
import { useParams } from 'next/navigation';
import { useProxy } from 'valtio/utils';
@@ -21,6 +23,7 @@ import CreatePendaftaran from '../create/page';
function Page() {
const params = useParams();
const state = useProxy(jadwalkegiatanState);
const [opened, { open, close }] = useDisclosure(false);
useShallowEffect(() => {
state.findUnique.load(params?.id as string);
@@ -66,28 +69,38 @@ function Page() {
<Stack gap="sm">
<Text fz="lg" fw="bold">Deskripsi Kegiatan</Text>
<Divider />
<Text ta="justify" fz="md" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: state.findUnique.data.deskripsijadwalkegiatan.deskripsi }} />
<Text ta="justify" fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.deskripsijadwalkegiatan.deskripsi }} />
</Stack>
<Stack gap="sm">
<Text fz="lg" fw="bold">Layanan yang Tersedia</Text>
<Divider />
<Text ta="justify" fz="md" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: state.findUnique.data.layananjadwalkegiatan.content }} />
<Text ta="justify" fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.layananjadwalkegiatan.content }} />
</Stack>
<Stack gap="sm">
<Text fz="lg" fw="bold">Syarat & Ketentuan</Text>
<Divider />
<Text ta="justify" fz="md" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: state.findUnique.data.syaratketentuanjadwalkegiatan.content }} />
<Text ta="justify" fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.syaratketentuanjadwalkegiatan.content }} />
</Stack>
<Stack gap="sm">
<Text fz="lg" fw="bold">Dokumen yang Perlu Dibawa</Text>
<Divider />
<Text ta="justify" fz="md" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: state.findUnique.data.dokumenjadwalkegiatan.content }} />
<Text ta="justify" fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.dokumenjadwalkegiatan.content }} />
</Stack>
<CreatePendaftaran />
<Stack gap="sm">
<Text fz="lg" fw="bold">Pendaftaran Kegiatan</Text>
<Divider />
<Group>
<Button onClick={open}>Buat Pendaftaran</Button>
</Group>
</Stack>
<Modal opened={opened} onClose={close}>
<CreatePendaftaran />
</Modal>
<Paper p="lg" radius="md" bg={colors['blue-button-trans']} shadow="sm">
<Stack gap="xs">

View File

@@ -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>

View File

@@ -121,13 +121,16 @@ function Page() {
</Badge>
</Group>
<Text fz="sm" c="dimmed">
Diposting: 12 Februari 2025 · Dinas Kesehatan
Diposting: {new Date(v.createdAt).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric',
})}
</Text>
<Divider />
<Text fz="sm" lh={1.5}>
{v.deskripsiSingkat}
</Text>
<Button variant="light" radius="md" size="md" onClick={() => router.push(`/admin/kesehatan/info-wabah-penyakit/${v.id}`)}>
<Text fz="sm" lh={1.5} lineClamp={3} truncate="end" dangerouslySetInnerHTML={{ __html: v.deskripsiSingkat }} />
<Button variant="light" radius="md" size="md" onClick={() => router.push(`/darmasaba/kesehatan/info-wabah-penyakit/${v.id}`)}>
Selengkapnya
</Button>
</Stack>

View File

@@ -101,27 +101,42 @@ function Page() {
}}
>
<Stack align="center" gap="sm">
<Image
src={v.image.link}
alt={v.name}
w={140}
h={140}
fit="contain"
radius="md"
loading="lazy"
/>
<Box
style={{
width: '100%',
aspectRatio: '16/9',
borderRadius: '12px',
overflow: 'hidden',
position: 'relative',
}}
>
<Image
src={v.image.link}
alt={v.name}
fit="cover"
loading="lazy"
style={{
width: '100%',
height: '100%',
transition: 'transform 0.4s ease',
}}
onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.05)')}
onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')}
/>
</Box>
<Text ta="center" fw={700} fz="lg" c={colors['blue-button']}>
{v.name}
</Text>
<Text fz="sm" c="dimmed" ta="center" lineClamp={3}>
<span style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: v.deskripsi }} />
</Text>
<Button
variant="light"
leftSection={<IconBrandWhatsapp size={18} />}
component="a"
href={`https://wa.me/${v.whatsapp.replace(/\D/g, '')}`}
target="_blank"
<Button
variant="light"
leftSection={<IconBrandWhatsapp size={18} />}
component="a"
href={`https://wa.me/${v.whatsapp.replace(/\D/g, '')}`}
target="_blank"
aria-label="Hubungi WhatsApp"
>WhatsApp</Button>
</Stack>

View File

@@ -2,7 +2,6 @@
import penangananDarurat from '@/app/admin/(dashboard)/_state/kesehatan/penanganan-darurat/penangananDarurat'
import colors from '@/con/colors'
import {
Badge,
Box,
Center,
Grid,
@@ -97,19 +96,43 @@ 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"
radius="md"
loading="lazy"
/>
<Box
style={{
width: '100%',
height: 180, // 🔥 tinggi fix biar semua seragam
borderRadius: 12,
overflow: 'hidden',
position: 'relative',
backgroundColor: '#f0f2f5', // fallback kalau gambar loading
}}
>
<Image
src={v.image?.link || '/img/default.png'}
alt={v.name}
fit="cover"
width="100%"
height="100%"
loading="lazy"
style={{
objectFit: 'cover',
objectPosition: 'center',
transition: 'transform 0.4s ease',
}}
onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.05)')}
onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')}
/>
</Box>
</Center>
<Stack gap={4} w="100%">
<Text
@@ -131,9 +154,6 @@ function Page() {
/>
</Box>
</Stack>
<Badge radius="md" color="blue" variant="light" mt="sm">
Darurat
</Badge>
</Stack>
</Paper>
))}
@@ -151,8 +171,11 @@ function Page() {
styles={{
control: {
border: `1px solid ${colors['blue-button']}`,
transition: 'all 0.3s ease',
'&:hover': { backgroundColor: colors['blue-button'], color: 'white' },
},
}}
/>
</Center>

View File

@@ -0,0 +1,120 @@
'use client';
import colors from '@/con/colors';
import { Button, Center, Flex, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconCalendar, IconInfoCircle, IconPhone } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import posyanduState from '@/app/admin/(dashboard)/_state/kesehatan/posyandu/posyandu';
export default function DetailPosyanduUser() {
const statePosyandu = useProxy(posyanduState);
const params = useParams();
const router = useRouter();
useShallowEffect(() => {
statePosyandu.findUnique.load(params?.id as string);
}, []);
if (!statePosyandu.findUnique.data) {
return (
<Stack py="xl" px={{ base: 'md', md: 100 }}>
<Skeleton height={500} radius="md" />
</Stack>
);
}
const data = statePosyandu.findUnique.data;
return (
<Stack pos="relative" bg={colors.Bg} py="xl" px={{ base: 'md', md: 100 }} gap="xl">
{/* Tombol Kembali */}
<Group>
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={22} color={colors['blue-button']} />}
mb="sm"
c={colors['blue-button']}
>
Kembali
</Button>
</Group>
<Paper
withBorder
p="xl"
radius="lg"
shadow="md"
bg={colors['white-trans-1']}
maw={800}
mx="auto"
>
<Stack gap="md">
{/* Header */}
<Text
ta="center"
fz={{ base: '1.8rem', md: '2.2rem' }}
fw={700}
c={colors['blue-button']}
>
{data.name || 'Posyandu Desa'}
</Text>
{/* Gambar */}
{data.image?.link ? (
<Center>
<Image
src={data.image.link}
alt={`Gambar ${data.name}`}
w="100%"
h={300}
radius="md"
fit="cover"
loading="lazy"
/>
</Center>
) : (
<Center>
<Text fz="sm" c="dimmed">
Tidak ada gambar
</Text>
</Center>
)}
{/* Info utama */}
<Stack gap="sm" mt="md">
<Flex align="flex-start" gap="xs">
<IconPhone size={18} stroke={1.5} style={{ marginTop: 3 }} />
<Text fz="sm" c="dimmed">
{data.nomor || 'Nomor tidak tersedia'}
</Text>
</Flex>
<Flex align="flex-start" gap="xs">
<IconCalendar size={18} stroke={1.5} style={{ marginTop: 3 }} />
<Text
fz="sm"
c="dimmed"
dangerouslySetInnerHTML={{ __html: data.jadwalPelayanan || '-' }}
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
/>
</Flex>
<Flex align="flex-start" gap="xs">
<IconInfoCircle size={18} stroke={1.5} style={{ marginTop: 3 }} />
<Text
fz="sm"
c="dimmed"
lh={1.6}
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
/>
</Flex>
</Stack>
</Stack>
</Paper>
</Stack>
);
}

View File

@@ -1,22 +1,25 @@
'use client'
import posyandustate from "@/app/admin/(dashboard)/_state/kesehatan/posyandu/posyandu";
import colors from "@/con/colors";
import { Badge, Box, Center, Flex, Group, Image, List, ListItem, Pagination, Paper, SimpleGrid, Skeleton, Spoiler, Stack, Text, TextInput } from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks";
import { Badge, Box, Button, Center, Flex, Group, Image, List, ListItem, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from "@mantine/core";
import { useDebouncedValue, useShallowEffect } from "@mantine/hooks";
import { IconCalendar, IconInfoCircle, IconPhone, IconSearch } 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";
export default function Page() {
const state = useProxy(posyandustate);
const [search, setSearch] = useState("");
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const router = useTransitionRouter()
const { data, page, totalPages, loading, load } = state.findMany;
useShallowEffect(() => {
load(page, 6, search);
}, [page, search]);
load(page, 6, debouncedSearch);
}, [page, debouncedSearch]);
if (loading || !data) {
return (
@@ -28,11 +31,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>
);
}
@@ -111,33 +134,41 @@ export default function Page() {
loading="lazy"
/>
</Center>
<Flex align="center" gap="xs">
<IconPhone size={18} stroke={1.5} />
<Text fz="sm" c="dimmed">
{v.nomor || "Tidak tersedia"}
</Text>
<Flex align="flex-start" gap="xs">
<IconPhone size={18} stroke={1.5} style={{ marginTop: 3 }} />
<Box>
<Text fz="sm" c="dimmed" lh={1.4}>
{v.nomor || "Tidak tersedia"}
</Text>
</Box>
</Flex>
<Flex align="center" gap="xs">
<IconCalendar size={18} stroke={1.5} />
<Text fz="sm" c="dimmed">
Jadwal:{" "}
<span style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: v.jadwalPelayanan }} />
</Text>
<Flex align="flex-start" gap="xs">
<IconCalendar size={18} stroke={1.5} style={{ marginTop: 3 }} />
<Box>
<Text fz="sm" c="dimmed" lh={1.4}>
<strong>Jadwal:</strong>{" "}
<span
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
dangerouslySetInnerHTML={{ __html: v.jadwalPelayanan }}
/>
</Text>
</Box>
</Flex>
<Spoiler
key={`spoiler-${v.id}`}
maxHeight={70}
showLabel="Lihat selengkapnya"
hideLabel="Sembunyikan"
transitionDuration={300}
>
<Flex align="flex-start" gap="xs">
<IconInfoCircle size={18} stroke={1.5} style={{ marginTop: 3 }} />
<Text
fz="sm"
lh={1.5}
c="dimmed"
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
lineClamp={3}
truncate="end"
/>
</Spoiler>
</Flex>
<Button radius="lg" size="md" variant="outline" onClick={() => router.push(`/darmasaba/kesehatan/posyandu/${v.id}`)}>Detail</Button>
</Stack>
</Paper>
))}

View File

@@ -126,17 +126,36 @@ export default function Page() {
className="hover-scale"
>
<Stack gap="md">
<Box h={180} w="100%">
<Image
src={v.image?.link}
alt={v.name}
radius="xl"
w="100%"
h="100%"
fit="cover"
loading="lazy"
/>
</Box>
<Center>
<Box
style={{
width: '100%',
height: 180, // 🔥 tinggi fix biar semua seragam
borderRadius: 12,
overflow: 'hidden',
position: 'relative',
backgroundColor: '#f0f2f5', // fallback kalau gambar loading
}}
>
<Image
src={v.image?.link || '/img/default.png'}
alt={v.name}
fit="cover"
width="100%"
height="100%"
loading="lazy"
style={{
objectFit: 'cover',
objectPosition: 'center',
transition: 'transform 0.4s ease',
}}
onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.05)')}
onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')}
/>
</Box>
</Center>
<Box px="lg" pb="lg">
<Text

View File

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

View File

@@ -1,5 +1,9 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import { motion, AnimatePresence } from 'framer-motion';
import { Transition } from '@mantine/core';
import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong';
import {
Badge,
@@ -23,12 +27,11 @@ import {
} from '@mantine/core';
import { IconArrowRight, IconCalendar } from '@tabler/icons-react';
import { useTransitionRouter } from 'next-view-transitions';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
export default function Content({ kategori }: { kategori: string }) {
const router = useTransitionRouter();
const [page, setPage] = useState(1);
const [animateKey, setAnimateKey] = useState(0);
const state = useProxy(gotongRoyongState.kegiatanDesa);
const featuredState = useProxy(gotongRoyongState.kegiatanDesa.findFirst);
@@ -37,119 +40,178 @@ export default function Content({ kategori }: { kategori: string }) {
const paginatedNews = state.findMany.data || [];
const totalPages = state.findMany.totalPages || 1;
// Load data
// Load data awal
useEffect(() => {
gotongRoyongState.kegiatanDesa.findFirst.load(kategori);
}, [kategori]);
// Load daftar berita
useEffect(() => {
state.findMany.load(page, 3, '', kategori);
setAnimateKey((prev) => prev + 1); // trigger animasi halus saat page berubah
}, [page, kategori]);
// Tampilan kosong
if (!featuredState.loading && !featured) {
return (
<Center py={100}>
<Stack align="center" gap="sm">
<Title order={3}>Belum Ada Data Gotong Royong</Title>
<Text c="dimmed">Tidak ada data gotong royong yang tersedia saat ini.</Text>
</Stack>
</Center>
);
}
return (
<Box py={20}>
<Container size="xl" px={{ base: 'md', md: 'xl' }}>
{/* === Gotong Royong Utama === */}
{featuredState.loading ? (
<Center><Skeleton h={400} /></Center>
) : featured ? (
<Box mb={50}>
<Text fz="h2" fw={700} mb="md">Gotong Royong Utama</Text>
<Paper shadow="md" radius="md" withBorder>
<Grid gutter={0}>
<GridCol span={{ base: 12, md: 6 }}>
<Image
src={featured.image?.link}
alt={featured.judul || 'Berita Utama'}
height={400}
fit="cover"
radius="md"
style={{ borderBottomRightRadius: 0, borderTopRightRadius: 0 }}
loading="lazy"
/>
</GridCol>
<GridCol span={{ base: 12, md: 6 }} p="xl">
<Stack h="100%" justify="space-between">
<div>
<Badge color="blue" variant="light" mb="md">
{featured.kategoriKegiatan?.nama || kategori}
</Badge>
<Title order={2} mb="md">{featured.judul}</Title>
<Text c="dimmed" lineClamp={3} mb="md" dangerouslySetInnerHTML={{ __html: featured.deskripsiLengkap }} />
</div>
<Group justify="apart" mt="auto">
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">
{new Date(featured.createdAt).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'long',
year: 'numeric',
})}
</Text>
</Group>
<Button
variant="light"
rightSection={<IconArrowRight size={16} />}
onClick={() => router.push(`/darmasaba/lingkungan/gotong-royong/${kategori}/${featured.id}`)}
>
Baca Selengkapnya
</Button>
</Group>
</Stack>
</GridCol>
</Grid>
</Paper>
</Box>
) : null}
<Transition mounted={!featuredState.loading} transition="fade" duration={250} timingFunction="ease">
{(styles) => (
<div style={styles}>
{featured ? (
<Box mb={50}>
<Text fz="h2" fw={700} mb="md">Gotong Royong Utama</Text>
<Paper shadow="md" radius="md" withBorder>
<Grid gutter={0}>
<GridCol span={{ base: 12, md: 6 }}>
<Image
src={featured.image?.link || '/images/placeholder.jpg'}
alt={featured.judul || 'Berita Utama'}
height={400}
fit="cover"
radius="md"
style={{ borderBottomRightRadius: 0, borderTopRightRadius: 0 }}
loading="lazy"
/>
</GridCol>
<GridCol span={{ base: 12, md: 6 }} p="xl">
<Stack h="100%" justify="space-between">
<div>
<Badge color="blue" variant="light" mb="md">
{featured.kategoriKegiatan?.nama || kategori}
</Badge>
<Title order={2} mb="md">{featured.judul}</Title>
<Text
c="dimmed"
lineClamp={3}
mb="md"
dangerouslySetInnerHTML={{ __html: featured.deskripsiLengkap }}
/>
</div>
<Group justify="apart" mt="auto">
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">
{new Date(featured.createdAt).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'long',
year: 'numeric',
})}
</Text>
</Group>
<Button
variant="light"
rightSection={<IconArrowRight size={16} />}
onClick={() =>
router.push(`/darmasaba/lingkungan/gotong-royong/${kategori}/${featured.id}`)
}
>
Baca Selengkapnya
</Button>
</Group>
</Stack>
</GridCol>
</Grid>
</Paper>
</Box>
) : (
<Skeleton h={400} radius="md" />
)}
</div>
)}
</Transition>
{/* === Daftar Gotong Royong === */}
{/* === Daftar Gotong Royong (Pagination + Fade-in Halus) === */}
<Box mt={50}>
<Title order={2} mb="md">Daftar Gotong Royong</Title>
<Divider mb="xl" />
{state.findMany.loading ? (
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl">
{Array(3).fill(0).map((_, i) => (
<Skeleton key={i} h={300} radius="md" />
))}
</SimpleGrid>
) : paginatedNews.length === 0 ? (
<Text c="dimmed" ta="center">Belum ada gotong royong di kategori &quot;{kategori}&quot;.</Text>
) : (
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl" verticalSpacing="xl">
{paginatedNews.map((item) => (
<Card
key={item.id}
shadow="sm"
p="lg"
radius="md"
withBorder
onClick={() => router.push(`/darmasaba/lingkungan/gotong-royong/${kategori}/${item.id}`)}
style={{ cursor: 'pointer' }}
>
<Card.Section>
<Image src={item.image?.link} height={200} alt={item.judul} fit="cover" loading="lazy"/>
</Card.Section>
<Badge color="blue" variant="light" mt="md">
{item.kategoriKegiatan?.nama || kategori}
</Badge>
<Text fw={600} size="lg" mt="sm" lineClamp={2}>{item.judul}</Text>
<Text size="sm" c="dimmed" lineClamp={3} style={{wordBreak: "break-word", whiteSpace: "normal"}} mt="xs" dangerouslySetInnerHTML={{ __html: item.deskripsiLengkap }} />
<Group justify="apart" mt="md" gap="xs">
<Text size="xs" c="dimmed">
{new Date(item.createdAt).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'short',
year: 'numeric',
})}
</Text>
<Badge color="gray" variant="outline">Baca Selengkapnya</Badge>
</Group>
</Card>
))}
</SimpleGrid>
)}
<AnimatePresence mode="wait">
<motion.div
key={animateKey}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.25, ease: 'easeInOut' }}
>
{state.findMany.loading ? (
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl">
{Array(3)
.fill(0)
.map((_, i) => (
<Skeleton key={i} h={300} radius="md" />
))}
</SimpleGrid>
) : paginatedNews.length === 0 ? (
<Center py={50}>
<Stack align="center" gap="sm">
<Title order={3}>Tidak Ada Data</Title>
<Text c="dimmed">Belum ada data gotong royong yang tersedia.</Text>
</Stack>
</Center>
) : (
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl" verticalSpacing="xl">
{paginatedNews.map((item) => (
<Card
key={item.id}
shadow="sm"
p="lg"
radius="md"
withBorder
onClick={() => router.push(`/darmasaba/lingkungan/gotong-royong/${kategori}/${item.id}`)}
style={{ cursor: 'pointer' }}
>
<Card.Section>
<Image
src={item.image?.link || '/images/placeholder-small.jpg'}
height={200}
alt={item.judul}
fit="cover"
loading="lazy"
/>
</Card.Section>
<Badge color="blue" variant="light" mt="md">
{item.kategoriKegiatan?.nama || kategori}
</Badge>
<Text fw={600} size="lg" mt="sm" lineClamp={2}>
{item.judul}
</Text>
<Text
size="sm"
c="dimmed"
lineClamp={3}
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
mt="xs"
dangerouslySetInnerHTML={{ __html: item.deskripsiLengkap }}
/>
<Group justify="apart" mt="md" gap="xs">
<Text size="xs" c="dimmed">
{new Date(item.createdAt).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'short',
year: 'numeric',
})}
</Text>
<Badge color="gray" variant="outline">Baca Selengkapnya</Badge>
</Group>
</Card>
))}
</SimpleGrid>
)}
</motion.div>
</AnimatePresence>
{/* Pagination */}
<Center mt="xl">
@@ -166,4 +228,4 @@ export default function Content({ kategori }: { kategori: string }) {
</Container>
</Box>
);
}
}

View File

@@ -1,169 +1,277 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
'use client';
import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong';
import { Badge, Box, Button, Card, Center, Container, Divider, Flex, Grid, GridCol, Group, Image, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, Title } from '@mantine/core';
import {
Badge,
Box,
Button,
Card,
Center,
Container,
Divider,
Flex,
Grid,
GridCol,
Group,
Image,
Pagination,
Paper,
SimpleGrid,
Skeleton,
Stack,
Text,
Title,
Transition,
} from '@mantine/core';
import { IconArrowRight, IconCalendar } from '@tabler/icons-react';
import { useTransitionRouter } from 'next-view-transitions';
import { useSearchParams } from 'next/navigation';
import { motion } from 'framer-motion';
import { useRouter, useSearchParams } from 'next/navigation';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
function Page() {
export default function Page() {
const searchParams = useSearchParams();
const router = useTransitionRouter();
const router = useRouter();
// Parameter URL
const search = searchParams.get('search') || '';
const page = parseInt(searchParams.get('page') || '1');
// Gunakan proxy untuk state
const state = useProxy(gotongRoyongState.kegiatanDesa);
const featured = useProxy(gotongRoyongState.kegiatanDesa.findFirst); // ✅ Berita utama
const featured = useProxy(gotongRoyongState.kegiatanDesa.findFirst);
const loadingGrid = state.findMany.loading;
const loadingFeatured = featured.loading;
// Load berita utama (hanya sekali)
// Load featured data once on component mount
useEffect(() => {
if (!featured.data && !loadingFeatured) {
gotongRoyongState.kegiatanDesa.findFirst.load();
}
}, [featured.data, loadingFeatured]);
let mounted = true;
const loadFeatured = async () => {
try {
if (!featured.data && !loadingFeatured) {
await gotongRoyongState.kegiatanDesa.findFirst.load();
}
} catch (error) {
console.error('Error loading featured data:', error);
}
};
if (mounted) {
loadFeatured();
}
return () => {
mounted = false;
};
}, []); // Empty dependency array to run only once on mount
// Load berita terbaru (untuk grid) saat page/search berubah
useEffect(() => {
const limit = 3; // Sesuaikan dengan tampilan grid
state.findMany.load(page, limit, search);
let mounted = true;
const loadData = async () => {
try {
const limit = 3;
await state.findMany.load(page, limit, search);
} catch (error) {
console.error('Error loading data:', error);
}
};
if (mounted) {
loadData();
}
return () => {
mounted = false;
};
}, [page, search]);
// Update URL saat page berubah
const handlePageChange = (newPage: number) => {
const url = new URLSearchParams(searchParams.toString());
if (search) url.set('search', search);
if (newPage > 1) url.set('page', newPage.toString());
else url.delete('page'); // biar page=1 ga muncul di URL
router.replace(`?${url.toString()}`);
else url.delete('page');
// Use push instead of replace to keep browser history
router.push(`?${url.toString()}`, { scroll: false });
};
const featuredData = featured.data;
const paginatedNews = state.findMany.data || [];
const totalPages = state.findMany.totalPages || 1;
return (
<Box py={20}>
<Container size="xl" px={{ base: "md", md: "xl" }}>
{/* === Gotong royong Utama (Tetap) === */}
{loadingFeatured ? (
<Center><Skeleton h={400} /></Center>
) : featuredData ? (
<Box mb={50}>
<Text fz="h2" fw={700} mb="md">Gotong royong Utama</Text>
<Paper shadow="md" radius="md" withBorder>
<Grid gutter={0}>
<GridCol span={{ base: 12, md: 6 }}>
<Image
src={featuredData.image?.link || '/images/placeholder.jpg'}
alt={featuredData.judul || 'Gotong royong Utama'}
height={400}
fit="cover"
radius="md"
style={{ borderBottomRightRadius: 0, borderTopRightRadius: 0 }}
loading="lazy"
/>
</GridCol>
<GridCol span={{ base: 12, md: 6 }} p="xl">
<Stack h="100%" justify="space-between">
<div>
<Badge color="blue" variant="light" mb="md">
{featuredData.kategoriKegiatan?.nama || 'Gotong royong'}
</Badge>
<Title order={2} mb="md">{featuredData.judul}</Title>
<Text c="dimmed" lineClamp={3} mb="md" dangerouslySetInnerHTML={{ __html: featuredData.deskripsiSingkat }} />
</div>
<Group justify="apart" mt="auto">
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">
{new Date(featuredData.createdAt).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'long',
year: 'numeric'
})}
</Text>
</Group>
<Button
variant="light"
rightSection={<IconArrowRight size={16} />}
onClick={() => router.push(`/darmasaba/lingkungan/gotong-royong/${featuredData.kategoriKegiatan?.nama}/${featuredData.id}`)}
>
Baca Selengkapnya
</Button>
</Group>
</Stack>
</GridCol>
</Grid>
</Paper>
</Box>
) : null}
// Animasi transisi halus tapi tetap instant load
const MotionBox = motion(Box as any);
{/* === Gotong royong Terbaru (Berubah Saat Pagination) === */}
// fallback kosong
if (!loadingGrid && !loadingFeatured && paginatedNews.length === 0) {
return (
<Container size="xl" py={80} ta="center">
<Title order={2} mb="md">Belum Ada Data Gotong Royong</Title>
<Text c="dimmed">Tidak ada data gotong royong yang tersedia saat ini.</Text>
</Container>
);
}
return (
<MotionBox
key={`${page}-${search}`}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2, ease: 'easeOut' }}
py={20}
>
<Container size="xl" px={{ base: 'md', md: 'xl' }}>
{/* === Gotong Royong Utama === */}
<Transition mounted={!loadingFeatured} transition="fade" duration={200} timingFunction="ease-out">
{(styles) =>
featuredData ? (
<Box mb={50} style={styles}>
<Text fz="h2" fw={700} mb="md">Gotong royong Utama</Text>
<Paper shadow="md" radius="md" withBorder>
<Grid gutter={0}>
<GridCol span={{ base: 12, md: 6 }}>
<Image
src={featuredData.image?.link || '/images/placeholder.jpg'}
alt={featuredData.judul || 'Gotong royong Utama'}
height={400}
fit="cover"
radius="md"
style={{ borderBottomRightRadius: 0, borderTopRightRadius: 0 }}
loading="lazy"
/>
</GridCol>
<GridCol span={{ base: 12, md: 6 }} p="xl">
<Stack h="100%" justify="space-between">
<div>
<Badge color="blue" variant="light" mb="md">
{featuredData.kategoriKegiatan?.nama || 'Gotong royong'}
</Badge>
<Title order={2} mb="md">{featuredData.judul}</Title>
<Text
c="dimmed"
lineClamp={3}
mb="md"
dangerouslySetInnerHTML={{ __html: featuredData.deskripsiSingkat }}
/>
</div>
<Group justify="apart" mt="auto">
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">
{new Date(featuredData.createdAt).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'long',
year: 'numeric',
})}
</Text>
</Group>
<Button
variant="light"
rightSection={<IconArrowRight size={16} />}
onClick={() =>
router.push(
`/darmasaba/lingkungan/gotong-royong/${featuredData.kategoriKegiatan?.nama}/${featuredData.id}`
)
}
>
Baca Selengkapnya
</Button>
</Group>
</Stack>
</GridCol>
</Grid>
</Paper>
</Box>
) : (
<Skeleton h={400} radius="md" mb="xl" />
)
}
</Transition>
{/* === Gotong royong Terbaru === */}
<Box mt={50}>
<Title order={2} mb="md">Gotong royong Terbaru</Title>
<Divider mb="xl" />
{loadingGrid ? (
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl">
{Array(3).fill(0).map((_, i) => (
<Skeleton key={i} h={300} radius="md" />
))}
</SimpleGrid>
) : paginatedNews.length === 0 ? (
<Text c="dimmed" ta="center">Tidak ada gotong royong ditemukan.</Text>
) : (
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl" verticalSpacing="xl">
{paginatedNews.map((item) => (
<Card
key={item.id}
shadow="sm"
p="lg"
radius="md"
withBorder
>
<Card.Section>
<Image
src={item.image?.link || '/images/placeholder-small.jpg'}
height={200}
alt={item.judul}
fit="cover"
loading="lazy"
/>
</Card.Section>
<Transition mounted={!loadingGrid} transition="fade" duration={200} timingFunction="ease-out">
{(styles) =>
loadingGrid ? (
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl">
{Array(3)
.fill(0)
.map((_, i) => (
<Skeleton key={i} h={300} radius="md" />
))}
</SimpleGrid>
) : paginatedNews.length === 0 ? (
<Text c="dimmed" ta="center">
Tidak ada gotong royong ditemukan.
</Text>
) : (
<Box style={styles}>
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl" verticalSpacing="xl">
{paginatedNews.map((item) => (
<Card key={item.id} shadow="sm" p="lg" radius="md" withBorder>
<Card.Section>
<Image
src={item.image?.link || '/images/placeholder-small.jpg'}
height={200}
alt={item.judul}
fit="cover"
loading="lazy"
/>
</Card.Section>
<Badge color="blue" variant="light" mt="md">
{item.kategoriKegiatan?.nama || 'Gotong royong'}
</Badge>
<Badge color="blue" variant="light" mt="md">
{item.kategoriKegiatan?.nama || 'Gotong royong'}
</Badge>
<Text fw={600} size="lg" mt="sm" lineClamp={2}>{item.judul}</Text>
<Text fw={600} size="lg" mt="sm" lineClamp={2}>
{item.judul}
</Text>
<Text size="sm" c="dimmed" lineClamp={3} mt="xs" dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat }} />
<Text
size="sm"
c="dimmed"
lineClamp={3}
mt="xs"
dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat }}
/>
<Flex align="center" justify="apart" mt="md" gap="xs">
<Text size="xs" c="dimmed">
{new Date(item.createdAt).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'short',
year: 'numeric'
})}
</Text>
<Button p="xs" variant="light" rightSection={<IconArrowRight size={16} />} onClick={() => router.push(`/darmasaba/lingkungan/gotong-royong/${item.kategoriKegiatan?.nama}/${item.id}`)}>Baca Selengkapnya</Button>
</Flex>
</Card>
))}
</SimpleGrid>
)}
<Flex align="center" justify="apart" mt="md" gap="xs">
<Text size="xs" c="dimmed">
{new Date(item.createdAt).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'short',
year: 'numeric',
})}
</Text>
{/* Pagination hanya untuk berita terbaru */}
<Button
p="xs"
variant="light"
rightSection={<IconArrowRight size={16} />}
onClick={() =>
router.push(
`/darmasaba/lingkungan/gotong-royong/${item.kategoriKegiatan?.nama}/${item.id}`
)
}
>
Baca Selengkapnya
</Button>
</Flex>
</Card>
))}
</SimpleGrid>
</Box>
)
}
</Transition>
{/* Pagination */}
<Center mt="xl">
<Pagination
total={totalPages}
@@ -176,9 +284,6 @@ function Page() {
</Center>
</Box>
</Container>
</Box>
</MotionBox>
);
}
export default Page;

View File

@@ -1,9 +1,9 @@
'use client'
import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah';
import colors from '@/con/colors';
import { Box, Center, Flex, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
import { Box, Center, Flex, Group, 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 { Icon, IconChartLine, IconClipboardTextFilled, IconLeaf, IconRecycle, IconRoute, IconScale, IconSearch, IconTent, IconTrashFilled, IconTrophy, IconTruckFilled } from '@tabler/icons-react';
import React, { useState } from 'react';
import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto';
@@ -122,20 +122,28 @@ function Page() {
<Stack gap="md">
{data2?.map((v, k) => (
<Paper key={k} p="md" withBorder radius="md">
<Text fw="bold" fz="lg">{v.namaTempatMaps}</Text>
<Text c="dimmed" fz="sm" mb="sm">{v.alamat}</Text>
{v.lat && v.lng ? (
<a
href={`https://www.google.com/maps/dir/?api=1&destination=${v.lat},${v.lng}`}
target="_blank"
rel="noopener noreferrer"
style={{ color: colors['blue-button'], textDecoration: 'none' }}
>
<Text fz="sm">📌 Buka di Google Maps</Text>
</a>
) : (
<Text c="dimmed" fz="sm">Koordinat belum tersedia</Text>
)}
<Group justify='space-between'>
<Box>
<Text fw="bold" fz="lg">{v.namaTempatMaps}</Text>
<Text c="dimmed" fz="sm" mb="sm">{v.alamat}</Text>
</Box>
<Box>
<IconRoute color={colors['blue-button']} size={30} />
<Text fw={"bold"} fz="sm" c={colors['blue-button']}>Rute</Text>
</Box>
</Group>
{v.lat && v.lng ? (
<a
href={`https://www.google.com/maps/dir/?api=1&destination=${v.lat},${v.lng}`}
target="_blank"
rel="noopener noreferrer"
style={{ color: colors['blue-button'], textDecoration: 'none' }}
>
<Text fz="sm">📌 Lihat Peta Lebih Besar</Text>
</a>
) : (
<Text c="dimmed" fz="sm">Koordinat belum tersedia</Text>
)}
</Paper>
))}
</Stack>

View File

@@ -74,7 +74,7 @@ function Page() {
<Title fz={55} fw={900} c={colors['blue-button']}>
Wujudkan Mimpi Pendidikanmu di Desa Darmasaba
</Title>
<Text fz="lg" mt="md" c="dimmed">
<Text fz="lg" mt="md" fw={"bold"}>
Program beasiswa untuk mendukung pendidikan berkualitas bagi generasi muda Desa Darmasaba.
</Text>
<Group mt="xl">

View File

@@ -15,7 +15,7 @@ import {
Timeline,
Title
} from '@mantine/core';
import { IconArrowLeft } from '@tabler/icons-react';
import { IconArrowLeft, IconChecklist, IconInfoCircle, IconQuote, IconSchool, IconTimeline, IconUserPlus } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useDisclosure } from '@mantine/hooks';
import { useProxy } from 'valtio/utils';
@@ -69,10 +69,11 @@ export default function BeasiswaPage() {
{/* 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">
<Group>
<IconSchool size={30} color={colors["blue-button"]} />
<Title order={2}>Program Beasiswa Pendidikan Desa Darmasaba</Title>
</Group>
<Text>
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>
@@ -81,21 +82,35 @@ export default function BeasiswaPage() {
{/* Tentang Program */}
<Container size="lg" py="xl">
<Title order={3} mb="sm">
Tentang Program
</Title>
<Group mb="sm">
<IconInfoCircle size={24} color={colors["blue-button"]} />
<Title order={3}>Tentang Program</Title>
</Group>
<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>
{/* Tambahkan info tahun berjalan di sini */}
<Paper mt="md" p="md" radius="lg" shadow="xs" bg="#f8fbff" withBorder>
<Text fw={500} c={colors["blue-button"]}>
📅 Periode Beasiswa Tahun 2025
</Text>
<Text fz="sm" c="dimmed">
Pendaftaran beasiswa dibuka mulai <strong>1 Januari 2025</strong> dan ditutup pada
<strong>31 Mei 2025</strong>.
Pengumuman hasil seleksi akan diumumkan pada pertengahan Juni 2025 melalui website resmi Desa Darmasaba.
</Text>
</Paper>
</Container>
{/* Syarat dan Ketentuan */}
<Container size="lg" py="xl">
<Title order={3} mb="sm">
Syarat Pendaftaran
</Title>
<Group mb="sm">
<IconChecklist size={24} color={colors["blue-button"]} />
<Title order={3}>Syarat Pendaftaran</Title>
</Group>
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="lg">
<Paper shadow="sm" p="md" radius="lg" withBorder>
@@ -123,42 +138,61 @@ export default function BeasiswaPage() {
{/* Proses Seleksi */}
<Container size="lg" py="xl">
<Title order={3} mb="sm">
Proses Seleksi
</Title>
<Group mb="sm">
<IconTimeline size={24} color={colors["blue-button"]} />
<Title order={3}>Proses Seleksi</Title>
</Group>
<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>
<Text size="sm" fw={500} c={colors["blue-button"]} mt={4}>
Estimasi waktu: 1 Februari 31 Mei 2025
</Text>
</Timeline.Item>
<Timeline.Item title="Seleksi Administrasi">
<Text c="dimmed" size="sm">
Panitia memverifikasi kelengkapan dan validitas berkas.
</Text>
<Text size="sm" fw={500} c={colors["blue-button"]} mt={4}>
Estimasi waktu: 57 hari kerja setelah penutupan pendaftaran
</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>
<Text size="sm" fw={500} c={colors["blue-button"]} mt={4}>
Estimasi waktu: 710 hari kerja setelah pengumuman seleksi administrasi
</Text>
</Timeline.Item>
<Timeline.Item title="Pengumuman Penerima">
<Text c="dimmed" size="sm">
Daftar penerima beasiswa diumumkan melalui website resmi Desa Darmasaba.
</Text>
<Text size="sm" fw={500} c={colors["blue-button"]} mt={4}>
Estimasi waktu: 5 hari kerja setelah tahap wawancara selesai
</Text>
</Timeline.Item>
</Timeline>
<Text c="dimmed" size="sm" mt="lg" ta="center">
🗓 Total estimasi keseluruhan proses: sekitar 34 minggu setelah penutupan pendaftaran
</Text>
</Container>
{/* Testimoni */}
<Container size="lg" py="xl">
<Title order={3} mb="sm">
Cerita Sukses Penerima Beasiswa
</Title>
<Group mb="sm">
<IconQuote size={24} color={colors["blue-button"]} />
<Title order={3}>Cerita Sukses Penerima Beasiswa</Title>
</Group>
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="lg">
<Paper shadow="md" p="lg" radius="lg">
@@ -183,7 +217,10 @@ export default function BeasiswaPage() {
{/* CTA Akhir */}
<Container size="lg" py="xl" ta="center">
<Title order={3}>Siap Bergabung dengan Program Ini?</Title>
<Group justify="center" mb="sm">
<IconUserPlus size={28} color={colors["blue-button"]} />
<Title order={3}>Siap Bergabung dengan Program Ini?</Title>
</Group>
<Text c="dimmed" mb="md">
Segera daftar dan wujudkan mimpimu bersama Desa Darmasaba.
</Text>

View File

@@ -1,7 +1,7 @@
'use client'
import stateBimbinganBelajarDesa from '@/app/admin/(dashboard)/_state/pendidikan/bimbingan-belajar-desa';
import colors from '@/con/colors';
import { Box, Paper, SimpleGrid, Skeleton, Stack, Text, Title, Tooltip, Divider, Badge } from '@mantine/core';
import { Box, Paper, SimpleGrid, Skeleton, Stack, Text, Title, Tooltip, Divider, Badge, Group } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useProxy } from 'valtio/utils';
import { IconMapPin, IconCalendarTime, IconBook2 } from '@tabler/icons-react';
@@ -49,46 +49,46 @@ function Page() {
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="xl">
<Paper p="xl" radius="lg" shadow="md" withBorder bg={colors['white-trans-1']}>
<Stack gap="sm">
<Box>
<Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
{stateTujuanProgram.findById.data?.judul}
</Badge>
<Group>
<Tooltip label="Gambaran manfaat utama program" position="top-start" withArrow>
<Box>
<IconBook2 size={36} stroke={1.5} color={colors['blue-button']} />
</Box>
</Tooltip>
</Box>
<Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
{stateTujuanProgram.findById.data?.judul}
</Badge>
</Group>
<Text fz="md" style={{wordBreak: "break-word", whiteSpace: "normal"}} lh={1.6} dangerouslySetInnerHTML={{ __html: stateTujuanProgram.findById.data?.deskripsi }} />
</Stack>
</Paper>
<Paper p="xl" radius="lg" shadow="md" withBorder bg={colors['white-trans-1']}>
<Stack gap="sm">
<Box>
<Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
{stateLokasiDanJadwal.findById.data?.judul}
</Badge>
<Group>
<Tooltip label="Tempat dan waktu pelaksanaan" position="top-start" withArrow>
<Box>
<IconMapPin size={36} stroke={1.5} color={colors['blue-button']} />
</Box>
</Tooltip>
</Box>
<Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
{stateLokasiDanJadwal.findById.data?.judul}
</Badge>
</Group>
<Text fz="md" lh={1.6} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: stateLokasiDanJadwal.findById.data?.deskripsi }} />
</Stack>
</Paper>
<Paper p="xl" radius="lg" shadow="md" withBorder bg={colors['white-trans-1']}>
<Stack gap="sm">
<Box>
<Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
{stateFasilitas.findById.data?.judul}
</Badge>
<Group>
<Tooltip label="Sarana yang disediakan untuk peserta" position="top-start" withArrow>
<Box>
<IconCalendarTime size={36} stroke={1.5} color={colors['blue-button']} />
</Box>
</Tooltip>
</Box>
<Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
{stateFasilitas.findById.data?.judul}
</Badge>
</Group>
<Text fz="md" lh={1.6} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: stateFasilitas.findById.data?.deskripsi }} />
</Stack>
</Paper>

View File

@@ -92,7 +92,7 @@ function Page() {
cursor={{ fill: 'var(--mantine-color-gray-1)' }}
/>
<Legend />
<Bar dataKey="jumlah" fill={colors['blue-button']} name="Jumlah Pendidikan" radius={[8, 8, 0, 0]} />
<Bar dataKey="jumlah" fill={colors['blue-button']} name="Jumlah Penduduk dengan Pendidikan" radius={[8, 8, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</Paper>

View File

@@ -84,7 +84,7 @@ function Page({ params }: PageProps) {
<TableTr>
<TableTh w="30%">Nama Pengajar</TableTh>
<TableTh w="30%">Nama Lembaga</TableTh>
<TableTh w="40%">Jenjang Pendidikan</TableTh>
<TableTh w="40%">Mengajar Di Jenjang Pendidikan</TableTh>
</TableTr>
</TableThead>
<TableTbody>
@@ -95,7 +95,7 @@ function Page({ params }: PageProps) {
<TableTd>{item.lembaga.jenjangPendidikan?.nama || '-'}</TableTd>
</TableTr>
))}
</TableTbody>
</TableTbody>
</Table>
)}
</Paper>

View File

@@ -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>

View File

@@ -1,7 +1,7 @@
'use client'
import pendidikanNonFormalState from '@/app/admin/(dashboard)/_state/pendidikan/pendidikan-non-formal';
import colors from '@/con/colors';
import { Box, Paper, SimpleGrid, Skeleton, Stack, Text, Title, Tooltip } from '@mantine/core';
import { Box, Group, Paper, SimpleGrid, Skeleton, Stack, Text, Title, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useProxy } from 'valtio/utils';
import { IconMapPin, IconTarget, IconBook2 } from '@tabler/icons-react';
@@ -43,7 +43,7 @@ function Page() {
Pendidikan Non Formal
</Title>
<Text ta="center" fz="lg" lh={1.6} c="black" maw={800} mx="auto">
Bentuk pendidikan di luar sekolah yang terstruktur, bertujuan memberikan keterampilan, pengetahuan, dan pengembangan karakter masyarakat dari berbagai usia serta latar belakang.
Pendidikan non formal merupakan bentuk pendidikan di luar sekolah yang terstruktur, bertujuan untuk memberikan keterampilan, pengetahuan, serta pengembangan karakter masyarakat dari berbagai usia dan latar belakang.
</Text>
</Box>
<SimpleGrid
@@ -59,13 +59,17 @@ function Page() {
withBorder
>
<Stack>
<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 }} />
<Group align="center" gap={8} wrap="nowrap">
<Tooltip label="Fokus utama program" withArrow>
<Box display="flex" style={{ alignItems: "center" }}>
<IconTarget color={colors['blue-button']} size={26} />
</Box>
</Tooltip>
<Text fw={700} fz="xl" c={colors['blue-button']}>
{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 }} />
</Text>
</Group>
<Text fz="md" lh={1.7} c="dark" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: stateTujuanPendidikanNonFormal.findById.data?.deskripsi }} />
</Stack>
</Paper>
<Paper
@@ -76,13 +80,17 @@ function Page() {
withBorder
>
<Stack>
<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 }} />
<Group align="center" gap={8} wrap="nowrap">
<Tooltip label="Lokasi pelaksanaan kegiatan" withArrow>
<Box display="flex" style={{ alignItems: "center" }}>
<IconMapPin color={colors['blue-button']} size={26} />
</Box>
</Tooltip>
<Text fw={700} fz="xl" c={colors['blue-button']}>
{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 }} />
</Text>
</Group>
<Text fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal" }} lh={1.7} c="dark" dangerouslySetInnerHTML={{ __html: stateTempatKegiatan.findById.data?.deskripsi }} />
</Stack>
</Paper>
</SimpleGrid>
@@ -95,13 +103,17 @@ function Page() {
withBorder
>
<Stack>
<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 }} />
<Group align="center" gap={8} wrap="nowrap">
<Tooltip label="Ragam jenis program yang tersedia" withArrow>
<Box display="flex" style={{ alignItems: "center" }}>
<IconBook2 color={colors['blue-button']} size={26} />
</Box>
</Tooltip>
<Text fw={700} fz="xl" c={colors['blue-button']}>
{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 }} />
</Text>
</Group>
<Text fz="md" lh={1.7} c="dark" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: stateJenisProgram.findById.data?.deskripsi }} />
</Stack>
</Paper>
</Box>

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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;

View File

@@ -0,0 +1,246 @@
'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);
const BATAS_HARI_PINJAM = 4;
// Reset form setiap modal dibuka
useEffect(() => {
if (opened && buku) {
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
/>
{/* === OTOMATIS SET BATAS DAN TANGGAL KEMBALI === */}
<DateInput
label="Tanggal Pinjam"
placeholder="Pilih tanggal pinjam"
value={
snap.create.form.tanggalPinjam
? new Date(snap.create.form.tanggalPinjam)
: null
}
onChange={(date) => {
if (date) {
const tanggalPinjam = new Date(date);
// simpan tanggal pinjam
perpustakaanDigitalState.peminjamanBuku.create.form.tanggalPinjam =
tanggalPinjam.toISOString();
// hitung batas +4 hari
const batasKembali = new Date(tanggalPinjam);
batasKembali.setDate(batasKembali.getDate() + BATAS_HARI_PINJAM);
// set batas & tanggal kembali otomatis
perpustakaanDigitalState.peminjamanBuku.create.form.batasKembali =
batasKembali.toISOString();
perpustakaanDigitalState.peminjamanBuku.create.form.tanggalKembali =
batasKembali.toISOString();
toast.info(
`Batas pengembalian otomatis diset ke ${batasKembali.toLocaleDateString('id-ID')} (+${BATAS_HARI_PINJAM} hari).`
);
} else {
perpustakaanDigitalState.peminjamanBuku.create.form.tanggalPinjam = '';
perpustakaanDigitalState.peminjamanBuku.create.form.batasKembali = '';
perpustakaanDigitalState.peminjamanBuku.create.form.tanggalKembali = '';
}
}}
required
/>
<Box>
<Text fw={500}>Catatan</Text>
<CreateEditor
value={snap.create.form.catatan}
onChange={(val) =>
(perpustakaanDigitalState.peminjamanBuku.create.form.catatan =
val)
}
/>
</Box>
<DateInput
label="Batas Pengembalian"
placeholder="Otomatis diatur +4 hari dari tanggal pinjam"
value={
snap.create.form.batasKembali
? new Date(snap.create.form.batasKembali)
: null
}
disabled
readOnly
/>
<DateInput
label="Tanggal Kembali"
placeholder="Otomatis sama dengan batas pengembalian"
value={
snap.create.form.tanggalKembali
? new Date(snap.create.form.tanggalKembali)
: null
}
disabled
readOnly
/>
<Button
onClick={handleSubmit}
loading={snap.create.loading}
disabled={
!snap.create.form.nama || !snap.create.form.tanggalPinjam
}
rightSection={<IconArrowRight size={16} />}
radius="xl"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Pinjam Buku
</Button>
</Stack>
) : (
<Text c="dimmed" ta="center">
Tidak ada data buku yang dipilih
</Text>
)}
</Modal>
);
}

View File

@@ -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,
3,
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;

View File

@@ -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>
);
}

View File

@@ -42,7 +42,7 @@ function Page() {
>
Dasar Hukum
</Text>
<Text ta="center" fz="sm" c="dimmed">
<Text ta="center" fz="md" >
Informasi regulasi dan kebijakan resmi yang menjadi dasar hukum
</Text>
</Stack>

View File

@@ -3,8 +3,8 @@
import indeksKepuasanState from "@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan";
import colors from "@/con/colors";
import { BarChart, PieChart } from '@mantine/charts';
import { Box, Button, Center, Container, Flex, Group, Modal, Paper, Select, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from "@mantine/core";
import { useDisclosure, useMediaQuery, useShallowEffect } from "@mantine/hooks";
import { Box, Button, Center, Container, Flex, Modal, Paper, Select, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from "@mantine/core";
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
import { useState } from "react";
import { useProxy } from "valtio/utils";
@@ -18,14 +18,13 @@ interface ChartDataItem {
function Kepuasan() {
const state = useProxy(indeksKepuasanState.responden);
const state = useProxy(indeksKepuasanState.responden);
const { data, loading } = state.findMany;
const [donutDataJenisKelamin, setDonutDataJenisKelamin] = useState<ChartDataItem[]>([]);
const [donutDataRating, setDonutDataRating] = useState<ChartDataItem[]>([]);
const [donutDataKelompokUmur, setDonutDataKelompokUmur] = useState<ChartDataItem[]>([]);
const [barChartData, setBarChartData] = useState<Array<{ month: string; count: number }>>([]);
const [opened, { open, close }] = useDisclosure(false);
const isMobile = useMediaQuery("(max-width: 768px)");
const [barChartData, setBarChartData] = useState<Array<{ month: string; Responden: number }>>([]);
const [opened, { open, close }] = useDisclosure(false)
const resetForm = () => {
state.create.form = {
@@ -122,18 +121,18 @@ function Kepuasan() {
// Convert map to array and sort by date
const barData = Array.from(monthYearMap.entries())
.map(([key, count]) => {
.map(([key, Responden]) => {
const [year, month] = key.split('-');
const monthName = new Date(Number(year), Number(month) - 1, 1)
.toLocaleString('id-ID', { month: 'long' });
return {
month: `${monthName} ${year}`,
count,
Responden,
sortKey: parseInt(`${year}${String(month).padStart(2, '0')}`, 10)
};
})
.sort((a, b) => a.sortKey - b.sortKey)
.map(({ month, count }) => ({ month, count }));
.map(({ month, Responden }) => ({ month, Responden }));
setBarChartData(barData);
}
@@ -141,12 +140,12 @@ function Kepuasan() {
if ((loading && !data) || !data) {
return (
<Stack py={10} px="sm">
<Skeleton height={200} mb="md" />
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="md">
<Skeleton height={200} />
<Skeleton height={200} />
<Skeleton height={200} />
<Stack py={10} px="xl">
<Skeleton height={300} mb="md" />
<SimpleGrid cols={{ base: 1, md: 3 }}>
<Skeleton height={300} />
<Skeleton height={300} />
<Skeleton height={300} />
</SimpleGrid>
</Stack>
);
@@ -157,10 +156,16 @@ function Kepuasan() {
<Stack p="sm">
<Container w={{ base: "100%", md: "80%" }} p={"xl"}>
<Center>
<Text ta={"center"} fz={{ base: "2.4rem", md: "3.4rem" }}>Indeks Kepuasan Masyarakat</Text>
<Text fw={"bold"} fz={{ base: "1.8rem", md: "3.4rem" }}>Indeks Kepuasan Masyarakat</Text>
</Center>
<Text ta={"center"} fz={{ base: "1rem", md: "1.3rem" }}>Ukur kebahagiaan warga, tingkatkan layanan desa! Dengan partisipasi aktif masyarakat, kami berkomitmen untuk terus memperbaiki layanan agar lebih transparan, efektif, dan sesuai dengan kebutuhan warga. Kepuasan Anda adalah prioritas utama kami dalam membangun desa yang lebih baik!</Text>
<Center mt={10}>
<Button radius={"lg"} bg={colors["blue-button"]} onClick={open}>Ajukan Responden</Button>
<Button
radius={"lg"}
onClick={open}
variant="gradient"
gradient={{ from: "#26667F", to: "#124170" }}
>Ajukan Responden</Button>
</Center>
</Container>
<Box px={"xl"}>
@@ -177,10 +182,10 @@ function Kepuasan() {
</Box>
</Flex>
<BarChart
h={300}
h={window.innerWidth < 480 ? 200 : 300}
data={barChartData}
dataKey="month"
series={[{ name: 'count', color: colors['blue-button'] }]}
series={[{ name: 'Responden', color: colors['blue-button'] }]}
tickLine="y"
xAxisLabel="Bulan"
yAxisLabel="Jumlah Responden"
@@ -191,12 +196,9 @@ function Kepuasan() {
</Paper>
<Box py={"xl"}>
<SimpleGrid
cols={{
base: 1,
md: 1,
lg: 1,
xl: 3
}}
cols={{ base: 1, sm: 2, lg: 3 }}
spacing="md"
verticalSpacing="md"
>
{/* Chart Jenis Kelamin */}
<Paper bg={colors['white-1']} p="md" radius="md">
@@ -215,7 +217,7 @@ function Kepuasan() {
withLabels
withTooltip
labelsType="percent"
size={200}
size={250} // Fixed size in pixels
data={donutDataJenisKelamin}
/>
</Center>
@@ -254,7 +256,7 @@ function Kepuasan() {
labelsPosition="outside"
labelsType="percent"
withLabelsLine
size={200}
size={250}
data={donutDataRating}
/>
</Center>
@@ -297,7 +299,7 @@ function Kepuasan() {
labelsPosition="outside"
labelsType="percent"
withLabelsLine
size={190}
size={250}
data={donutDataKelompokUmur}
/>
</Center>
@@ -330,7 +332,7 @@ function Kepuasan() {
<TextInput
label="Nama"
type='text'
placeholder="masukkan nama"
placeholder="Masukkan nama"
defaultValue={state.create.form.name}
onChange={(val) => {
state.create.form.name = val.currentTarget.value;
@@ -413,41 +415,57 @@ function Kepuasan() {
);
}
return (
<Stack p="sm">
<Container w={{ base: "100%", md: "80%" }} p={isMobile ? "md" : "xl"}>
<Stack gap="xs">
<Text ta="center" fz={{ base: "2rem", md: "3rem" }}>Indeks Kepuasan Masyarakat</Text>
<Group justify="center">
<Button radius="lg" bg={colors["blue-button"]} onClick={open}>
Ajukan Responden
</Button>
</Group>
</Stack>
<Stack p={"sm"}>
<Container size="lg" px="md">
<Center>
<Text ta={"center"} fz={{ base: "2.4rem", md: "3.4rem" }}>Indeks Kepuasan Masyarakat</Text>
</Center>
<Text fz={{ base: "1.2rem", md: "1.4rem" }} ta={"center"}>Ukur kebahagiaan warga, tingkatkan layanan desa! Dengan partisipasi aktif masyarakat, kami berkomitmen untuk terus memperbaiki layanan agar lebih transparan, efektif, dan sesuai dengan kebutuhan warga. Kepuasan Anda adalah prioritas utama kami dalam membangun desa yang lebih baik!</Text>
<Center mt={10}>
<Button radius={"lg"} bg={colors["blue-button"]} onClick={open}>Ajukan Responden</Button>
</Center>
</Container>
<Box px={isMobile ? "sm" : "xl"}>
<Paper p="lg" bg={colors.Bg}>
<Paper p={isMobile ? "sm" : "lg"}>
<Stack gap="xs">
<Flex direction={isMobile ? "column" : "row"} justify="space-between" align={isMobile ? "start" : "center"}>
<Text fw="bold" mb={isMobile ? "sm" : 0}>Pelayanan Terhadap Publik Desa Darmasaba</Text>
<Box>
<Text fz="sm" fw="bold" c={colors["blue-button"]}>Total Responden</Text>
<Text ta="end" fz="h1" fw="bold" c={colors["blue-button"]}>
{state.findMany.total.toLocaleString("id-ID")}
<Box px={"xl"}>
<Paper p={"lg"} bg={colors.Bg}>
<Paper p={"lg"}>
<Stack gap={"xs"}>
<Flex
direction={{ base: "column", sm: "row" }}
justify="space-between"
align={{ base: "flex-start", sm: "center" }}
>
<Text fw="bold" ta={{ base: "center", sm: "left" }}>
Pelayanan Terhadap Publik Desa Darmasaba
</Text>
<Box mt={{ base: "sm", sm: 0 }}>
<Text fz={"sm"} fw={"bold"} c={colors["blue-button"]}>Total Responden</Text>
<Text ta={"end"} fz={"h1"} fw={"bold"} c={colors["blue-button"]}>
{state.findMany.total.toLocaleString('id-ID')}
</Text>
</Box>
</Flex>
<BarChart
h={isMobile ? 200 : 300}
h={300}
data={barChartData}
dataKey="month"
series={[{ name: "count", color: colors["blue-button"] }]}
series={[{ name: 'Responden', color: colors['blue-button'] }]}
tickLine="y"
xAxisLabel="Bulan"
yAxisLabel="Jumlah Responden"
withTooltip
tooltipAnimationDuration={200}
/>
</Stack>
</Paper>
<Box py="xl">
<SimpleGrid cols={{ base: 1, sm: 2, xl: 3 }} spacing="lg">
<Box py={"xl"}>
<SimpleGrid
cols={{
base: 1,
md: 1,
lg: 1,
xl: 3
}}
>
{/* Chart Jenis Kelamin */}
<Paper bg={colors['white-1']} p="md" radius="md">
<Stack>
@@ -457,17 +475,28 @@ function Kepuasan() {
Belum ada data untuk ditampilkan dalam grafik
</Text>
) : (
<Paper p="md" radius="md">
<Stack>
<Center>
<PieChart
size={isMobile ? 150 : 200}
withLabels
data={donutDataJenisKelamin}
withTooltip
/>
</Center>
</Stack>
<Paper p="md" radius="md" withBorder>
<Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<Box style={{ position: 'relative', width: '100%' }}>
<Center>
<PieChart
withLabels
withTooltip
labelsType="percent"
size={200}
data={donutDataJenisKelamin}
/>
</Center>
</Box>
<Stack gap="sm" mt="md">
{donutDataJenisKelamin.map((entry) => (
<Flex key={entry.name} gap="md" align="center">
<Box bg={entry.color} w={20} h={20} style={{ flexShrink: 0 }} />
<Text size="sm">{entry.name}: {entry.value}</Text>
</Flex>
))}
</Stack>
</Box>
</Paper>
)}
</Stack>
@@ -482,18 +511,35 @@ function Kepuasan() {
Belum ada data untuk ditampilkan dalam grafik
</Text>
) : (
<Paper p="md" radius="md">
<Stack>
<Center>
<PieChart
size={isMobile ? 150 : 200}
withLabels
labelsPosition="outside"
withLabelsLine
data={donutDataRating}
/>
</Center>
</Stack>
<Paper p="md" radius="md" withBorder>
<Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<Box style={{ position: 'relative', width: '100%' }}>
<Center>
<PieChart
withTooltip
tooltipAnimationDuration={200}
withLabels
labelsPosition="outside"
labelsType="percent"
withLabelsLine
size={200}
data={donutDataRating}
/>
</Center>
</Box>
<Box mt="md" style={{ width: '100%' }}>
<SimpleGrid cols={2} spacing="xs" verticalSpacing="xs">
{donutDataRating.map((entry) => (
<Flex key={entry.name} gap="sm" align="center" style={{ overflow: 'hidden' }}>
<Box bg={entry.color} w={16} h={16} style={{ flexShrink: 0 }} />
<Text size="xs" lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{entry.name}: {entry.value}
</Text>
</Flex>
))}
</SimpleGrid>
</Box>
</Box>
</Paper>
)}
</Stack>
@@ -508,18 +554,35 @@ function Kepuasan() {
Belum ada data untuk ditampilkan dalam grafik
</Text>
) : (
<Paper p="md" radius="md">
<Stack>
<Center>
<PieChart
size={isMobile ? 150 : 200}
withLabels
labelsPosition="outside"
withLabelsLine
data={donutDataKelompokUmur}
/>
</Center>
</Stack>
<Paper p="md" radius="md" withBorder>
<Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<Box style={{ position: 'relative', width: '100%' }}>
<Center>
<PieChart
withTooltip
tooltipAnimationDuration={200}
withLabels
labelsPosition="outside"
labelsType="percent"
withLabelsLine
size={190}
data={donutDataKelompokUmur}
/>
</Center>
</Box>
<Box mt="md" style={{ width: '100%' }}>
<SimpleGrid cols={2} spacing="xs" verticalSpacing="xs">
{donutDataKelompokUmur.map((entry) => (
<Flex key={entry.name} gap="sm" align="center" style={{ overflow: 'hidden' }}>
<Box bg={entry.color} w={16} h={16} style={{ flexShrink: 0 }} />
<Text size="xs" lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{entry.name}: {entry.value}
</Text>
</Flex>
))}
</SimpleGrid>
</Box>
</Box>
</Paper>
)}
</Stack>
@@ -542,7 +605,7 @@ function Kepuasan() {
}}
/>
<TextInput
label="Tanggal"
label="Tanggal Pengisian"
type="date"
placeholder="masukkan tanggal"
defaultValue={state.create.form.tanggal}

View File

@@ -32,7 +32,7 @@ export default function JenisInformasiSelector({ onChange }: {
return (
<Group>
<Select
placeholder='pilih jenis informasi'
placeholder='Pilih jenis informasi'
label='Jenis Informasi'
data={data.map((item) => ({
value: item.id,

View File

@@ -28,7 +28,7 @@ function MemperolehInformasi({ onChange }: {
return (
<Group>
<Select
placeholder='pilih cara memperoleh informasi'
placeholder='Pilih cara memperoleh informasi'
label={"Cara Memperoleh Informasi"}
data={data.map((item) => ({
value: item.id,

View File

@@ -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"

View File

@@ -26,7 +26,7 @@ function MemperolehSalinan({ onChange }: {
return (
<Group>
<Select
placeholder='pilih cara memperoleh salinan informasi'
placeholder='Pilih cara memperoleh salinan informasi'
label={'Cara Memperoleh Salinan Informasi'}
data={data.map((item) => ({
value: item.id,

View File

@@ -178,7 +178,7 @@ function Page() {
<TextInput
label="Alamat Email"
placeholder="contoh: nama@email.com"
placeholder="Contoh: nama@email.com"
radius="md"
size="md"
type="email"
@@ -190,7 +190,7 @@ function Page() {
<TextInput
label="Nomor Telepon"
placeholder="contoh: 0812-3456-7890"
placeholder="Contoh: 0812-3456-7890"
radius="md"
size="md"
withAsterisk

View File

@@ -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,109 @@ 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 }}>
<Center>
<Image loading='lazy' src="/darmasaba-icon.png" h={{ base: 70, md: 120 }} w={{ base: 70, md: 120 }} alt="Logo Desa" />
</Center>
<Text ta="center" fz={{ base: "1.2rem", md: "2rem", lg: "2.5rem", xl: "3rem" }} fw="bold">
Pejabat Pengelola Informasi dan Dokumentasi
</Text>
</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="xl" shadow="md" withBorder>
<Stack gap={0}>
<Image
pt={{ base: 0, md: 100 }}
px="lg"
src={item.image?.link ? `${item.image.link}?t=${Date.now()}` : "/perbekel.png"}
alt="Foto Pimpinan"
radius="lg"
onError={(e) => e.currentTarget.src = "/perbekel.png"}
loading="lazy"
/>
<Paper
bg={colors['blue-button']}
px="lg"
radius="0 0 var(--mantine-radius-xl) var(--mantine-radius-xl)"
className="glass3"
py={{ base: 20, md: 50 }}
>
<Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "xl", md: "h2" }}>
{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>
)
}

View 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;

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