Compare commits

..

15 Commits

Author SHA1 Message Date
d66a952d4c Merge pull request 'nico/27-okt-25' (#1) from nico/27-okt-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/1
2025-10-27 22:17:59 +08:00
ed371bd0d9 Fix QC Kak Inno 24 Okt 25
Fix QC Kak Ayu 24 Okt 25
Fix QC Keano 24 Okt 25
Fix Detail Lowongan Kerja
2025-10-27 22:15:55 +08:00
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
152 changed files with 8430 additions and 2655 deletions

BIN
bun.lockb

Binary file not shown.

View File

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

View File

@@ -143,7 +143,7 @@ model MediaSosial {
isActive Boolean @default(true) isActive Boolean @default(true)
} }
//========================================= PROFILE ========================================= // //========================================= DESA ANTI KORUPSI ========================================= //
model DesaAntiKorupsi { model DesaAntiKorupsi {
id String @id @default(cuid()) id String @id @default(cuid())
name String @unique name String @unique
@@ -324,8 +324,8 @@ model PegawaiPPID {
model StrukturOrganisasiPPID { model StrukturOrganisasiPPID {
id String @id @default(uuid()) id String @id @default(uuid())
posisiOrganisasiId String @db.VarChar(50) posisiOrganisasiId String @db.VarChar(50)
pegawaiId String pegawaiId String
hubunganOrganisasiId String hubunganOrganisasiId String
posisiOrganisasi PosisiOrganisasiPPID @relation(fields: [posisiOrganisasiId], references: [id]) posisiOrganisasi PosisiOrganisasiPPID @relation(fields: [posisiOrganisasiId], references: [id])
pegawai PegawaiPPID @relation(fields: [pegawaiId], references: [id]) pegawai PegawaiPPID @relation(fields: [pegawaiId], references: [id])
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@@ -1450,8 +1450,8 @@ model PegawaiBumDes {
model StrukturOrganisasiBumDes { model StrukturOrganisasiBumDes {
id String @id @default(uuid()) id String @id @default(uuid())
posisiOrganisasiId String @db.VarChar(50) posisiOrganisasiId String @db.VarChar(50)
pegawaiId String pegawaiId String
hubunganOrganisasiId String hubunganOrganisasiId String
posisiOrganisasi PosisiOrganisasiBumDes @relation(fields: [posisiOrganisasiId], references: [id]) posisiOrganisasi PosisiOrganisasiBumDes @relation(fields: [posisiOrganisasiId], references: [id])
pegawai PegawaiBumDes @relation(fields: [pegawaiId], references: [id]) pegawai PegawaiBumDes @relation(fields: [pegawaiId], references: [id])
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@@ -1606,7 +1606,7 @@ model Pembiayaan {
ApbDesa ApbDesa[] @relation("ApbDesaPembiayaan") ApbDesa ApbDesa[] @relation("ApbDesaPembiayaan")
} }
// ========================================= INOVASI ========================================= // // ========================================= MENU INOVASI ========================================= //
// ========================================= DESA DIGITAL / SMART VILLAGE ========================================= // // ========================================= DESA DIGITAL / SMART VILLAGE ========================================= //
model DesaDigital { model DesaDigital {
id String @id @default(cuid()) id String @id @default(cuid())
@@ -1820,7 +1820,7 @@ model KategoriKegiatan {
isActive Boolean @default(true) isActive Boolean @default(true)
KegiatanDesa KegiatanDesa[] KegiatanDesa KegiatanDesa[]
} }
// ========================================= EDUKASI LINGKUNGAN ========================================= // // ========================================= EDUKASI LINGKUNGAN ========================================= //
model TujuanEdukasiLingkungan { model TujuanEdukasiLingkungan {
id String @id @default(cuid()) id String @id @default(cuid())
@@ -2087,6 +2087,9 @@ model DataPerpustakaan {
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime @default(now()) deletedAt DateTime @default(now())
isActive Boolean @default(true) isActive Boolean @default(true)
// relasi baru ke peminjaman
peminjamanBuku PeminjamanBuku[]
} }
model KategoriBuku { model KategoriBuku {
@@ -2099,6 +2102,31 @@ model KategoriBuku {
DataPerpustakaan DataPerpustakaan[] 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 ========================================= // // ========================================= USER ========================================= //
model 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, loading: false,
search: "", search: "",
load: async (page = 1, limit = 10, search = "", kategori = "") => { 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.page = page;
berita.findMany.search = search; berita.findMany.search = search;
try { try {
const query: any = { page, limit }; const query: any = { page, limit };
if (search) query.search = search; if (search) query.search = search;
if (kategori) query.kategori = kategori; if (kategori) query.kategori = kategori;
const res = await ApiFetch.api.desa.berita["find-many"].get({ query }); const res = await ApiFetch.api.desa.berita["find-many"].get({ query });
if (res.status === 200 && res.data?.success) { if (res.status === 200 && res.data?.success) {
berita.findMany.data = res.data.data ?? []; berita.findMany.data = res.data.data ?? [];
berita.findMany.totalPages = res.data.totalPages ?? 1; berita.findMany.totalPages = res.data.totalPages ?? 1;
@@ -98,9 +99,16 @@ const berita = proxy({
berita.findMany.data = []; berita.findMany.data = [];
berita.findMany.totalPages = 1; berita.findMany.totalPages = 1;
} finally { } 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: { findUnique: {

View File

@@ -55,46 +55,95 @@ const dataPerpustakaan = proxy({
}, },
}, },
findMany: { findMany: {
data: null as data: null as
| Prisma.DataPerpustakaanGetPayload<{ | Prisma.DataPerpustakaanGetPayload<{
include: { include: {
image: true; image: true;
kategori: true; kategori: true;
}; };
}>[] }>[]
| null, | null,
page: 1, page: 1,
totalPages: 1, totalPages: 1,
loading: false, loading: false,
search: "", search: "",
load: async (page = 1, limit = 10, search = "", kategori = "") => { load: async (page = 1, limit = 10, search = "", kategori = "") => {
dataPerpustakaan.findMany.loading = true; // ✅ Akses langsung via nama path const startTime = Date.now();
dataPerpustakaan.findMany.page = page; dataPerpustakaan.findMany.loading = true; // ✅ Akses langsung via nama path
dataPerpustakaan.findMany.search = search; dataPerpustakaan.findMany.page = page;
dataPerpustakaan.findMany.search = search;
try {
const query: any = { page, limit }; try {
if (search) query.search = search; const query: any = { page, limit };
if (kategori) query.kategori = kategori; if (search) query.search = search;
if (kategori) query.kategori = kategori;
const res = await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan["findMany"].get({ query });
const res =
if (res.status === 200 && res.data?.success) { await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan[
dataPerpustakaan.findMany.data = res.data.data ?? []; "findMany"
dataPerpustakaan.findMany.totalPages = res.data.totalPages ?? 1; ].get({ query });
} else {
dataPerpustakaan.findMany.data = []; if (res.status === 200 && res.data?.success) {
dataPerpustakaan.findMany.totalPages = 1; dataPerpustakaan.findMany.data = res.data.data ?? [];
} dataPerpustakaan.findMany.totalPages = res.data.totalPages ?? 1;
} catch (err) { } else {
console.error("Gagal fetch data perpustakaan paginated:", err);
dataPerpustakaan.findMany.data = []; dataPerpustakaan.findMany.data = [];
dataPerpustakaan.findMany.totalPages = 1; 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: { findUnique: {
data: null as Prisma.DataPerpustakaanGetPayload<{ data: null as Prisma.DataPerpustakaanGetPayload<{
include: { include: {
@@ -321,17 +370,20 @@ const kategoriBuku = proxy({
totalPages: 1, totalPages: 1,
loading: false, loading: false,
search: "", 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.loading = true; // ✅ Akses langsung via nama path
kategoriBuku.findMany.page = page; kategoriBuku.findMany.page = page;
kategoriBuku.findMany.search = search; kategoriBuku.findMany.search = search;
try { try {
const query: any = { page, limit }; const query: any = { page, limit };
if (search) query.search = search; 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) { if (res.status === 200 && res.data?.success) {
kategoriBuku.findMany.data = res.data.data ?? []; kategoriBuku.findMany.data = res.data.data ?? [];
kategoriBuku.findMany.totalPages = res.data.totalPages ?? 1; 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({ const perpustakaanDigitalState = proxy({
dataPerpustakaan, dataPerpustakaan,
kategoriBuku, kategoriBuku,
peminjamanBuku,
}); });
export default perpustakaanDigitalState; 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: { findUnique: {
data: null as data: null as
| (Prisma.PegawaiPPIDGetPayload<{ | (Prisma.PegawaiPPIDGetPayload<{

View File

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

View File

@@ -43,7 +43,7 @@ function PerizinanBerusaha() {
try { try {
setLoading(true); setLoading(true);
// You should get the ID from your router query or params // 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); await pelayananPerizinanBerusaha.findById.load(id);
} catch (err) { } catch (err) {
setError('Gagal memuat data'); setError('Gagal memuat data');

View File

@@ -55,9 +55,9 @@ function EditProgramKemiskinan() {
useEffect(() => { useEffect(() => {
if (!id) return; if (!id) return;
stateProgram.findUnique const loadData = async () => {
.load(id) try {
.then(() => { await stateProgram.findUnique.load(id);
const data = stateProgram.findUnique.data; const data = stateProgram.findUnique.data;
if (data) { if (data) {
setFormData({ setFormData({
@@ -70,12 +70,16 @@ function EditProgramKemiskinan() {
}, },
}); });
} }
}) } catch (err) {
.catch((err) => {
console.error('Error load data:', err); console.error('Error load data:', err);
toast.error('Gagal mengambil data program'); 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 // generic handler untuk field top-level
const handleChange = useCallback( const handleChange = useCallback(

View File

@@ -183,7 +183,7 @@ function EditArtikelKesehatan() {
{/* Gambar */} {/* Gambar */}
<Box> <Box>
<Text fw="bold" fz="sm" mb={6}> <Text fw="bold" fz="sm" mb={6}>
Gambar Berita Gambar Artikel Kesehatan
</Text> </Text>
<Dropzone <Dropzone
onDrop={handleFileChange} onDrop={handleFileChange}
@@ -240,15 +240,15 @@ function EditArtikelKesehatan() {
/> />
{/* Pendahuluan */} {/* Pendahuluan */}
<InputText <Box>
label="Pendahuluan" <Text fw="bold">Pendahuluan</Text>
value={formData.introduction.content} <EditEditor
onChange={(value) => value={formData.introduction.content}
setFormData((prev) => ({ ...prev, introduction: { content: value } })) onChange={(value) =>
} setFormData((prev) => ({ ...prev, introduction: { ...prev.introduction, content: value } }))
placeholder="Masukkan pendahuluan" }
/> />
</Box>
{/* Gejala */} {/* Gejala */}
<Box> <Box>
<Text fw="bold">Gejala</Text> <Text fw="bold">Gejala</Text>

View File

@@ -115,7 +115,7 @@ function CreateArtikelKesehatan() {
<Stack gap="md"> <Stack gap="md">
<Box> <Box>
<Text fw="bold" fz="sm" mb={6}> <Text fw="bold" fz="sm" mb={6}>
Gambar Berita Gambar Artikel Kesehatan
</Text> </Text>
<Dropzone <Dropzone
onDrop={(files) => { onDrop={(files) => {
@@ -163,7 +163,7 @@ function CreateArtikelKesehatan() {
</Box> </Box>
)} )}
</Box> </Box>
<TextInput <TextInput
label={"Judul"} label={"Judul"}
placeholder="Masukkan judul" placeholder="Masukkan judul"
@@ -182,16 +182,15 @@ function CreateArtikelKesehatan() {
}} }}
required required
/> />
<TextInput <Box>
label={"Pendahuluan"} <Text fz="sm" fw="bold">Pendahuluan</Text>
placeholder="Masukkan pendahuluan" <CreateEditor
required value={stateArtikelKesehatan.create.form.introduction.content}
defaultValue={stateArtikelKesehatan.create.form.introduction.content} onChange={(e) => {
onChange={(e) => { stateArtikelKesehatan.create.form.introduction.content = e;
stateArtikelKesehatan.create.form.introduction.content = e.target.value; }}
}} />
/> </Box>
{/* Gejala */} {/* Gejala */}
<Box> <Box>
<Text fz="md" fw="bold">Gejala</Text> <Text fz="md" fw="bold">Gejala</Text>

View File

@@ -89,26 +89,26 @@ function ListArtikelKesehatan({ search }: { search: string }) {
<Table highlightOnHover> <Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Judul</TableTh> <TableTh style={{ minWidth: 200 }}>Judul</TableTh>
<TableTh>Konten</TableTh> <TableTh style={{ minWidth: 200 }}>Konten</TableTh>
<TableTh>Aksi</TableTh> <TableTh style={{ minWidth: 200 }}>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.length > 0 ? ( {filteredData.length > 0 ? (
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd style={{ minWidth: 200 }}>
<Text fw={500} truncate="end" lineClamp={1}> <Text fw={500} truncate="end" lineClamp={1}>
{item.title} {item.title}
</Text> </Text>
</TableTd> </TableTd>
<TableTd> <TableTd style={{ minWidth: 200 }} >
<Text truncate fz="sm" c="dimmed" lineClamp={1}> <Text truncate fz="sm" c="dimmed" lineClamp={1}>
{item.content} {item.content}
</Text> </Text>
</TableTd> </TableTd>
<TableTd> <TableTd style={{ minWidth: 200 }}>
<Button <Button
variant="light" variant="light"
color="blue" color="blue"

View File

@@ -223,7 +223,7 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
{/* Chart */} {/* Chart */}
<Box mt="lg" style={{ width: '100%', minWidth: 300, height: 420, minHeight: 300 }}> <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> <Title pb={10} order={4}>Grafik Hasil Kepuasan Masyarakat</Title>
{mounted && diseaseChartData.length > 0 ? ( {mounted && diseaseChartData.length > 0 ? (
<Center> <Center>

View File

@@ -111,9 +111,7 @@ function ListInfoWabahPenyakit({ search }: { search: string }) {
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={200}> <Box w={200}>
<Text truncate fz="sm" c="dimmed"> <Text truncate="end" fz="sm" c="dimmed" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat }} />
{item.deskripsiSingkat}
</Text>
</Box> </Box>
</TableTd> </TableTd>
<TableTd> <TableTd>

View File

@@ -66,7 +66,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
return ( return (
<Stack gap="lg"> <Stack gap="lg">
<Title order={2} fw={700} style={{ color: "#1A1B1E" }}> <Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
Profil Desa Profile Desa
</Title> </Title>
<Tabs <Tabs

View File

@@ -65,7 +65,11 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
background: "linear-gradient(135deg, #e7ebf7, #f9faff)", background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem", borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)", 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) => ( {tabs.map((tab, i) => (
<Tooltip <Tooltip

View File

@@ -72,7 +72,11 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
background: "linear-gradient(135deg, #e7ebf7, #f9faff)", background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem", borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)", 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) => ( {tabs.map((tab, i) => (
<Tooltip <Tooltip

View File

@@ -79,7 +79,11 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
background: "linear-gradient(135deg, #e7ebf7, #f9faff)", background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem", borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)", 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) => ( {tabs.map((tab, i) => (
<Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}> <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)", background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem", borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)", 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) => ( {tabs.map((tab, i) => (
<Tooltip <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 { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; 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 }) { function LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter(); const router = useRouter();
@@ -25,6 +25,13 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
icon: <IconCategory size={18} stroke={1.8} />, icon: <IconCategory size={18} stroke={1.8} />,
tooltip: "Atur kategori untuk buku digital", 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); const currentTab = tabs.find(tab => tab.href === pathname);
@@ -65,7 +72,11 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
background: "linear-gradient(135deg, #e7ebf7, #f9faff)", background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem", borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)", 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) => ( {tabs.map((tab, i) => (
<Tooltip <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)", background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem", borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)", 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) => ( {tabs.map((tab, i) => (
<Tooltip <Tooltip

View File

@@ -103,18 +103,7 @@ function ListPegawaiPPID({ search }: { search: string }) {
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{(() => { {filteredData.map((item) => (
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) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Box w={150}> <Box w={150}>

View File

@@ -21,10 +21,10 @@ function ListStrukturOrganisasiPPID() {
const stateOrganisasi = useProxy(stateStrukturPPID.pegawai); const stateOrganisasi = useProxy(stateStrukturPPID.pegawai);
useEffect(() => { useEffect(() => {
stateOrganisasi.findMany.load(); stateOrganisasi.findManyAll.load();
}, []); }, []);
if (stateOrganisasi.findMany.loading) { if (stateOrganisasi.findManyAll.loading) {
return ( return (
<Center py={40}> <Center py={40}>
<Loader size="lg" /> <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 ( return (
<Stack align="center" py={60} gap="sm"> <Stack align="center" py={60} gap="sm">
<IconUsers size={60} stroke={1.5} color="var(--mantine-color-gray-6)" /> <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 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) { for (const pegawai of aktifPegawai) {
const posisiId = pegawai.posisi.id; const posisiId = pegawai.posisi.id;

View File

@@ -377,22 +377,5 @@ export const navBar = [
path: "/admin/pendidikan/data-pendidikan" 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> <Tooltip label="Keluar" position="bottom" withArrow>
<ActionIcon <ActionIcon
onClick={() => { onClick={() => {
router.push("/login"); router.push("/darmasaba");
}} }}
color={colors["blue-button"]} color={colors["blue-button"]}
radius="xl" 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 { return {
success: true, success: true,
message: "Success fetch lembaga pendidikan with pagination", 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 dataPerpustakaanFindMany from "./findMany";
import dataPerpustakaanFindUnique from "./findUnique"; import dataPerpustakaanFindUnique from "./findUnique";
import dataPerpustakaanUpdate from "./updt"; import dataPerpustakaanUpdate from "./updt";
import dataPerpustakaanFindManyAll from "./findManyAll";
const DataPerpustakaan = new Elysia({ const DataPerpustakaan = new Elysia({
prefix: "/dataperpustakaan", prefix: "/dataperpustakaan",
@@ -18,7 +19,7 @@ const DataPerpustakaan = new Elysia({
kategoriId: t.String(), kategoriId: t.String(),
}), }),
}) })
.get("/findManyAll", dataPerpustakaanFindManyAll)
.get("/findMany", dataPerpustakaanFindMany) .get("/findMany", dataPerpustakaanFindMany)
.get("/:id", async (context) => { .get("/:id", async (context) => {
const response = await dataPerpustakaanFindUnique( const response = await dataPerpustakaanFindUnique(

View File

@@ -1,12 +1,14 @@
import Elysia from "elysia"; import Elysia from "elysia";
import DataPerpustakaan from "./data-perpustakaan"; import DataPerpustakaan from "./data-perpustakaan";
import KategoriBuku from "./kategori-buku"; import KategoriBuku from "./kategori-buku";
import PeminjamanBuku from "./peminjaman";
const PerpustakaanDigital = new Elysia({ const PerpustakaanDigital = new Elysia({
prefix: "/perpustakaandigital", prefix: "/perpustakaandigital",
tags: ["Pendidikan / Perpustakaan Digital"], tags: ["Pendidikan / Perpustakaan Digital"],
}) })
.use(DataPerpustakaan) .use(DataPerpustakaan)
.use(KategoriBuku); .use(KategoriBuku)
.use(PeminjamanBuku);
export default PerpustakaanDigital; 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 prisma from "@/lib/prisma";
import { Context } from "elysia"; import { Context } from "elysia";
// Di findMany.ts
export default async function pegawaiFindMany(context: Context) { export default async function pegawaiFindMany(context: Context) {
const page = Number(context.query.page) || 1; const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10; const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || ""; const search = (context.query.search as string) || "";
const skip = (page - 1) * limit; const skip = (page - 1) * limit;
// Buat where clause
const isActiveParam = context.query.isActive; const isActiveParam = context.query.isActive;
// where clause dinamis
const where: any = {}; const where: any = {};
if (isActiveParam !== undefined) { if (isActiveParam !== undefined) {
where.isActive = isActiveParam === "true"; where.isActive = isActiveParam === "true";
} }
// Tambahkan pencarian (jika ada)
if (search) { if (search) {
where.OR = [ where.OR = [
{ namaLengkap: { contains: search, mode: "insensitive" } }, { namaLengkap: { contains: search, mode: "insensitive" } },
{ alamat: { contains: search, mode: "insensitive" } }, { alamat: { contains: search, mode: "insensitive" } },
{ posisi: { nama: { contains: search, mode: "insensitive" } } },
]; ];
} }
try { try {
const [data, total] = await Promise.all([ // Ambil semua data terlebih dahulu (tanpa pagination)
const [allData, total] = await Promise.all([
prisma.pegawaiPPID.findMany({ prisma.pegawaiPPID.findMany({
where, where,
include: { include: {
posisi: true, posisi: true,
image: 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); const totalPages = Math.ceil(total / limit);
return { return {
success: true, success: true,
message: "Success fetch pegawai with pagination", message: "Success fetch pegawai with hierarchy order",
data, data: paginatedData,
page, page,
totalPages, totalPages,
total, total,
}; };
} catch (e) { } catch (error) {
console.error("Find many paginated error:", e); console.error("Find many pegawai error:", error);
return { return {
success: false, success: false,
message: "Failed fetch pegawai with pagination", message: "Failed fetch pegawai",
data: [], data: [],
page: 1, page: 1,
totalPages: 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 pegawaiNonActive from "./nonActive";
import pegawaiUpdate from "./updt"; import pegawaiUpdate from "./updt";
import pegawaiDelete from "./del"; import pegawaiDelete from "./del";
import pegawaiFindManyAll from "./findManyAll";
const Pegawai = new Elysia({ const Pegawai = new Elysia({
@@ -15,6 +16,9 @@ const Pegawai = new Elysia({
// ✅ Find all // ✅ Find all
.get("/find-many", pegawaiFindMany) .get("/find-many", pegawaiFindMany)
// ✅ Find all (non-paginated)
.get("/find-many-all", pegawaiFindManyAll)
// ✅ Find by ID // ✅ Find by ID
.get("/:id", async (context) => { .get("/:id", async (context) => {
const response = await pegawaiFindUnique(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 Pendidikan from "./_lib/pendidikan";
import User from "./_lib/user"; import User from "./_lib/user";
import Role from "./_lib/user/role"; import Role from "./_lib/user/role";
import Search from "./_lib/search";
const ROOT = process.cwd(); const ROOT = process.cwd();
@@ -95,6 +96,7 @@ const ApiServer = new Elysia()
.use(Pendidikan) .use(Pendidikan)
.use(User) .use(User)
.use(Role) .use(Role)
.use(Search)
.onError(({ code }) => { .onError(({ code }) => {
if (code === "NOT_FOUND") { 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 Informasi dan Pelayanan Administrasi Digital
</Text> </Text>
</Box> </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> </Container>
<Box px={{ base: "md", md: 100 }}> <Box px={{ base: "md", md: 100 }}>
<Stack gap={"xs"}> <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> </Stack>
</Box> </Box>
</Stack> </Stack>

View File

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

View File

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

View File

@@ -16,7 +16,7 @@ function PelayananPendudukNonPermanent() {
const loadData = async () => { const loadData = async () => {
try { try {
setLoading(true); setLoading(true);
await state.pelayananPendudukNonPermanen.findById.load('1'); await state.pelayananPendudukNonPermanen.findById.load('edit');
} catch (error) { } catch (error) {
console.error('Gagal memuat data:', error); console.error('Gagal memuat data:', error);
} finally { } finally {

View File

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

View File

@@ -47,13 +47,13 @@ function PelayananSuratKeterangan({ search }: { search: string }) {
<Box pb="xl"> <Box pb="xl">
<Group justify="space-between" align="center" mb="md"> <Group justify="space-between" align="center" mb="md">
<Group gap="xs"> <Group gap="xs">
<IconFileDescription size={28} stroke={1.8} color={colors["blue-button"]} /> <IconFileDescription size={28} stroke={1.8} />
<Text fz={{ base: "h4", md: "h2" }} fw={700}> <Text fz={{ base: "h4", md: "h2" }} fw={700}>
Layanan Surat Keterangan Layanan Surat Keterangan
</Text> </Text>
</Group> </Group>
<Tooltip label="Pilih layanan surat keterangan sesuai kebutuhan Anda" withArrow> <Tooltip label="Pilih layanan surat keterangan sesuai kebutuhan Anda" withArrow>
<IconInfoCircle size={22} stroke={1.8} color={colors["blue-button"]} /> <IconInfoCircle size={22} stroke={1.8} />
</Tooltip> </Tooltip>
</Group> </Group>

View File

@@ -77,9 +77,7 @@ function Page() {
fallbackSrc="https://placehold.co/800x400?text=Gambar+tidak+tersedia" fallbackSrc="https://placehold.co/800x400?text=Gambar+tidak+tersedia"
loading="lazy" loading="lazy"
/> />
<Text py="md" fz={{ base: "sm", md: "md" }} ta="justify" lh={1.8}> <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.' }} />
{state.findUnique.data?.deskripsi || 'Belum ada deskripsi untuk potensi desa ini.'}
</Text>
</Stack> </Stack>
</Paper> </Paper>
</Container> </Container>

View File

@@ -43,7 +43,7 @@ function Page() {
<Text fz={{ base: "2rem", md: "3rem" }} fw={900} c={colors["blue-button"]} lh={1.2}> <Text fz={{ base: "2rem", md: "3rem" }} fw={900} c={colors["blue-button"]} lh={1.2}>
Potensi Desa Darmasaba Potensi Desa Darmasaba
</Text> </Text>
<Text fz="lg" c="dimmed" ta="justify"> <Text fz="lg" ta="justify">
Temukan berbagai potensi unggulan, peluang, dan daya tarik yang menjadikan Desa Darmasaba istimewa. Temukan berbagai potensi unggulan, peluang, dan daya tarik yang menjadikan Desa Darmasaba istimewa.
</Text> </Text>
</Stack> </Stack>

View File

@@ -10,31 +10,36 @@ import ProfilPerbekel from './ui/profilPerbekel';
// import LembagaDesa from './ui/lembagaDesa'; // import LembagaDesa from './ui/lembagaDesa';
import MotoDesa from './ui/motoDesa'; import MotoDesa from './ui/motoDesa';
import SemuaPerbekel from './ui/semuaPerbekel'; import SemuaPerbekel from './ui/semuaPerbekel';
import ScrollToTopButton from '@/app/darmasaba/_com/scrollToTopButton';
function Page() { function Page() {
return ( return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}> <Box>
<Box px={{ base: 'md', md: 100 }}> <Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<BackButton /> <Box px={{ base: 'md', md: 100 }}>
</Box> <BackButton />
<Container w={{ base: "100%", md: "50%" }}> </Box>
<Stack align='center' gap={0}> <Container w={{ base: "100%", md: "50%" }}>
<Text fz={{base: "h1", md: "2.5rem"}} c={colors["blue-button"]} fw={"bold"}> <Stack align='center' gap={0}>
Profile Desa <Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
</Text> Profile Desa
</Stack> </Text>
</Container> </Stack>
<Box px={{ base: "md", md: 100 }}> </Container>
<ProfileDesa /> <Box px={{ base: "md", md: 100 }}>
<SejarahDesa /> <ProfileDesa />
<VisimisiDesa /> <SejarahDesa />
<LambangDesa /> <VisimisiDesa />
<MaskotDesa /> <LambangDesa />
<ProfilPerbekel /> <MaskotDesa />
<MotoDesa /> <ProfilPerbekel />
<SemuaPerbekel/> <MotoDesa />
</Box> <SemuaPerbekel />
</Stack> </Box>
</Stack>
{/* Tombol Scroll ke Atas */}
<ScrollToTopButton />
</Box>
); );
} }

View File

@@ -2,7 +2,7 @@
'use client' 'use client'
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile' import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'
import colors from '@/con/colors' import colors from '@/con/colors'
import { Box, Center, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core' import { Box, Center, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'
import { useEffect } from 'react' import { useEffect } from 'react'
import { useProxy } from 'valtio/utils' import { useProxy } from 'valtio/utils'
@@ -58,7 +58,6 @@ function LambangDesa() {
borderColor: '#e0e9ff', borderColor: '#e0e9ff',
}} }}
> >
<Tooltip label="Deskripsi lambang desa" position="top-start" withArrow>
<Text <Text
fz={{ base: 'md', md: 'lg' }} fz={{ base: 'md', md: 'lg' }}
lh={1.8} lh={1.8}
@@ -67,7 +66,6 @@ function LambangDesa() {
style={{ fontWeight: 400, wordBreak: "break-word", whiteSpace: "normal", }} style={{ fontWeight: 400, wordBreak: "break-word", whiteSpace: "normal", }}
dangerouslySetInnerHTML={{ __html: data.deskripsi }} dangerouslySetInnerHTML={{ __html: data.deskripsi }}
/> />
</Tooltip>
</Paper> </Paper>
</Stack> </Stack>
</Box> </Box>

View File

@@ -1,11 +1,11 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import { Box, Card, Center, Group, Image, Loader, Paper, Stack, Text, Tooltip } from '@mantine/core'; import colors from '@/con/colors';
import { Box, Card, Center, Group, Image, Loader, Paper, Stack, Text } from '@mantine/core';
import { IconPhoto } from '@tabler/icons-react';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { IconPhoto } from '@tabler/icons-react';
import colors from '@/con/colors';
function MaskotDesa() { function MaskotDesa() {
const state = useProxy(stateProfileDesa.maskotDesa); const state = useProxy(stateProfileDesa.maskotDesa);
@@ -54,8 +54,8 @@ function MaskotDesa() {
<Group justify="center" gap="lg" mt="lg"> <Group justify="center" gap="lg" mt="lg">
{data.images.length > 0 ? ( {data.images.length > 0 ? (
data.images.map((img, index) => ( data.images.map((img, index) => (
<Tooltip key={index} label={img.label} position="bottom" withArrow>
<Card <Card
key={index}
radius="lg" radius="lg"
shadow="md" shadow="md"
withBorder withBorder
@@ -79,7 +79,6 @@ function MaskotDesa() {
{img.label} {img.label}
</Text> </Text>
</Card> </Card>
</Tooltip>
)) ))
) : ( ) : (
<Stack align="center" gap="xs" mt="lg"> <Stack align="center" gap="xs" mt="lg">

View File

@@ -2,10 +2,10 @@
'use client' 'use client'
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Image, Paper, SimpleGrid, Skeleton, Stack, Text, Divider, Tooltip } from '@mantine/core'; import { Box, Divider, Image, Paper, SimpleGrid, Skeleton, Stack, Text } from '@mantine/core';
import { IconBriefcase, IconTargetArrow, IconUser, IconUsers } from '@tabler/icons-react';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { IconUser, IconBriefcase, IconUsers, IconTargetArrow } from '@tabler/icons-react';
function ProfilPerbekel() { function ProfilPerbekel() {
const state = useProxy(stateProfileDesa.profilPerbekel) const state = useProxy(stateProfileDesa.profilPerbekel)
@@ -27,10 +27,10 @@ function ProfilPerbekel() {
return ( return (
<Box pb={80} px="md"> <Box pb={80} px="md">
<Stack align="center" gap={0} mb={40}> <Stack align="center" gap={0} mb={40}>
<Text <Text
c={colors['blue-button']} c={colors['blue-button']}
ta="center" ta="center"
fw="bold" fw="bold"
fz={{ base: "2rem", md: "2.8rem" }} fz={{ base: "2rem", md: "2.8rem" }}
style={{ letterSpacing: "0.5px" }} style={{ letterSpacing: "0.5px" }}
> >
@@ -41,11 +41,11 @@ function ProfilPerbekel() {
<SimpleGrid cols={{ base: 1, md: 2 }} spacing="xl" pb={50}> <SimpleGrid cols={{ base: 1, md: 2 }} spacing="xl" pb={50}>
<Box> <Box>
<Paper <Paper
bg={colors['white-trans-1']} bg={colors['white-trans-1']}
w="100%" w="100%"
radius="xl" radius="xl"
shadow="md" shadow="md"
withBorder withBorder
> >
<Stack gap={0}> <Stack gap={0}>
@@ -70,9 +70,9 @@ function ProfilPerbekel() {
<Text c={colors['white-1']} fz={{ base: "lg", md: "h3" }}> <Text c={colors['white-1']} fz={{ base: "lg", md: "h3" }}>
Perbekel Desa Darmasaba Perbekel Desa Darmasaba
</Text> </Text>
<Text <Text
c={colors['white-1']} c={colors['white-1']}
fw="bolder" fw="bolder"
fz={{ base: "xl", md: "h2" }} fz={{ base: "xl", md: "h2" }}
mt={8} mt={8}
> >
@@ -83,60 +83,56 @@ function ProfilPerbekel() {
</Paper> </Paper>
</Box> </Box>
<Paper <Paper
p="xl" p="xl"
bg={colors['white-trans-1']} bg={colors['white-trans-1']}
w="100%" w="100%"
radius="xl" radius="xl"
shadow="md" shadow="md"
withBorder withBorder
> >
<Stack gap="xl"> <Stack gap="xl">
<Box> <Box>
<Tooltip label="Informasi pribadi perbekel" withArrow> <Stack gap={6}>
<Stack gap={6}> <Stack align="center" gap={6}>
<Stack align="center" gap={6}> <IconUser size={22} color={colors['blue-button']} />
<IconUser size={22} color={colors['blue-button']} /> <Text fz={{ base: "1.2rem", md: "1.5rem" }} fw="bold">Biodata</Text>
<Text fz={{ base: "1.2rem", md: "1.5rem" }} fw="bold">Biodata</Text>
</Stack>
<Text
fz={{ base: "1rem", md: "1.2rem" }}
ta="justify"
lh={1.6}
dangerouslySetInnerHTML={{ __html: data.biodata }}
style={{wordBreak: "break-word", whiteSpace: "normal"}}
/>
</Stack> </Stack>
</Tooltip> <Text
fz={{ base: "1rem", md: "1.2rem" }}
ta="justify"
lh={1.6}
dangerouslySetInnerHTML={{ __html: data.biodata }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/>
</Stack>
</Box> </Box>
<Box> <Box>
<Tooltip label="Pengalaman kerja perbekel" withArrow> <Stack gap={6}>
<Stack gap={6}> <Stack align="center" gap={6}>
<Stack align="center" gap={6}> <IconBriefcase size={22} color={colors['blue-button']} />
<IconBriefcase size={22} color={colors['blue-button']} /> <Text fz={{ base: "1.2rem", md: "1.5rem" }} fw="bold">Pengalaman</Text>
<Text fz={{ base: "1.2rem", md: "1.5rem" }} fw="bold">Pengalaman</Text>
</Stack>
<Text
fz={{ base: "1rem", md: "1.2rem" }}
ta="justify"
lh={1.6}
dangerouslySetInnerHTML={{ __html: data.pengalaman }}
style={{wordBreak: "break-word", whiteSpace: "normal"}}
/>
</Stack> </Stack>
</Tooltip> <Text
fz={{ base: "1rem", md: "1.2rem" }}
ta="justify"
lh={1.6}
dangerouslySetInnerHTML={{ __html: data.pengalaman }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/>
</Stack>
</Box> </Box>
</Stack> </Stack>
</Paper> </Paper>
</SimpleGrid> </SimpleGrid>
<Paper <Paper
p="xl" p="xl"
bg={colors['white-trans-1']} bg={colors['white-trans-1']}
w="100%" w="100%"
radius="xl" radius="xl"
shadow="md" shadow="md"
withBorder withBorder
> >
<Stack gap="xl"> <Stack gap="xl">
@@ -145,27 +141,27 @@ function ProfilPerbekel() {
<IconUsers size={22} color={colors['blue-button']} /> <IconUsers size={22} color={colors['blue-button']} />
<Text fz={{ base: "1.2rem", md: "1.5rem" }} fw="bold">Pengalaman Organisasi</Text> <Text fz={{ base: "1.2rem", md: "1.5rem" }} fw="bold">Pengalaman Organisasi</Text>
</Stack> </Stack>
<Text <Text
fz={{ base: "1rem", md: "1.2rem" }} fz={{ base: "1rem", md: "1.2rem" }}
ta="justify" ta="justify"
lh={1.6} lh={1.6}
dangerouslySetInnerHTML={{ __html: data.pengalamanOrganisasi }} dangerouslySetInnerHTML={{ __html: data.pengalamanOrganisasi }}
style={{wordBreak: "break-word", whiteSpace: "normal"}} style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/> />
</Box> </Box>
<Box> <Box>
<Stack align="center" gap={6} mb={6}> <Stack align="center" gap={6} mb={6}>
<IconTargetArrow size={22} color={colors['blue-button']} /> <IconTargetArrow size={22} color={colors['blue-button']} />
<Text fz={{ base: "1.2rem", md: "1.5rem" }} fw="bold">Program Kerja Unggulan</Text> <Text fz={{ base: "1.2rem", md: "1.5rem" }} fw="bold">Program Kerja Unggulan</Text>
</Stack> </Stack>
<Box px={10}> <Box px={10}>
<Text <Text
fz={{ base: "1rem", md: "1.2rem" }} fz={{ base: "1rem", md: "1.2rem" }}
ta="justify" ta="justify"
lh={1.6} lh={1.6}
dangerouslySetInnerHTML={{ __html: data.programUnggulan }} dangerouslySetInnerHTML={{ __html: data.programUnggulan }}
style={{wordBreak: "break-word", whiteSpace: "normal"}} style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/> />
</Box> </Box>
</Box> </Box>

View File

@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
'use client' 'use client'
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import { Box, Center, Image, Paper, SimpleGrid, Skeleton, Stack, Text, Title, Tooltip } from '@mantine/core'; import { Box, Center, Image, Paper, SimpleGrid, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconUser } from '@tabler/icons-react'; import { IconUser } from '@tabler/icons-react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
@@ -77,23 +77,17 @@ function SemuaPerbekel() {
</Box> </Box>
<Stack gap={4} align="center"> <Stack gap={4} align="center">
<Tooltip label="Nama Perbekel" withArrow>
<Text fw={700} fz="lg" ta="center"> <Text fw={700} fz="lg" ta="center">
{v.nama} {v.nama}
</Text> </Text>
</Tooltip>
<Tooltip label="Wilayah menjabat" withArrow> <Text c="dimmed" fz="sm" ta="center">
<Text c="dimmed" fz="sm" ta="center">
{v.daerah} {v.daerah}
</Text> </Text>
</Tooltip>
<Tooltip label="Periode jabatan" withArrow> <Text c="blue" fw={600} fz="sm" ta="center">
<Text c="blue" fw={600} fz="sm" ta="center">
{v.periode} {v.periode}
</Text> </Text>
</Tooltip>
</Stack> </Stack>
</Stack> </Stack>
</Paper> </Paper>

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' 'use client'
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa'; import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
import colors from '@/con/colors'; import colors from '@/con/colors';
@@ -206,32 +42,41 @@ function Page() {
<Stack gap="lg" justify="center"> <Stack gap="lg" justify="center">
<Paper bg={colors['white-1']} p="xl"> <Paper bg={colors['white-1']} p="xl">
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="md"> <SimpleGrid cols={{ base: 1, md: 3 }} spacing="md">
{/* Pendapatan Card */} {/* Pendapatan Card */}
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}> <Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
<Stack gap={"xs"}> <Stack gap={"xs"}>
<Title order={3}>Pendapatan</Title> <Title order={3}>Pendapatan</Title>
{PendapatanAsliDesa.pendapatan.findMany.data?.map((item) => ( {latestApb?.pendapatan?.map((item) => (
<Box key={item.id}> <Box key={item.id}>
<Grid> <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> <Text fz="md" fw={500}>{item.name}</Text>
</GridCol> </GridCol>
<GridCol span={{ base: 12, md: 6 }}> <GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', { <Text
style: 'currency', fz="md"
currency: 'IDR', fw={500}
minimumFractionDigits: 0 style={{
}).format(item.value)}</Text> wordBreak: 'break-word',
whiteSpace: 'normal',
textAlign: 'right',
}}
>
{new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(item.value)}
</Text>
</GridCol> </GridCol>
</Grid> </Grid>
</Box> </Box>
))} ))}
<Grid> <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> <Text fz="lg" fw={600} mb="xs">Total Pendapatan</Text>
</GridCol> </GridCol>
<GridCol span={{ base: 12, md: 6 }}> <GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text fz="xl" fw={700} c={colors['blue-button']}> <Text style={{
wordBreak: 'break-word',
whiteSpace: 'normal'
}} fz="xl" fw={700} c={colors['blue-button']}>
{new Intl.NumberFormat('id-ID', { {new Intl.NumberFormat('id-ID', {
style: 'currency', style: 'currency',
currency: 'IDR', currency: 'IDR',
@@ -247,18 +92,28 @@ function Page() {
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}> <Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
<Stack gap={"xs"}> <Stack gap={"xs"}>
<Title order={3}>Belanja</Title> <Title order={3}>Belanja</Title>
{PendapatanAsliDesa.belanja.findMany.data?.map((item) => ( {latestApb?.belanja?.map((item) => (
<Box key={item.id}> <Box key={item.id}>
<Grid> <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> <Text fz="md" fw={500}>{item.name}</Text>
</GridCol> </GridCol>
<GridCol span={{ base: 12, md: 6 }}> <GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', { <Text
style: 'currency', fz="md"
currency: 'IDR', fw={500}
minimumFractionDigits: 0 style={{
}).format(item.value)}</Text> wordBreak: 'break-word',
whiteSpace: 'normal',
textAlign: 'right',
}}
>
{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(item.value)}
</Text>
</GridCol> </GridCol>
</Grid> </Grid>
</Box> </Box>
@@ -284,18 +139,28 @@ function Page() {
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}> <Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
<Stack gap={"xs"}> <Stack gap={"xs"}>
<Title order={3}>Pembiayaan</Title> <Title order={3}>Pembiayaan</Title>
{PendapatanAsliDesa.pembiayaan.findMany.data?.map((item) => ( {latestApb?.pembiayaan?.map((item) => (
<Box key={item.id}> <Box key={item.id}>
<Grid> <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> <Text fz="md" fw={500}>{item.name}</Text>
</GridCol> </GridCol>
<GridCol span={{ base: 12, md: 6 }}> <GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', { <Text
style: 'currency', fz="md"
currency: 'IDR', fw={500}
minimumFractionDigits: 0 style={{
}).format(item.value)}</Text> wordBreak: 'break-word',
whiteSpace: 'normal',
textAlign: 'right',
}}
>
{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(item.value)}
</Text>
</GridCol> </GridCol>
</Grid> </Grid>
</Box> </Box>
@@ -366,5 +231,4 @@ function Page() {
); );
} }
export default Page; export default Page;

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,136 @@
'use client'
import lowonganKerjaState from '@/app/admin/(dashboard)/_state/ekonomi/lowongan-kerja';
import colors from '@/con/colors';
import { Box, Button, Center, Group, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconBrandWhatsapp, IconBriefcase, IconCurrencyDollar, IconMapPin, IconPhone } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function DetailLowonganKerjaUser() {
const state = useProxy(lowonganKerjaState);
const router = useRouter();
const params = useParams();
const [loading, setLoading] = useState(true);
useShallowEffect(() => {
const loadData = async () => {
await state.findUnique.load(params?.id as string);
setLoading(false);
};
loadData();
}, []);
const data = state.findUnique.data;
if (loading || !data) {
return (
<Center py="xl">
<Skeleton height={500} w={{ base: '90%', md: '70%' }} radius="lg" />
</Center>
);
}
return (
<Stack bg={colors.Bg} py="xl" px={{ base: 'md', md: 100 }} align="center">
<Box w={{ base: '100%', md: '70%' }}>
<Button
variant="subtle"
color="blue"
leftSection={<IconArrowBack size={20} />}
mb="md"
onClick={() => router.back()}
>
Kembali
</Button>
<Paper
radius="lg"
shadow="md"
withBorder
p="xl"
bg={colors['white-1']}
>
<Stack gap="lg">
{/* Judul */}
<Text fz={{ base: '1.6rem', md: '2rem' }} fw={700} c={colors['blue-button']}>
{data.posisi}
</Text>
<Text c="dimmed" fz="sm">
Diposting: {new Date(data.createdAt).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric'
})}
</Text>
{/* Info Ringkas */}
<Stack gap="sm" mt="md">
<Group gap="xs">
<IconBriefcase size={20} color={colors['blue-button']} />
<Text fz="md" fw={600}>{data.namaPerusahaan}</Text>
</Group>
<Group gap="xs">
<IconMapPin size={20} color={colors['blue-button']} />
<Text fz="md">{data.lokasi}</Text>
</Group>
<Group gap="xs">
<IconPhone size={20} color={colors['blue-button']} />
<Text fz="md">{data.notelp}</Text>
</Group>
<Group gap="xs">
<IconCurrencyDollar size={20} color={colors['blue-button']} />
<Text fz="md">{data.gaji || '-'}</Text>
</Group>
<Group gap="xs">
<IconBriefcase size={20} color={colors['blue-button']} />
<Text fz="md">{data.tipePekerjaan}</Text>
</Group>
</Stack>
<Box>
<Text fw={600} fz="lg" mb={4}>
Deskripsi Pekerjaan
</Text>
<Text
fz="sm"
lh={1.6}
style={{ wordBreak: 'break-word' }}
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
/>
</Box>
<Box>
<Text fw={600} fz="lg" mb={4}>
Kualifikasi
</Text>
<Text
fz="sm"
lh={1.6}
style={{ wordBreak: 'break-word' }}
dangerouslySetInnerHTML={{ __html: data.kualifikasi || '-' }}
/>
</Box>
<Center>
<Button
radius="md"
size="md"
mt="md"
bg={colors['blue-button']}
onClick={() => window.open(`https://wa.me/${data.notelp}`, '_blank')}
leftSection={<IconBrandWhatsapp size={20} />}
>
Hubungi Sekarang
</Button>
</Center>
</Stack>
</Paper>
</Box>
</Stack>
);
}
export default DetailLowonganKerjaUser;

View File

@@ -12,13 +12,13 @@ import BackButton from '../../desa/layanan/_com/BackButto';
const formatCurrency = (value: string | number) => { const formatCurrency = (value: string | number) => {
// Convert to string if it's a number // Convert to string if it's a number
const numStr = typeof value === 'number' ? value.toString() : value; const numStr = typeof value === 'number' ? value.toString() : value;
// Remove all non-digit characters // Remove all non-digit characters
const digitsOnly = numStr.replace(/\D/g, ''); const digitsOnly = numStr.replace(/\D/g, '');
// Format with thousand separators // Format with thousand separators
const formatted = digitsOnly.replace(/\B(?=(\d{3})+(?!\d))/g, '.'); const formatted = digitsOnly.replace(/\B(?=(\d{3})+(?!\d))/g, '.');
return `Rp.${formatted}`; return `Rp.${formatted}`;
}; };
@@ -103,7 +103,7 @@ function Page() {
</Box> </Box>
</Flex> </Flex>
</Box> </Box>
<Button onClick={() => router.push(`https://wa.me/${v.notelp?.replace(/\D/g, '')}`)}>Lamar Sekarang</Button> <Button onClick={() => router.push(`/darmasaba/ekonomi/lowongan-kerja-lokal/${v.id}`)}>Detail</Button>
</Stack> </Stack>
</Paper> </Paper>
) )

View File

@@ -0,0 +1,157 @@
'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 */}
<Box px={{ base: 'md', md: 100 }}>
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={20} color={colors['blue-button']} />}
mb={15}
>
Kembali ke daftar produk
</Button>
</Box>
<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={250}
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

@@ -71,8 +71,11 @@ function Page() {
/> />
</GridCol> </GridCol>
</Grid> </Grid>
<Text px={{ base: 'md', md: 100 }} ta={"justify"} fz={{ base: "h4", md: "h3" }} > <Text px={{ base: 'md', md: 100 }} ta={"justify"} fz="md" >
Pasar Desa Online merupakan Media Promosi yang bertujuan untuk membantu warga desa dalam memasarkan dan memperkenalkan produknya kepada masyarakat. Pasar Desa Online adalah media promosi untuk membantu warga memasarkan
</Text>
<Text px={{ base: 'md', md: 100 }} ta={"justify"} fz="md" >
dan memperkenalkan produk mereka.
</Text> </Text>
</Box> </Box>
<Box px={{ base: "md", md: 100 }}> <Box px={{ base: "md", md: 100 }}>
@@ -105,7 +108,7 @@ function Page() {
return ( return (
<Stack key={k}> <Stack key={k}>
<motion.div <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 }} whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.8 }} whileTap={{ scale: 0.8 }}
> >
@@ -117,7 +120,7 @@ function Page() {
h={200} h={200}
w='100%' w='100%'
style={{ objectFit: 'cover' }} style={{ objectFit: 'cover' }}
loading="lazy" loading="lazy"
/> />
<Text py={10} fw={'bold'} fz={'lg'}>{v.nama}</Text> <Text py={10} fw={'bold'} fz={'lg'}>{v.nama}</Text>
<Text fz={'md'}>Rp {v.harga.toLocaleString('id-ID')}</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 ( return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}> <Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
@@ -45,27 +71,34 @@ function Page() {
return ( return (
<Paper p={'xl'} key={k}> <Paper p={'xl'} key={k}>
<Text fw={'bold'} fz={'h4'}>{v.name}</Text> <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>
) )
})} })}
<Paper p={'xl'}> <Box style={{ width: '100%', overflowX: 'auto' }}>
<Text pb={10} fw={'bold'} fz={'h4'}>Statistik Sektor Unggulan Darmasaba</Text> <Paper p="xl">
<BarChart <Text pb={10} fw="bold" fz="h4">Statistik Sektor Unggulan Darmasaba</Text>
p={10} <Box style={{ width: '100%', minWidth: '600px' }}>
h={300} <BarChart
data={data.map((item) => ({ p={10}
id: item.id, h={300}
sektor: item.name, data={chartData}
Ton: item.value, dataKey="sektor"
}))} series={[
dataKey="sektor" { name: 'Ton', color: colors['blue-button'] },
series={[ ]}
{ name: 'Ton', color: colors['blue-button'] }, tickLine="y"
]} tooltipAnimationDuration={200}
tickLine="y" withTooltip
/> style={{
</Paper> fontFamily: 'inherit',
}}
xAxisLabel="Sektor"
yAxisLabel="Ton"
/>
</Box>
</Paper>
</Box>
</Stack> </Stack>
</Box> </Box>
</Stack> </Stack>

View File

@@ -56,7 +56,8 @@ function Page() {
/> />
</GridCol> </GridCol>
</Grid> </Grid>
<Text fz={'h4'}>Mewujudkan Desa Darmasaba sebagai pusat inovasi digital yang memberdayakan masyarakat, meningkatkan kesejahteraan, dan menciptakan peluang ekonomi berbasis teknologi.</Text> <Text fz={'md'}>Menjadikan Desa Darmasaba pusat inovasi digital untuk pemberdayaan masyarakat</Text>
<Text fz={'md'}>dan peningkatan ekonomi berbasis teknologi.</Text>
</Box> </Box>
<Box px={{ base: "md", md: 100 }}> <Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'} justify='center'> <Stack gap={'lg'} justify='center'>

View File

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

View File

@@ -64,8 +64,8 @@ function Page() {
/> />
</GridCol> </GridCol>
</Grid> </Grid>
<Text px={{ base: 'md', md: 100 }} ta={"justify"} fz={{ base: "h4", md: "h3" }} > <Text px={{ base: 'md', md: 100 }} ta={"justify"} fz="md" mt={4} >
Keamanan dan ketertiban lingkungan di Desa Darmasaba dijaga melalui peran aktif Pecalang dan Patwal (Patroli Pengawal). Mereka bertugas memastikan desa tetap aman, tertib, dan kondusif bagi seluruh warga. Pecalang dan Patwal (Patroli Pengawal) bertugas memastikan desa tetap aman, tertib, dan kondusif bagi seluruh warga.
</Text> </Text>
</Box> </Box>
<Box px={{ base: "md", md: 100 }}> <Box px={{ base: "md", md: 100 }}>

View File

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

View File

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

View File

@@ -45,7 +45,7 @@ function DetailPencegahanKriminalitas() {
const data = kriminalitasState.findUnique.data; const data = kriminalitasState.findUnique.data;
return ( return (
<Box py="md" px="md"> <Box py="md" px={{ base: 'md', md: 100 }}>
<Group mb="md"> <Group mb="md">
<Button <Button
variant="light" variant="light"

View File

@@ -41,13 +41,44 @@ 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 fz='md'>
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 ( return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap={22}> <Stack pos="relative" bg={colors.Bg} py="xl" gap={22}>
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
<Text fz={{ base: 'h1', md: '2.5rem' }} c={colors['blue-button']} fw="bold"> <Text fz={{ base: 'h1', md: '2.5rem' }} c={colors['blue-button']} fw="bold">
Pencegahan Kriminalitas Pencegahan Kriminalitas
</Text> </Text>
<Text c={colors['blue-button']} fz={{ base: 'h4', md: 'h3' }}> <Text fz='md'>
Keamanan Komunitas & Pencegahan Kriminal Keamanan Komunitas & Pencegahan Kriminal
</Text> </Text>
</Box> </Box>
@@ -61,31 +92,63 @@ function Page() {
Program Keamanan Berjalan Program Keamanan Berjalan
</Text> </Text>
<Stack pt={30} gap="lg"> <Stack pt={30} gap="lg">
{data.length > 0 ? ( <Box
data.map((item) => ( style={{
<a key={item.id} href={`/darmasaba/keamanan/pencegahan-kriminalitas/${item.id}`}> minHeight: 300, // sesuaikan: tinggi area yg muat 3 item
<Paper p="md" bg={colors['blue-button']} radius="md" shadow="sm"> }}
<Stack gap={"xs"}> >
{data.length > 0 ? (
data.map((item) => (
<Paper
key={item.id}
p="md"
radius="md"
shadow="sm"
style={{
cursor: 'pointer',
backgroundColor: colors['blue-button'],
transition: 'all 0.2s ease',
}}
onClick={() =>
router.push(`/darmasaba/keamanan/pencegahan-kriminalitas/${item.id}`)
}
onMouseEnter={(e) =>
(e.currentTarget.style.backgroundColor = '#1a3e7a')
}
onMouseLeave={(e) =>
(e.currentTarget.style.backgroundColor = colors['blue-button'])
}
>
<Stack gap="xs">
<Text fz="h3" c={colors['white-1']}> <Text fz="h3" c={colors['white-1']}>
{item.judul} {item.judul}
</Text> </Text>
</Stack> </Stack>
</Paper> </Paper>
</a> ))
)) ) : (
) : ( <Text c="dimmed">Tidak ada data pencegahan kriminalitas yang cocok</Text>
<Text color="dimmed"> )}
Tidak ada data pencegahan kriminalitas yang cocok </Box>
</Text>
)}
<Button <Button
mt={20} mt={20}
fullWidth fullWidth
radius="xl" radius="xl"
size="md" size="md"
bg={colors['blue-button']} variant="outline"
rightSection={<IconArrowRight size={20} color={colors['white-1']} />} color="blue"
onClick={() => router.push(`/darmasaba/keamanan/pencegahan-kriminalitas/program-lainnya`)} rightSection={<IconArrowRight size={20} />}
styles={{
root: {
fontWeight: 600,
borderWidth: 2,
},
}}
onClick={() =>
router.push(
`/darmasaba/keamanan/pencegahan-kriminalitas/program-lainnya`
)
}
> >
Jelajahi Program Lainnya Jelajahi Program Lainnya
</Button> </Button>
@@ -111,9 +174,7 @@ function Page() {
<Text py={10} fz={{ base: 'h3', md: 'h2' }} fw="bold" c={colors['blue-button']}> <Text py={10} fz={{ base: 'h3', md: 'h2' }} fw="bold" c={colors['blue-button']}>
{findFirst.data?.judul} {findFirst.data?.judul}
</Text> </Text>
<Text fz="h4" c={colors['blue-button']}> <Text fz="h4" dangerouslySetInnerHTML={{ __html: findFirst.data?.deskripsiSingkat }} />
{findFirst.data?.deskripsiSingkat}
</Text>
</Paper> </Paper>
) : null} ) : null}
</Box> </Box>

View File

@@ -21,12 +21,23 @@ import pencegahanKriminalitasState from '@/app/admin/(dashboard)/_state/keamanan
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { useState } from 'react'; import { useState } from 'react';
import HeaderSearch from '@/app/admin/(dashboard)/_com/header'; import HeaderSearch from '@/app/admin/(dashboard)/_com/header';
import { IconArrowLeft } from '@tabler/icons-react';
function PencegahanKriminalitas() { function PencegahanKriminalitas() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const router = useRouter();
return ( return (
<Box> <Box pt={20} px={{ base: 'md', md: 100 }}>
<Group mb="md">
<Button
variant="light"
color="blue"
onClick={() => router.back()}
leftSection={<IconArrowLeft size={20} />}
>
Kembali
</Button>
</Group>
<HeaderSearch <HeaderSearch
title="Program Pencegahan Kriminalitas" title="Program Pencegahan Kriminalitas"
placeholder="Cari program atau deskripsi..." placeholder="Cari program atau deskripsi..."
@@ -82,7 +93,7 @@ function ListPencegahanKriminalitas({ search }: { search: string }) {
c="dimmed" c="dimmed"
lineClamp={2} lineClamp={2}
dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat || '' }} dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat || '' }}
style={{wordBreak: "break-word", whiteSpace: "normal"}} style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/> />
<Group justify="flex-end" mt="sm"> <Group justify="flex-end" mt="sm">
<Tooltip label="Lihat detail program" withArrow> <Tooltip label="Lihat detail program" withArrow>

View File

@@ -43,7 +43,7 @@ function Page() {
<Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}> <Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
Kantor Polisi Terdekat Kantor Polisi Terdekat
</Text> </Text>
<Text pb={15} fz={'h4'} > <Text pb={15} fz={'md'} >
Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung
</Text> </Text>
</Box> </Box>

View File

@@ -78,15 +78,12 @@ function Page() {
<Box> <Box>
<Text fz="h4" fw="bold">Pendahuluan</Text> <Text fz="h4" fw="bold">Pendahuluan</Text>
<Divider my="xs" /> <Divider my="xs" />
<Text fz="md" lh={1.6} ta="justify"> <Text fz="md" lh={1.6} ta="justify" dangerouslySetInnerHTML={{ __html: state.findUnique.data.introduction?.content }} />
{state.findUnique.data.introduction?.content}
</Text>
</Box> </Box>
<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" /> <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 }} /> <Text fz="md" lh={1.6} ta="justify" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: state.findUnique.data.symptom?.content }} />
</Box> </Box>

View File

@@ -1,7 +1,7 @@
'use client' 'use client'
import artikelKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/artikelKesehatan'; import artikelKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/artikelKesehatan';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Anchor, Box, Card, Divider, Group, Image, Loader, Paper, Stack, Text, Title, Tooltip } from '@mantine/core'; import { Box, Button, Card, Divider, Group, Image, Loader, Paper, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconCalendar, IconChevronRight } from '@tabler/icons-react'; import { IconCalendar, IconChevronRight } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@@ -28,9 +28,9 @@ function ArtikelKesehatanPage() {
<Box> <Box>
<Paper p="xl" bg={colors['white-trans-1']} radius="xl" shadow="md"> <Paper p="xl" bg={colors['white-trans-1']} radius="xl" shadow="md">
<Stack gap="lg"> <Stack gap="lg">
<Title order={2} ta="center" c={colors['blue-button']}> <Text ta="center" fw={700} fz="32px" c={colors['blue-button']}>
Artikel Kesehatan Artikel Kesehatan
</Title> </Text>
<Divider size="sm" color={colors['blue-button']} /> <Divider size="sm" color={colors['blue-button']} />
{state.findMany.data.length === 0 ? ( {state.findMany.data.length === 0 ? (
<Box py="xl" ta="center"> <Box py="xl" ta="center">
@@ -51,7 +51,7 @@ function ArtikelKesehatanPage() {
onMouseLeave={(e) => (e.currentTarget.style.transform = 'translateY(0)')} onMouseLeave={(e) => (e.currentTarget.style.transform = 'translateY(0)')}
> >
<Card.Section> <Card.Section>
<Image src={item.image?.link} alt={item.title} height={200} fit="cover" loading="lazy"/> <Image style={{ borderTopLeftRadius: '10px', borderTopRightRadius: '10px' }} src={item.image?.link} alt={item.title} height={200} fit="cover" loading="lazy" />
</Card.Section> </Card.Section>
<Stack gap="xs" mt="md"> <Stack gap="xs" mt="md">
<Text fw="bold" fz="xl" c="dark">{item.title}</Text> <Text fw="bold" fz="xl" c="dark">{item.title}</Text>
@@ -64,18 +64,17 @@ function ArtikelKesehatanPage() {
<Text fz="md" c="dark" lineClamp={3}> <Text fz="md" c="dark" lineClamp={3}>
{item.content} {item.content}
</Text> </Text>
<Tooltip label="Baca artikel lengkap"> <Group justify="flex-start">
<Anchor <Button
bg={colors['blue-button']}
radius="lg"
size="sm"
rightSection={<IconChevronRight size={18} />}
onClick={() => router.push(`/darmasaba/kesehatan/data-kesehatan-warga/artikel-kesehatan-page/${item.id}`)} onClick={() => router.push(`/darmasaba/kesehatan/data-kesehatan-warga/artikel-kesehatan-page/${item.id}`)}
variant="light"
c={colors['blue-button']}
> >
<Group gap="xs"> Baca Selengkapnya
<Text fw="bold" fz="md">Baca Selengkapnya</Text> </Button>
<IconChevronRight size={18} /> </Group>
</Group>
</Anchor>
</Tooltip>
</Stack> </Stack>
</Card> </Card>
)) ))

View File

@@ -3,9 +3,9 @@
import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan'; import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto'; import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
import colors from '@/con/colors'; 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 { 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 { useParams } from 'next/navigation';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
@@ -149,11 +149,6 @@ function Page() {
</CopyButton> </CopyButton>
</Group> </Group>
</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> </Stack>
</Card> </Card>
</Box> </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={<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> <Button variant="light" leftSection={<IconMail size={18} />} component="a" href={`mailto:${kontak.email}`} aria-label="Kirim Email">Email</Button>
</Group> </Group>
<Anchor target="_blank" underline="hover">Kunjungi situs resmi</Anchor>
</Stack> </Stack>
</Card> </Card>
@@ -246,15 +240,8 @@ function Page() {
</Table> </Table>
</Stack> </Stack>
</Card> </Card>
</Stack>
</Grid.Col>
</Grid>
</Box>
<Box px={{ base: 'md', md: 100 }}> <Card radius="xl" p="lg" withBorder>
<Grid gutter="lg">
<Grid.Col span={{ base: 12, md: 8 }}>
<Card radius="xl" p="lg" withBorder>
<Stack gap="md"> <Stack gap="md">
<Title order={3}>Fasilitas Pendukung</Title> <Title order={3}>Fasilitas Pendukung</Title>
<Divider /> <Divider />
@@ -270,8 +257,7 @@ function Page() {
)} )}
</Stack> </Stack>
</Card> </Card>
</Grid.Col>
<Grid.Col span={{ base: 12, md: 4 }}>
<Card radius="xl" p="lg" withBorder> <Card radius="xl" p="lg" withBorder>
<Stack gap="md"> <Stack gap="md">
<Title order={3}>Layanan & Tarif</Title> <Title order={3}>Layanan & Tarif</Title>
@@ -309,10 +295,11 @@ function Page() {
)} )}
</Stack> </Stack>
</Card> </Card>
</Stack>
</Grid.Col> </Grid.Col>
</Grid> </Grid>
</Box> </Box>
<Box px={{ base: 'md', md: 100 }} pb="xl"> <Box px={{ base: 'md', md: 100 }} pb="xl">
<Paper radius="xl" p="lg" withBorder> <Paper radius="xl" p="lg" withBorder>
<Stack gap="md"> <Stack gap="md">

View File

@@ -1,11 +1,11 @@
'use client' 'use client'
import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan'; import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Anchor, Badge, Box, Card, Divider, Group, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { Badge, Box, Button, Card, Divider, Group, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconChevronRight, IconClock, IconMapPin } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { IconMapPin, IconClock, IconArrowRight } from '@tabler/icons-react';
function FasilitasKesehatanPage() { function FasilitasKesehatanPage() {
const state = useProxy(fasilitasKesehatanState.fasilitasKesehatan); const state = useProxy(fasilitasKesehatanState.fasilitasKesehatan);
@@ -36,72 +36,73 @@ function FasilitasKesehatanPage() {
</Text> </Text>
<Divider size="sm" color={colors['blue-button']} /> <Divider size="sm" color={colors['blue-button']} />
<Stack gap="lg"> <Stack gap="lg">
{state.findMany.data.length === 0 ? ( {state.findMany.data.length === 0 ? (
<Box py="xl" ta="center"> <Box py="xl" ta="center">
<Text fz="lg" c="dimmed"> <Text fz="lg" c="dimmed">
Belum ada fasilitas kesehatan yang tersedia Belum ada fasilitas kesehatan yang tersedia
</Text> </Text>
</Box> </Box>
) : ( ) : (
state.findMany.data.map((item) => ( state.findMany.data.map((item) => (
<Card <Card
key={item.id} key={item.id}
withBorder withBorder
radius="xl" radius="xl"
shadow="sm" shadow="sm"
p="lg" p="lg"
style={{ style={{
background: 'linear-gradient(135deg, #fdfdfd, #f7faff)', background: 'linear-gradient(135deg, #fdfdfd, #f7faff)',
transition: 'transform 0.2s ease, box-shadow 0.2s ease', transition: 'transform 0.2s ease, box-shadow 0.2s ease',
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
(e.currentTarget as HTMLElement).style.transform = 'translateY(-4px)'; (e.currentTarget as HTMLElement).style.transform = 'translateY(-4px)';
(e.currentTarget as HTMLElement).style.boxShadow = '0 8px 20px rgba(0,0,0,0.08)'; (e.currentTarget as HTMLElement).style.boxShadow = '0 8px 20px rgba(0,0,0,0.08)';
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
(e.currentTarget as HTMLElement).style.transform = 'translateY(0px)'; (e.currentTarget as HTMLElement).style.transform = 'translateY(0px)';
(e.currentTarget as HTMLElement).style.boxShadow = '0 4px 12px rgba(0,0,0,0.05)'; (e.currentTarget as HTMLElement).style.boxShadow = '0 4px 12px rgba(0,0,0,0.05)';
}} }}
> >
<Stack gap="sm"> <Stack gap="sm">
<Group justify="space-between" align="center"> <Group justify="space-between" align="center">
<Text fw={700} fz="lg" c={colors['blue-button']}> <Text fw={700} fz="lg" c={colors['blue-button']}>
{item.name} {item.name}
</Text> </Text>
<Badge color="blue" radius="sm" variant="light" fz="xs"> <Badge color="blue" radius="sm" variant="light" fz="xs">
Aktif Aktif
</Badge> </Badge>
</Group> </Group>
<Group gap="xs"> <Group gap="xs">
<IconMapPin size={18} stroke={1.5} color={colors['blue-button']} /> <IconMapPin size={18} stroke={1.5} color={colors['blue-button']} />
<Text fz="sm" c="dimmed"> <Text fz="sm" c="dimmed">
{item.informasiumum.alamat} {item.informasiumum.alamat}
</Text> </Text>
</Group> </Group>
<Group gap="xs"> <Group gap="xs">
<IconClock size={18} stroke={1.5} color={colors['blue-button']} /> <IconClock size={18} stroke={1.5} color={colors['blue-button']} />
<Text fz="sm" c="dimmed"> <Text fz="sm" c="dimmed">
{item.informasiumum.jamOperasional} {item.informasiumum.jamOperasional}
</Text> </Text>
</Group> </Group>
<Anchor <Group justify="flex-start">
onClick={() => <Button
router.push( bg={colors['blue-button']}
`/darmasaba/kesehatan/data-kesehatan-warga/fasilitas-kesehatan-page/${item.id}` radius="lg"
) size="sm"
} rightSection={<IconChevronRight size={18} />}
c={colors['blue-button']} onClick={() =>
fz="sm" router.push(
fw={600} `/darmasaba/kesehatan/data-kesehatan-warga/fasilitas-kesehatan-page/${item.id}`
style={{ display: 'inline-flex', alignItems: 'center', gap: '4px' }} )
> }
Lihat Detail >
<IconArrowRight size={16} stroke={1.5} /> Lihat Detail
</Anchor> </Button>
</Stack> </Group>
</Card> </Stack>
)) </Card>
)} ))
)}
</Stack> </Stack>
</Stack> </Stack>
</Paper> </Paper>

View File

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

View File

@@ -73,9 +73,9 @@ function JadwalKegiatanPage() {
<Divider my="sm" /> <Divider my="sm" />
<Group justify="flex-end"> <Group justify="flex-start">
<Button <Button
variant="light" bg={colors['blue-button']}
radius="lg" radius="lg"
size="sm" size="sm"
rightSection={<IconChevronRight size={18} />} rightSection={<IconChevronRight size={18} />}
@@ -84,14 +84,6 @@ function JadwalKegiatanPage() {
`/darmasaba/kesehatan/data-kesehatan-warga/jadwal-kegiatan-page/${item.id}` `/darmasaba/kesehatan/data-kesehatan-warga/jadwal-kegiatan-page/${item.id}`
) )
} }
styles={{
root: {
background: colors['blue-button'],
color: 'white',
boxShadow: '0 0 12px rgba(0, 123, 255, 0.4)',
transition: 'all 0.2s ease',
},
}}
> >
Lihat Detail & Daftar Lihat Detail & Daftar
</Button> </Button>

View File

@@ -2,7 +2,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import colors from '@/con/colors'; import colors from '@/con/colors';
import { BarChart as MantineBarChart } from '@mantine/charts'; 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 { useEffect, useState } from 'react';
import BackButton from '../../desa/layanan/_com/BackButto'; import BackButton from '../../desa/layanan/_com/BackButto';
@@ -107,20 +107,7 @@ function Page() {
<Box> <Box>
<Paper p={"xl"} bg={colors['white-trans-1']}> <Paper p={"xl"} bg={colors['white-trans-1']}>
<Box pb={30}> <Box pb={30}>
<Flex pb={30} justify={'flex-end'} gap={'xl'} align={'center'}> <Title order={2} mb="md">Data Kematian dan Kelahiran</Title>
<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>
{chartData.length === 0 ? ( {chartData.length === 0 ? (
<Text c="dimmed" ta="center" py="xl"> <Text c="dimmed" ta="center" py="xl">
Belum ada data yang tersedia untuk ditampilkan Belum ada data yang tersedia untuk ditampilkan
@@ -150,6 +137,20 @@ function Page() {
</Center> </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> </Box>
</Paper> </Paper>
</Box> </Box>
@@ -163,11 +164,11 @@ function Page() {
}} }}
> >
{/* Fasilitas Kesehatan */} {/* Fasilitas Kesehatan */}
<FasilitasKesehatan/> <FasilitasKesehatan />
{/* Jadwal Kegiatan */} {/* Jadwal Kegiatan */}
<JadwalKegiatan/> <JadwalKegiatan />
{/* Artikel Kesehatan */} {/* Artikel Kesehatan */}
<ArtikelKesehatanPage/> <ArtikelKesehatanPage />
</SimpleGrid> </SimpleGrid>
</Box> </Box>
</Stack> </Stack>

View File

@@ -28,16 +28,18 @@ function DetailInfoWabahPenyakitUser() {
const data = state.findUnique.data; const data = state.findUnique.data;
return ( return (
<Box py={10}> <Box py={10} px={{ base: 'md', md: 100 }}>
{/* Tombol Back */} {/* Tombol Back */}
<Button <Box>
variant="subtle" <Button
onClick={() => router.back()} variant="subtle"
leftSection={<IconArrowBack size={22} color={colors['blue-button']} />} onClick={() => router.back()}
mb={15} leftSection={<IconArrowBack size={22} color={colors['blue-button']} />}
> mb={15}
Kembali >
</Button> Kembali
</Button>
</Box>
{/* Wrapper Detail */} {/* Wrapper Detail */}
<Paper <Paper

View File

@@ -61,7 +61,7 @@ function Page() {
> >
Informasi Wabah & Penyakit Informasi Wabah & Penyakit
</Text> </Text>
<Text fz="md" c="dimmed" mt={4}> <Text fz="md" mt={4}>
Dapatkan informasi terbaru mengenai wabah dan penyakit yang sedang Dapatkan informasi terbaru mengenai wabah dan penyakit yang sedang
diawasi. diawasi.
</Text> </Text>
@@ -84,7 +84,7 @@ function Page() {
<Center py="6xl"> <Center py="6xl">
<Stack align="center" gap="sm"> <Stack align="center" gap="sm">
<IconInfoCircle size={50} color={colors['blue-button']} /> <IconInfoCircle size={50} color={colors['blue-button']} />
<Text fz="lg" fw={500} c="dimmed"> <Text fz="lg" fw={500} >
Tidak ada data yang cocok dengan pencarian Anda. Tidak ada data yang cocok dengan pencarian Anda.
</Text> </Text>
</Stack> </Stack>
@@ -101,17 +101,35 @@ function Page() {
bg={colors['white-trans-1']} bg={colors['white-trans-1']}
style={{ style={{
transition: 'transform 200ms ease, box-shadow 200ms ease', transition: 'transform 200ms ease, box-shadow 200ms ease',
display: 'flex',
flexDirection: 'column',
}} }}
> >
<Stack gap="sm"> <Stack gap="sm" style={{ flex: 1 }}>
<Image {/* Gambar */}
radius="md" <Box
h={180} h={180}
src={v.image.link} w="100%"
alt={v.name} style={{
fit="cover" overflow: 'hidden',
loading="lazy" borderRadius: '8px',
/> }}
>
<Image
src={v.image?.link}
alt={v.name}
fit="cover"
w="100%"
h="100%"
style={{
objectFit: 'cover',
objectPosition: 'center',
}}
loading="lazy"
/>
</Box>
{/* Judul dan badge */}
<Group justify="space-between" mt="sm"> <Group justify="space-between" mt="sm">
<Text fw={700} fz="lg" c={colors['blue-button']}> <Text fw={700} fz="lg" c={colors['blue-button']}>
{v.name} {v.name}
@@ -120,20 +138,46 @@ function Page() {
Wabah Wabah
</Badge> </Badge>
</Group> </Group>
<Text fz="sm" c="dimmed"> <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> </Text>
<Divider /> <Divider />
<Text fz="sm" lh={1.5}>
{v.deskripsiSingkat} {/* Bagian deskripsi dan tombol */}
</Text> <Box style={{ display: 'flex', flexDirection: 'column', flexGrow: 1 }}>
<Button variant="light" radius="md" size="md" onClick={() => router.push(`/admin/kesehatan/info-wabah-penyakit/${v.id}`)}> <Text
Selengkapnya fz="sm"
</Button> lh={1.5}
lineClamp={3}
dangerouslySetInnerHTML={{ __html: v.deskripsiSingkat }}
style={{ flexGrow: 1 }}
/>
<Button
variant="light"
radius="md"
size="md"
mt="md"
onClick={() =>
router.push(`/darmasaba/kesehatan/info-wabah-penyakit/${v.id}`)
}
>
Selengkapnya
</Button>
</Box>
</Stack> </Stack>
</Paper> </Paper>
))} ))}
</SimpleGrid> </SimpleGrid>
)} )}
</Box> </Box>

View File

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

View File

@@ -0,0 +1,86 @@
'use client';
import penangananDarurat from '@/app/admin/(dashboard)/_state/kesehatan/penanganan-darurat/penangananDarurat';
import colors from '@/con/colors';
import { Box, Button, Image, Paper, Skeleton, Stack, Text } 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 DetailPenangananDaruratUser() {
const state = useProxy(penangananDarurat);
const router = useRouter();
const params = useParams();
useShallowEffect(() => {
state.findUnique.load(params?.id as string);
}, []);
if (!state.findUnique.data) {
return (
<Stack py={40}>
<Skeleton height={400} radius="md" />
<Skeleton height={20} width="80%" radius="md" />
<Skeleton height={20} width="60%" radius="md" />
</Stack>
);
}
const data = state.findUnique.data;
return (
<Box py={20}>
{/* Tombol Back */}
<Box px={{ base: 'md', md: 100 }}>
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={22} color={colors['blue-button']} />}
mb={20}
>
Kembali
</Button>
</Box>
{/* Wrapper Detail */}
<Paper
withBorder
w={{ base: '100%', md: '70%', lg: '60%' }}
mx="auto"
bg={colors['white-1']}
p="xl"
radius="lg"
shadow="sm"
>
<Stack gap="md" align="center" ta="center">
<Text fz="xl" fw={700} c={colors['blue-button']}>
{data.name || 'Penanganan Darurat'}
</Text>
{data.image?.link && (
<Image
src={data.image.link}
alt={data.name}
radius="md"
mah={300}
fit="contain"
loading="lazy"
mb="md"
/>
)}
<Box>
<Text
fz="md"
ta="justify"
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
/>
</Box>
</Stack>
</Paper>
</Box>
);
}
export default DetailPenangananDaruratUser;

View File

@@ -2,8 +2,8 @@
import penangananDarurat from '@/app/admin/(dashboard)/_state/kesehatan/penanganan-darurat/penangananDarurat' import penangananDarurat from '@/app/admin/(dashboard)/_state/kesehatan/penanganan-darurat/penangananDarurat'
import colors from '@/con/colors' import colors from '@/con/colors'
import { import {
Badge,
Box, Box,
Button,
Center, Center,
Grid, Grid,
GridCol, GridCol,
@@ -97,19 +97,43 @@ function Page() {
shadow="sm" shadow="sm"
withBorder withBorder
bg={colors['white-trans-1']} 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"> <Stack align="center" gap="md">
<Center> <Center>
<Image <Box
src={v.image.link} style={{
alt={v.name} width: '100%',
w={160} height: 180, // 🔥 tinggi fix biar semua seragam
h={160} borderRadius: 12,
fit="contain" overflow: 'hidden',
radius="md" position: 'relative',
loading="lazy" 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> </Center>
<Stack gap={4} w="100%"> <Stack gap={4} w="100%">
<Text <Text
@@ -125,15 +149,20 @@ function Page() {
<Text <Text
fz="sm" fz="sm"
c="dimmed" c="dimmed"
lineClamp={4} lineClamp={3}
dangerouslySetInnerHTML={{ __html: v.deskripsi }} dangerouslySetInnerHTML={{ __html: v.deskripsi }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }} style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/> />
</Box> </Box>
<Button
radius="xl"
size="md"
component="a"
href={`/darmasaba/kesehatan/penanganan-darurat/${v.id}`}
>
Lihat Detail
</Button>
</Stack> </Stack>
<Badge radius="md" color="blue" variant="light" mt="sm">
Darurat
</Badge>
</Stack> </Stack>
</Paper> </Paper>
))} ))}
@@ -151,8 +180,11 @@ function Page() {
styles={{ styles={{
control: { control: {
border: `1px solid ${colors['blue-button']}`, border: `1px solid ${colors['blue-button']}`,
transition: 'all 0.3s ease',
'&:hover': { backgroundColor: colors['blue-button'], color: 'white' },
}, },
}} }}
/> />
</Center> </Center>

View File

@@ -0,0 +1,121 @@
'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>
{/* Konten utama */}
<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="center" gap="xs">
<IconPhone size={18} stroke={1.5} />
<Text fz="sm" c="dimmed">
{data.nomor || 'Nomor tidak tersedia'}
</Text>
</Flex>
<Flex align="center" gap="xs">
<IconCalendar size={18} stroke={1.5} />
<Text
fz="sm"
c="dimmed"
dangerouslySetInnerHTML={{ __html: data.jadwalPelayanan || '-' }}
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
/>
</Flex>
<Flex align="center" gap="xs">
<IconInfoCircle size={18} stroke={1.5} />
<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' 'use client'
import posyandustate from "@/app/admin/(dashboard)/_state/kesehatan/posyandu/posyandu"; import posyandustate from "@/app/admin/(dashboard)/_state/kesehatan/posyandu/posyandu";
import colors from "@/con/colors"; 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 { Badge, Box, Button, Center, Flex, Group, Image, List, ListItem, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks"; import { useDebouncedValue, useShallowEffect } from "@mantine/hooks";
import { IconCalendar, IconInfoCircle, IconPhone, IconSearch } from "@tabler/icons-react"; import { IconCalendar, IconInfoCircle, IconPhone, IconSearch } from "@tabler/icons-react";
import { useState } from "react"; import { useState } from "react";
import { useProxy } from "valtio/utils"; import { useProxy } from "valtio/utils";
import BackButton from "../../desa/layanan/_com/BackButto"; import BackButton from "../../desa/layanan/_com/BackButto";
import { useTransitionRouter } from "next-view-transitions";
export default function Page() { export default function Page() {
const state = useProxy(posyandustate); const state = useProxy(posyandustate);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const router = useTransitionRouter()
const { data, page, totalPages, loading, load } = state.findMany; const { data, page, totalPages, loading, load } = state.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 6, search); load(page, 6, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
if (loading || !data) { if (loading || !data) {
return ( return (
@@ -28,11 +31,31 @@ export default function Page() {
if (data.length === 0) { if (data.length === 0) {
return ( return (
<Box py="xl" px={{ base: "md", md: 100 }}> <Stack pos="relative" bg={colors.Bg} py="xl" gap="xl">
<Text fz="lg" fw="bold" c={colors["blue-button"]}> <Box px={{ base: "md", md: 100 }}>
Tidak ada posyandu yang ditemukan <BackButton />
</Text> <Flex mt="md" justify="space-between" align="center" wrap="wrap" gap="md">
</Box> <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" loading="lazy"
/> />
</Center> </Center>
<Flex align="center" gap="xs"> <Flex align="flex-start" gap="xs">
<IconPhone size={18} stroke={1.5} /> <IconPhone size={18} stroke={1.5} style={{ marginTop: 3 }} />
<Text fz="sm" c="dimmed"> <Box>
{v.nomor || "Tidak tersedia"} <Text fz="sm" c="dimmed" lh={1.4}>
</Text> {v.nomor || "Tidak tersedia"}
</Text>
</Box>
</Flex> </Flex>
<Flex align="center" gap="xs">
<IconCalendar size={18} stroke={1.5} /> <Flex align="flex-start" gap="xs">
<Text fz="sm" c="dimmed"> <IconCalendar size={18} stroke={1.5} style={{ marginTop: 3 }} />
Jadwal:{" "} <Box>
<span style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: v.jadwalPelayanan }} /> <Text fz="sm" c="dimmed" lh={1.4}>
</Text> <strong>Jadwal:</strong>{" "}
<span
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
dangerouslySetInnerHTML={{ __html: v.jadwalPelayanan }}
/>
</Text>
</Box>
</Flex> </Flex>
<Spoiler
key={`spoiler-${v.id}`} <Flex align="flex-start" gap="xs">
maxHeight={70} <IconInfoCircle size={18} stroke={1.5} style={{ marginTop: 3 }} />
showLabel="Lihat selengkapnya"
hideLabel="Sembunyikan"
transitionDuration={300}
>
<Text <Text
fz="sm" fz="sm"
lh={1.5} lh={1.5}
c="dimmed"
dangerouslySetInnerHTML={{ __html: v.deskripsi }} dangerouslySetInnerHTML={{ __html: v.deskripsi }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }} 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> </Stack>
</Paper> </Paper>
))} ))}

View File

@@ -88,7 +88,7 @@ export default function Page() {
> >
Program Kesehatan Desa Program Kesehatan Desa
</Text> </Text>
<Text fz="lg" c="dimmed" mt="xs"> <Text fz="lg" mt="xs">
Temukan berbagai program kesehatan untuk mendukung kualitas hidup Temukan berbagai program kesehatan untuk mendukung kualitas hidup
masyarakat Darmasaba. masyarakat Darmasaba.
</Text> </Text>
@@ -126,17 +126,36 @@ export default function Page() {
className="hover-scale" className="hover-scale"
> >
<Stack gap="md"> <Stack gap="md">
<Box h={180} w="100%"> <Center>
<Image <Box
src={v.image?.link} style={{
alt={v.name} width: '100%',
radius="xl" height: 180, // 🔥 tinggi fix biar semua seragam
w="100%" borderRadius: 12,
h="100%" overflow: 'hidden',
fit="cover" position: 'relative',
loading="lazy" backgroundColor: '#f0f2f5', // fallback kalau gambar loading
/> }}
</Box> >
<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"> <Box px="lg" pb="lg">
<Text <Text
@@ -149,7 +168,7 @@ export default function Page() {
</Text> </Text>
<Text <Text
fz="sm" fz="sm"
c="dimmed" ta={"justify"}
lineClamp={3} lineClamp={3}
dangerouslySetInnerHTML={{ __html: v.deskripsi }} dangerouslySetInnerHTML={{ __html: v.deskripsi }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }} style={{ wordBreak: "break-word", whiteSpace: "normal" }}
@@ -230,7 +249,7 @@ export default function Page() {
> >
Manfaat Program Kesehatan Manfaat Program Kesehatan
</Text> </Text>
<Text fz="lg" c="dimmed" maw={700}> <Text fz="lg" maw={700}>
Program kesehatan Desa Darmasaba berperan penting dalam meningkatkan Program kesehatan Desa Darmasaba berperan penting dalam meningkatkan
kesejahteraan dan kualitas hidup warganya. kesejahteraan dan kualitas hidup warganya.
</Text> </Text>
@@ -260,7 +279,7 @@ export default function Page() {
<Text ta="center" fw="bold" fz="xl" c={colors["blue-button"]}> <Text ta="center" fw="bold" fz="xl" c={colors["blue-button"]}>
{v.title} {v.title}
</Text> </Text>
<Text ta="center" fz="sm" c="dimmed"> <Text ta="center" fz="sm">
{v.desc} {v.desc}
</Text> </Text>
</Stack> </Stack>

View File

@@ -43,7 +43,7 @@ function Page() {
<Text fz={{ base: "2rem", md: "2.5rem" }} c={colors["blue-button"]} fw="bold"> <Text fz={{ base: "2rem", md: "2.5rem" }} c={colors["blue-button"]} fw="bold">
Daftar Puskesmas Daftar Puskesmas
</Text> </Text>
<Text fz="sm" c="dimmed"> <Text fz="md">
Temukan informasi lengkap mengenai layanan, kontak, dan lokasi Puskesmas Darmasaba Temukan informasi lengkap mengenai layanan, kontak, dan lokasi Puskesmas Darmasaba
</Text> </Text>
</GridCol> </GridCol>

View File

@@ -71,8 +71,11 @@ function Page() {
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
/> />
</Group> </Group>
<Text fz="lg" c={'black'}> <Text fz="md" >
Desa Darmasaba menjaga dan mengembangkan lingkungan demi kesejahteraan warganya. Fokus utama meliputi penghijauan, pengelolaan sampah, dan perlindungan kawasan hijau. Desa Darmasaba menjaga dan mengembangkan lingkungan demi kesejahteraan warganya.
</Text>
<Text fz="md">
Fokus utama meliputi penghijauan, pengelolaan sampah, dan perlindungan kawasan hijau.
</Text> </Text>
</Box> </Box>
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>

View File

@@ -0,0 +1,66 @@
// Create a new component: components/EdukasiCard.tsx
'use client';
import { Box, Paper, Stack, Text, Tooltip } from '@mantine/core';
import { ReactNode } from 'react';
interface EdukasiCardProps {
icon: ReactNode;
title: string;
description: string;
color?: string;
}
export function EdukasiCard({ icon, title, description, color = '#1e88e5' }: EdukasiCardProps) {
return (
<Paper
p={{ base: 'md', md: 'lg' }}
radius="md"
shadow="sm"
withBorder
style={{
height: '100%',
transition: 'transform 0.2s, box-shadow 0.2s',
'&:hover': {
transform: 'translateY(-4px)',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)'
}
}}
>
<Stack h="100%" justify="space-between" gap="md">
<Box>
<Stack align="center" gap="xs" mb="md">
<Box style={{ color }}>{icon}</Box>
<Tooltip label={title} maw={250} multiline withArrow position="top">
<Text
fz={{ base: 'h5', md: 'h4' }}
fw={700}
c={color}
ta="center"
lineClamp={2}
style={{
wordBreak: 'break-word',
minHeight: '3.5rem',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
{title}
</Text>
</Tooltip>
</Stack>
<Text
size="sm"
style={{
wordBreak: 'break-word',
lineHeight: 1.6,
color: 'var(--mantine-color-gray-7)'
}}
dangerouslySetInnerHTML={{ __html: description }}
/>
</Box>
</Stack>
</Paper>
);
}

View File

@@ -1,130 +1,104 @@
'use client' 'use client';
import stateEdukasiLingkungan from '@/app/admin/(dashboard)/_state/lingkungan/edukasi-lingkungan';
import colors from '@/con/colors'; import { Box, Container, SimpleGrid, Skeleton, Stack, Text } from '@mantine/core';
import { Box, Paper, SimpleGrid, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconLeaf, IconPlant2, IconRecycle } from '@tabler/icons-react'; import { IconLeaf, IconPlant2, IconRecycle } from '@tabler/icons-react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto'; import BackButton from '../../desa/layanan/_com/BackButto';
function Page() { import stateEdukasiLingkungan from '@/app/admin/(dashboard)/_state/lingkungan/edukasi-lingkungan';
const tujuan = useProxy(stateEdukasiLingkungan.stateTujuanEdukasi.findById) import colors from '@/con/colors';
const materi = useProxy(stateEdukasiLingkungan.stateMateriEdukasiLingkungan.findById) import { EdukasiCard } from './component/edukasiCard';
const contoh = useProxy(stateEdukasiLingkungan.stateContohEdukasiLingkungan.findById)
function LoadingSkeleton() {
return (
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="lg">
{[1, 2, 3].map((item) => (
<Skeleton key={item} height={300} radius="md" />
))}
</SimpleGrid>
);
}
export default function EdukasiLingkunganPage() {
const tujuan = useProxy(stateEdukasiLingkungan.stateTujuanEdukasi.findById);
const materi = useProxy(stateEdukasiLingkungan.stateMateriEdukasiLingkungan.findById);
const contoh = useProxy(stateEdukasiLingkungan.stateContohEdukasiLingkungan.findById);
useShallowEffect(() => { useShallowEffect(() => {
tujuan.load('edit') tujuan.load('edit');
materi.load('edit') materi.load('edit');
contoh.load('edit') contoh.load('edit');
}, []) }, []);
if (tujuan.loading || !tujuan.data || materi.loading || !materi.data || contoh.loading || !contoh.data) { const isLoading = tujuan.loading || !tujuan.data ||
materi.loading || !materi.data ||
contoh.loading || !contoh.data;
if (isLoading) {
return ( return (
<Stack py={20}> <Stack py="xl" px={{ base: 'md', md: 'xl' }}>
<Skeleton radius="md" height={600} /> <BackButton />
<LoadingSkeleton />
</Stack> </Stack>
); );
} }
return ( return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22"> <Stack bg={colors.Bg} py="xl" gap="xl" px={{ base: 'md', md: 'xl' }}>
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
<BackButton /> <BackButton />
</Box> </Box>
<Box px={{ base: 'md', md: 100 }} pb={20}> <Container size="lg" ta="center">
<Text ta={'center'} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw="bold"> <Text
component="h1"
fz={{ base: 'h2', md: '2.5rem' }}
c={colors['blue-button']}
fw={700}
mb="md"
>
Edukasi Lingkungan Edukasi Lingkungan
</Text> </Text>
<Text ta={'center'} fz="h4" c="black"> <Text
fz={{ base: 'md', md: 'lg' }}
c="dimmed"
maw={800}
mx="auto"
>
Program edukasi ini membimbing masyarakat untuk peduli dan bertanggung jawab terhadap alam, Program edukasi ini membimbing masyarakat untuk peduli dan bertanggung jawab terhadap alam,
meningkatkan kesehatan, kenyamanan, dan keberlanjutan hidup bersama. meningkatkan kesehatan, kenyamanan, dan keberlanjutan hidup bersama.
</Text> </Text>
</Box> </Container>
<Box px={{ base: 'md', md: 100 }}> <Container size="xl">
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg" style={{ alignItems: 'stretch' }}> <SimpleGrid
{/* Tujuan Edukasi Lingkungan */} cols={{ base: 1, sm: 2, lg: 3 }}
<Box style={{ display: 'flex', height: '100%' }}> spacing="xl"
<Paper p={20} bg={colors['white-trans-1']} shadow="md" radius="md" style={{ width: '100%', display: 'flex', flexDirection: 'column' }}> verticalSpacing={{ base: 'md', md: 'xl' }}
<Stack gap="md"> >
<Box> <EdukasiCard
<Tooltip label={tujuan.data?.judul} position="top" withArrow> icon={<IconLeaf size={32} />}
<Stack gap={4} align="center"> title={tujuan.data?.judul || ''}
<IconLeaf size={28} color={colors['blue-button']} /> description={tujuan.data?.deskripsi || ''}
<Text fz="h3" fw="bold" c={colors['blue-button']} ta="center"> color={colors['blue-button']}
{tujuan.data?.judul} />
</Text>
</Stack> <EdukasiCard
</Tooltip> icon={<IconRecycle size={32} />}
</Box> title={materi.data?.judul || ''}
<Text description={materi.data?.deskripsi || ''}
style={{ color={colors['blue-button']}
wordBreak: "break-word", />
whiteSpace: "normal",
flexGrow: 1 <EdukasiCard
}} icon={<IconPlant2 size={32} />}
dangerouslySetInnerHTML={{ __html: tujuan.data?.deskripsi || '' }} title={contoh.data?.judul || ''}
/> description={contoh.data?.deskripsi || ''}
<Box style={{ flexGrow: 1 }} /> color={colors['blue-button']}
</Stack> />
</Paper>
</Box>
{/* Materi Edukasi Lingkungan */}
<Box style={{ display: 'flex', height: '100%' }}>
<Paper p={20} bg={colors['white-trans-1']} shadow="md" radius="md">
<Stack gap="md">
<Box>
<Tooltip label={materi.data?.judul} position="top" withArrow>
<Stack gap={4} align="center">
<IconRecycle size={28} color={colors['blue-button']} />
<Text fz="h3" fw="bold" c={colors['blue-button']} ta="center">
{materi.data?.judul}
</Text>
</Stack>
</Tooltip>
</Box>
<Text
style={{
wordBreak: "break-word",
whiteSpace: "normal",
flexGrow: 1
}}
dangerouslySetInnerHTML={{ __html: materi.data?.deskripsi || '' }}
/>
<Box style={{ flexGrow: 1 }} />
</Stack>
</Paper>
</Box>
{/* Contoh Edukasi Lingkungan */}
<Box style={{ display: 'flex', height: '100%' }}>
<Paper p={20} bg={colors['white-trans-1']} shadow="md" radius="md">
<Stack gap="md">
<Box>
<Tooltip label={contoh.data?.judul} position="top" withArrow>
<Stack gap={4} align="center">
<IconPlant2 size={28} color={colors['blue-button']} />
<Text fz="h3" fw="bold" c={colors['blue-button']} ta="center">
{contoh.data?.judul}
</Text>
</Stack>
</Tooltip>
</Box>
<Text
style={{
wordBreak: "break-word",
whiteSpace: "normal",
flexGrow: 1
}}
dangerouslySetInnerHTML={{ __html: contoh.data?.deskripsi || '' }}
/>
</Stack>
</Paper>
</Box>
</SimpleGrid> </SimpleGrid>
</Box> </Container>
</Stack> </Stack>
); );
} }
export default Page;

View File

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

View File

@@ -1,323 +1,5 @@
// 'use client'
// import colors from '@/con/colors';
// import { Box, Container, Grid, GridCol, Stack, Tabs, TabsList, TabsTab, Text, TextInput } from '@mantine/core';
// import { IconSearch } from '@tabler/icons-react';
// import { usePathname, useRouter, useSearchParams } from 'next/navigation';
// import React, { useEffect, useState } from 'react';
// import BackButton from '../../../desa/layanan/_com/BackButto';
// type HeaderSearchProps = {
// placeholder?: string;
// searchIcon?: React.ReactNode;
// value?: string;
// onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
// children?: React.ReactNode;
// };
// function LayoutTabsGotongRoyong({
// children,
// placeholder = "pencarian",
// searchIcon = <IconSearch size={20} />
// }: HeaderSearchProps) {
// const router = useRouter();
// const pathname = usePathname();
// const searchParams = useSearchParams();
// // Get active tab from URL path
// const activeTab = pathname.split('/').pop() || 'semua';
// // Get initial search value from URL
// const initialSearch = searchParams.get('search') || '';
// const [searchValue, setSearchValue] = useState(initialSearch);
// const [searchTimeout, setSearchTimeout] = useState<number | null>(null);
// // Update active tab state when pathname changes
// const [activeTabState, setActiveTabState] = useState(activeTab);
// useEffect(() => {
// setActiveTabState(activeTab);
// }, [activeTab]);
// // Clean up timeouts on unmount
// useEffect(() => {
// return () => {
// if (searchTimeout !== null) {
// clearTimeout(searchTimeout);
// }
// };
// }, [searchTimeout]);
// // Handle search input change with debounce
// const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
// const value = event.target.value;
// setSearchValue(value);
// // Clear previous timeout
// if (searchTimeout !== null) {
// clearTimeout(searchTimeout);
// }
// // Set new timeout
// const newTimeout = window.setTimeout(() => {
// const params = new URLSearchParams(searchParams.toString());
// if (value) {
// params.set('search', value);
// } else {
// params.delete('search');
// }
// // Only update URL if the search value has actually changed
// if (params.toString() !== searchParams.toString()) {
// router.push(`/darmasaba/lingkungan/gotong-royong/${activeTab}?${params.toString()}`);
// }
// }, 500); // 500ms debounce delay
// setSearchTimeout(newTimeout);
// };
// const tabs = [
// {
// label: "Semua",
// value: "semua",
// href: "/darmasaba/lingkungan/gotong-royong/semua"
// },
// {
// label: "Kebersihan",
// value: "kebersihan",
// href: "/darmasaba/lingkungan/gotong-royong/kebersihan"
// },
// {
// label: "Infrastruktur",
// value: "infrastruktur",
// href: "/darmasaba/lingkungan/gotong-royong/infrastruktur"
// },
// {
// label: "Sosial",
// value: "sosial",
// href: "/darmasaba/lingkungan/gotong-royong/sosial"
// },
// {
// label: "Lingkungan",
// value: "lingkungan",
// href: "/darmasaba/lingkungan/gotong-royong/lingkungan"
// }
// ];
// const handleTabChange = (value: string | null) => {
// if (!value) return;
// const tab = tabs.find(t => t.value === value);
// if (tab) {
// const params = new URLSearchParams(searchParams.toString());
// router.push(`/darmasaba/lingkungan/gotong-royong/${value}${params.toString() ? `?${params.toString()}` : ''}`);
// }
// };
// return (
// <Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
// {/* Header */}
// <Box px={{ base: "md", md: 100 }}>
// <BackButton />
// </Box>
// <Container size="lg" px="md">
// <Stack align="center" gap="0" >
// <Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" ta="center">
// Gotong Royong Desa Darmasaba
// </Text>
// <Text ta="center" px="md">
// Gotong royong rutin dilakukan oleh warga desa untuk meningkatkan kualitas hidup dan kesejahteraan masyarakat Desa Darmasaba
// </Text>
// </Stack>
// </Container>
// <Tabs
// color={colors['blue-button']}
// variant="pills"
// value={activeTabState}
// onChange={handleTabChange}
// >
// <Box px={{ base: "md", md: 100 }} py="md" bg={colors['BG-trans']}>
// <Grid>
// <GridCol span={{ base: 12, md: 9, lg: 8, xl: 9 }}>
// <TabsList>
// {tabs.map((tab, index) => (
// <TabsTab
// key={index}
// value={tab.value}
// onClick={() => router.push(tab.href)}
// >
// {tab.label}
// </TabsTab>
// ))}
// </TabsList>
// </GridCol>
// <GridCol span={{ base: 12, md: 3, lg: 4, xl: 3 }}>
// <TextInput
// radius="lg"
// placeholder={placeholder}
// leftSection={searchIcon}
// w="100%"
// value={searchValue}
// onChange={handleSearchChange}
// />
// </GridCol>
// </Grid>
// </Box>
// {children}
// </Tabs>
// </Stack>
// );
// }
// export default LayoutTabsGotongRoyong;
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
// 'use client'
// import colors from '@/con/colors';
// import { Box, Group, Stack, Tabs, TabsList, TabsTab, Text, TextInput } from '@mantine/core';
// import { IconSearch } from '@tabler/icons-react';
// import { usePathname, useRouter, useSearchParams } from 'next/navigation';
// import React, { useEffect, useState } from 'react';
// import BackButton from '../../layanan/_com/BackButto';
// type HeaderSearchProps = {
// placeholder?: string;
// searchIcon?: React.ReactNode;
// value?: string;
// onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
// children?: React.ReactNode;
// };
// function LayoutTabsBerita({
// children,
// placeholder = "pencarian",
// searchIcon = <IconSearch size={20} />
// }: HeaderSearchProps) {
// const router = useRouter();
// const pathname = usePathname();
// const searchParams = useSearchParams();
// const activeTab = pathname.split('/').pop() || 'semua';
// const initialSearch = searchParams.get('search') || '';
// const [searchValue, setSearchValue] = useState(initialSearch);
// const [searchTimeout, setSearchTimeout] = useState<number | null>(null);
// const [activeTabState, setActiveTabState] = useState(activeTab);
// useEffect(() => {
// setActiveTabState(activeTab);
// }, [activeTab]);
// useEffect(() => {
// return () => {
// if (searchTimeout !== null) clearTimeout(searchTimeout);
// };
// }, [searchTimeout]);
// const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
// const value = event.target.value;
// setSearchValue(value);
// if (searchTimeout !== null) clearTimeout(searchTimeout);
// const newTimeout = window.setTimeout(() => {
// const params = new URLSearchParams(searchParams.toString());
// if (value) params.set('search', value);
// else params.delete('search');
// if (params.toString() !== searchParams.toString()) {
// router.push(`/darmasaba/desa/berita/${activeTab}?${params.toString()}`);
// }
// }, 500);
// setSearchTimeout(newTimeout);
// };
// const tabs = [
// { label: "Semua", value: "semua", href: "/darmasaba/desa/berita/semua" },
// { label: "Budaya", value: "budaya", href: "/darmasaba/desa/berita/budaya" },
// { label: "Pemerintahan", value: "pemerintahan", href: "/darmasaba/desa/berita/pemerintahan" },
// { label: "Ekonomi", value: "ekonomi", href: "/darmasaba/desa/berita/ekonomi" },
// { label: "Pembangunan", value: "pembangunan", href: "/darmasaba/desa/berita/pembangunan" },
// { label: "Sosial", value: "sosial", href: "/darmasaba/desa/berita/sosial" },
// { label: "Teknologi", value: "teknologi", href: "/darmasaba/desa/berita/teknologi" },
// ];
// const handleTabChange = (value: string | null) => {
// if (!value) return;
// const tab = tabs.find(t => t.value === value);
// if (tab) {
// const params = new URLSearchParams(searchParams.toString());
// router.push(`/darmasaba/desa/berita/${value}${params.toString() ? `?${params.toString()}` : ''}`);
// }
// };
// return (
// <Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
// {/* Header */}
// <Box px={{ base: "md", md: 100 }}>
// <BackButton />
// </Box>
// <Box px={{ base: 'md', md: 100 }}>
// <Group justify='space-between' align="center">
// <Stack gap="0">
// <Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" >
// Portal Berita Darmasaba
// </Text>
// <Text>
// Temukan berbagai potensi dan keunggulan yang dimiliki Desa Darmasaba
// </Text>
// </Stack>
// <Box>
// <TextInput
// radius="lg"
// placeholder={placeholder}
// leftSection={searchIcon}
// w="100%"
// value={searchValue}
// onChange={handleSearchChange}
// />
// </Box>
// </Group>
// </Box>
// <Tabs
// color={colors['blue-button']}
// variant="pills"
// value={activeTabState}
// onChange={handleTabChange}
// >
// <Box px={{ base: "md", md: 100 }} py="md" bg={colors['BG-trans']}>
// {/* SCROLLABLE TABS */}
// <Box style={{ overflowX: 'auto', whiteSpace: 'nowrap' }}>
// <TabsList style={{ display: 'flex', flexWrap: 'nowrap', gap: '0.5rem' }}>
// {tabs.map((tab, index) => (
// <TabsTab
// key={index}
// value={tab.value}
// onClick={() => router.push(tab.href)}
// style={{
// flex: '0 0 auto', // Prevent shrinking
// minWidth: 100, // optional: makes them touch-friendly
// textAlign: 'center'
// }}
// >
// {tab.label}
// </TabsTab>
// ))}
// </TabsList>
// </Box>
// </Box>
// {children}
// </Tabs>
// </Stack>
// );
// }
// export default LayoutTabsBerita;
'use client' 'use client'
import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong'; import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong';
@@ -402,7 +84,7 @@ function LayoutTabsGotongRoyong({ children }: { children: React.ReactNode }) {
<Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold"> <Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold">
Portal Gotong royong Darmasaba Portal Gotong royong Darmasaba
</Text> </Text>
<Text>Temukan berbagai kegiatan lingkungan yang dimiliki Desa Darmasaba</Text> <Text fz="md">Temukan berbagai kegiatan lingkungan yang dimiliki Desa Darmasaba</Text>
</Stack> </Stack>
<Box> <Box>
<TextInput <TextInput

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