Compare commits

...

6 Commits

Author SHA1 Message Date
a9d98895bb Fix Admin Menu SDGs Desa & APBdes Desa, Fix UI IMage Layanan Landing Page & Layanan Desa 2025-09-09 17:14:28 +08:00
75475dc62e Fix Package.json Bun 2025-09-08 21:52:17 +08:00
b39800a475 Fix UI Admin Keamanan Lingkungan 2025-09-08 15:45:56 +08:00
797713ef49 Fix UI Admin Menu Kesehatan, Login Admin, OTP 2025-09-08 14:02:21 +08:00
8817b937b1 API Auth 2025-09-04 11:46:08 +08:00
2adf60f9eb Fix UI Admin menu desa 2025-09-03 15:30:02 +08:00
198 changed files with 15499 additions and 10113 deletions

2
.gitignore vendored
View File

@@ -48,3 +48,5 @@ next-env.d.ts
.env.* .env.*
*.tar.gz

BIN
bun.lockb

Binary file not shown.

View File

@@ -3,11 +3,9 @@
"version": "0.1.5", "version": "0.1.5",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev --turbopack", "dev": "bun --bun next dev",
"build": "next build", "build": "bun --bun next build",
"start": "next start", "start": "bun --bun next start"
"lint": "next lint",
"prisma:seed": "bun run prisma/seed.ts"
}, },
"prisma": { "prisma": {
"seed": "bun run prisma/seed.ts" "seed": "bun run prisma/seed.ts"
@@ -57,6 +55,8 @@
"form-data": "^4.0.2", "form-data": "^4.0.2",
"framer-motion": "^12.23.5", "framer-motion": "^12.23.5",
"get-port": "^7.1.0", "get-port": "^7.1.0",
"iron-session": "^8.0.4",
"jose": "^6.1.0",
"jotai": "^2.12.3", "jotai": "^2.12.3",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
@@ -64,7 +64,7 @@
"lodash": "^4.17.21", "lodash": "^4.17.21",
"motion": "^12.4.1", "motion": "^12.4.1",
"nanoid": "^5.1.5", "nanoid": "^5.1.5",
"next": "15.1.6", "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",
"p-limit": "^6.2.0", "p-limit": "^6.2.0",
@@ -73,6 +73,7 @@
"prisma": "^6.3.1", "prisma": "^6.3.1",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-international-phone": "^4.6.0",
"react-leaflet": "^5.0.0", "react-leaflet": "^5.0.0",
"react-simple-toasts": "^6.1.0", "react-simple-toasts": "^6.1.0",
"react-toastify": "^11.0.5", "react-toastify": "^11.0.5",

View File

@@ -1,7 +1,7 @@
[ [
{ {
"id": "role_admin_desa", "id": "1",
"name": "ADMIN_DESA", "name": "ADMIN DESA",
"description": "Administrator Desa", "description": "Administrator Desa",
"permissions": ["manage_users", "manage_content", "view_reports"], "permissions": ["manage_users", "manage_content", "view_reports"],
"isActive": true, "isActive": true,
@@ -9,8 +9,8 @@
"updatedAt": "2025-09-01T00:00:00.000Z" "updatedAt": "2025-09-01T00:00:00.000Z"
}, },
{ {
"id": "role_admin_kesehatan", "id": "2",
"name": "ADMIN_KESEHATAN", "name": "ADMIN KESEHATAN",
"description": "Administrator Bidang Kesehatan", "description": "Administrator Bidang Kesehatan",
"permissions": ["manage_health_data", "view_reports"], "permissions": ["manage_health_data", "view_reports"],
"isActive": true, "isActive": true,
@@ -18,8 +18,8 @@
"updatedAt": "2025-09-01T00:00:00.000Z" "updatedAt": "2025-09-01T00:00:00.000Z"
}, },
{ {
"id": "role_admin_sekolah", "id": "3",
"name": "ADMIN_SEKOLAH", "name": "ADMIN SEKOLAH",
"description": "Administrator Sekolah", "description": "Administrator Sekolah",
"permissions": ["manage_school_data", "view_reports"], "permissions": ["manage_school_data", "view_reports"],
"isActive": true, "isActive": true,

View File

@@ -1,32 +1,29 @@
[ [
{ {
"id": "user_admin_desa", "id": "1",
"nama": "Admin Desa", "nama": "Admin Desa",
"email": "admin.desa@example.com", "nomor": "089647037426",
"password": "$2b$10$XFDWYOJFxQ7ZM5bA0N4Z0O8u0eKYv58wLsaR7h6XK9bqWJ1YQJQ9q", "roleId": "1",
"roleId": "role_admin_desa",
"isActive": true, "isActive": true,
"lastLogin": "2025-08-31T10:00:00.000Z", "lastLogin": "2025-08-31T10:00:00.000Z",
"createdAt": "2025-09-01T00:00:00.000Z", "createdAt": "2025-09-01T00:00:00.000Z",
"updatedAt": "2025-09-01T00:00:00.000Z" "updatedAt": "2025-09-01T00:00:00.000Z"
}, },
{ {
"id": "user_admin_puskesmas", "id": "2",
"nama": "Admin Kesehatan", "nama": "Admin Kesehatan",
"email": "admin.kesehatan@example.com", "nomor": "082339004198",
"password": "$2b$10$XFDWYOJFxQ7ZM5bA0N4Z0O8u0eKYv58wLsaR7h6XK9bqWJ1YQJQ9q", "roleId": "2",
"roleId": "role_admin_kesehatan",
"isActive": true, "isActive": true,
"lastLogin": null, "lastLogin": null,
"createdAt": "2025-09-01T00:00:00.000Z", "createdAt": "2025-09-01T00:00:00.000Z",
"updatedAt": "2025-09-01T00:00:00.000Z" "updatedAt": "2025-09-01T00:00:00.000Z"
}, },
{ {
"id": "user_admin_sekolah", "id": "3",
"nama": "Admin Sekolah", "nama": "Admin Sekolah",
"email": "admin.sekolah@example.com", "nomor": "085237157222",
"password": "$2b$10$XFDWYOJFxQ7ZM5bA0N4Z0O8u0eKYv58wLsaR7h6XK9bqWJ1YQJQ9q", "roleId": "3",
"roleId": "role_admin_sekolah",
"isActive": true, "isActive": true,
"lastLogin": null, "lastLogin": null,
"createdAt": "2025-09-01T00:00:00.000Z", "createdAt": "2025-09-01T00:00:00.000Z",

View File

@@ -2103,14 +2103,16 @@ model KategoriBuku {
DataPerpustakaan DataPerpustakaan[] DataPerpustakaan DataPerpustakaan[]
} }
// ========================================= USER ========================================= //
model User { model User {
id String @id @default(cuid()) id String @id @default(cuid())
nama String username String
email String @unique nomor String @unique
password String?
role Role @relation(fields: [roleId], references: [id]) role Role @relation(fields: [roleId], references: [id])
roleId String roleId String @default("1")
instansi String? // Nama instansi (Puskesmas, Sekolah, dll) instansi String?
UserSession UserSession? // Nama instansi (Puskesmas, Sekolah, dll)
isActive Boolean @default(true) isActive Boolean @default(true)
lastLogin DateTime? lastLogin DateTime?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@@ -2132,6 +2134,15 @@ model Role {
@@map("roles") @@map("roles")
} }
model KodeOtp {
id String @id @default(cuid())
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
nomor String
otp Int
}
// Tabel untuk menyimpan permission // Tabel untuk menyimpan permission
model Permission { model Permission {
id String @id @default(cuid()) id String @id @default(cuid())
@@ -2143,6 +2154,17 @@ model Permission {
@@map("permissions") @@map("permissions")
} }
model UserSession {
id String @id @default(cuid())
token String
expires DateTime?
active Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
User User @relation(fields: [userId], references: [id])
userId String @unique
}
// ========================================= DATA PENDIDIKAN ========================================= // // ========================================= DATA PENDIDIKAN ========================================= //
model DataPendidikan { model DataPendidikan {
id String @id @default(cuid()) id String @id @default(cuid())

View File

@@ -57,6 +57,9 @@ import users from "./data/user/users.json";
(async () => { (async () => {
// =========== USER & ROLE =========== // =========== USER & ROLE ===========
// In your seed.ts
// =========== ROLES ===========
console.log("🔄 Seeding roles...");
for (const r of roles) { for (const r of roles) {
await prisma.role.upsert({ await prisma.role.upsert({
where: { id: r.id }, where: { id: r.id },
@@ -64,36 +67,46 @@ import users from "./data/user/users.json";
name: r.name, name: r.name,
description: r.description, description: r.description,
permissions: r.permissions, permissions: r.permissions,
isActive: true, isActive: r.isActive,
}, },
create: { create: {
id: r.id, id: r.id,
name: r.name, name: r.name,
description: r.description, description: r.description,
permissions: r.permissions, permissions: r.permissions,
isActive: true, isActive: r.isActive,
}, },
}); });
} }
console.log("✅ Roles seeded"); console.log("✅ Roles seeded");
//users
// =========== USERS ===========
console.log("🔄 Seeding users...");
for (const u of users) { for (const u of users) {
// First verify the role exists
const roleExists = await prisma.role.findUnique({
where: { id: u.roleId }
});
if (!roleExists) {
console.error(`❌ Role with id ${u.roleId} not found for user ${u.nama}`);
continue;
}
await prisma.user.upsert({ await prisma.user.upsert({
where: { id: u.id }, where: { id: u.id },
update: { update: {
nama: u.nama, username: u.nama,
email: u.email, nomor: u.nomor,
password: u.password,
roleId: u.roleId, roleId: u.roleId,
isActive: true, isActive: u.isActive,
}, },
create: { create: {
id: u.id, id: u.id,
nama: u.nama, username: u.nama,
email: u.email, nomor: u.nomor,
password: u.password,
roleId: u.roleId, roleId: u.roleId,
isActive: true, isActive: u.isActive,
}, },
}); });
} }

View File

@@ -368,11 +368,37 @@ const kategoriBerita = proxy({
isActive: true; isActive: true;
}; };
}>[], }>[],
page: 1,
totalPages: 1,
loading: false, loading: false,
async load() { search: "",
const res = await ApiFetch.api.desa.kategoriberita["findMany"].get(); load: async (page = 1, limit = 10, search = "") => {
if (res.status === 200) { kategoriBerita.findMany.loading = true; // ✅ Akses langsung via nama path
kategoriBerita.findMany.data = res.data?.data ?? []; kategoriBerita.findMany.page = page;
kategoriBerita.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.desa.kategoriberita[
"findMany"
].get({ query });
if (res.status === 200 && res.data?.success) {
kategoriBerita.findMany.data = res.data.data ?? [];
kategoriBerita.findMany.totalPages =
res.data.totalPages ?? 1;
} else {
kategoriBerita.findMany.data = [];
kategoriBerita.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch kategori berita paginated:", err);
kategoriBerita.findMany.data = [];
kategoriBerita.findMany.totalPages = 1;
} finally {
kategoriBerita.findMany.loading = false;
} }
}, },
}, },

View File

@@ -30,7 +30,6 @@ const templateTelunjukSaktiDesaForm = z.object({
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"), deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
}); });
const templatePelayananPerizinanBerusaha = z.object({ const templatePelayananPerizinanBerusaha = z.object({
name: z.string().min(3, "Nama minimal 3 karakter"), name: z.string().min(3, "Nama minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"), deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
@@ -72,7 +71,6 @@ const pelayananPendudukNonPermanenForm = {
deskripsi: "", deskripsi: "",
}; };
const suratKeterangan = proxy({ const suratKeterangan = proxy({
create: { create: {
form: { ...suratKeteranganForm }, form: { ...suratKeteranganForm },
@@ -113,14 +111,19 @@ const suratKeterangan = proxy({
totalPages: 1, totalPages: 1,
total: 0, total: 0,
loading: false, loading: false,
load: async (page = 1, limit = 10) => { // Change to arrow function search: "",
load: async (page = 1, limit = 10, search = "") => {
// Change to arrow function
suratKeterangan.findMany.loading = true; // Use the full path to access the property suratKeterangan.findMany.loading = true; // Use the full path to access the property
suratKeterangan.findMany.page = page; suratKeterangan.findMany.page = page;
suratKeterangan.findMany.search = search;
try { try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.desa.layanan.pelayanansuratketerangan[ const res = await ApiFetch.api.desa.layanan.pelayanansuratketerangan[
"find-many" "find-many"
].get({ ].get({
query: { page, limit }, query,
}); });
if (res.status === 200 && res.data?.success) { if (res.status === 200 && res.data?.success) {
@@ -341,28 +344,34 @@ const pelayananTelunjukSaktiDesa = proxy({
totalPages: 1, totalPages: 1,
total: 0, total: 0,
loading: false, loading: false,
load: async (page = 1, limit = 10) => { // Change to arrow function search: "",
load: async (page = 1, limit = 10, search = "") => {
// Change to arrow function
pelayananTelunjukSaktiDesa.findMany.loading = true; // Use the full path to access the property pelayananTelunjukSaktiDesa.findMany.loading = true; // Use the full path to access the property
pelayananTelunjukSaktiDesa.findMany.page = page; pelayananTelunjukSaktiDesa.findMany.page = page;
pelayananTelunjukSaktiDesa.findMany.search = search;
try { try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.desa.layanan.pelayanantelunjuksaktidesa[ const res = await ApiFetch.api.desa.layanan.pelayanantelunjuksaktidesa[
"find-many" "find-many"
].get({ ].get({
query: { page, limit }, query,
}); });
if (res.status === 200 && res.data?.success) { if (res.status === 200 && res.data?.success) {
pelayananTelunjukSaktiDesa.findMany.data = res.data.data || []; pelayananTelunjukSaktiDesa.findMany.data = res.data.data || [];
pelayananTelunjukSaktiDesa.findMany.total = res.data.total || 0; pelayananTelunjukSaktiDesa.findMany.total = res.data.total || 0;
pelayananTelunjukSaktiDesa.findMany.totalPages = res.data.totalPages || 1; pelayananTelunjukSaktiDesa.findMany.totalPages =
res.data.totalPages || 1;
} else { } else {
console.error("Failed to load telunjuk sakti desa:", res.data?.message); console.error("Failed to load surat keterangan:", res.data?.message);
pelayananTelunjukSaktiDesa.findMany.data = []; pelayananTelunjukSaktiDesa.findMany.data = [];
pelayananTelunjukSaktiDesa.findMany.total = 0; suratKeterangan.findMany.total = 0;
pelayananTelunjukSaktiDesa.findMany.totalPages = 1; suratKeterangan.findMany.totalPages = 1;
} }
} catch (error) { } catch (error) {
console.error("Error loading telunjuk sakti desa:", error); console.error("Error loading surat keterangan:", error);
pelayananTelunjukSaktiDesa.findMany.data = []; pelayananTelunjukSaktiDesa.findMany.data = [];
pelayananTelunjukSaktiDesa.findMany.total = 0; pelayananTelunjukSaktiDesa.findMany.total = 0;
pelayananTelunjukSaktiDesa.findMany.totalPages = 1; pelayananTelunjukSaktiDesa.findMany.totalPages = 1;
@@ -410,7 +419,9 @@ const pelayananTelunjukSaktiDesa = proxy({
); );
const result = await response.json(); const result = await response.json();
if (response.ok) { if (response.ok) {
toast.success(result.message || "Telunjuk Sakti Desa berhasil dihapus"); toast.success(
result.message || "Telunjuk Sakti Desa berhasil dihapus"
);
await pelayananTelunjukSaktiDesa.findMany.load(); // refresh list await pelayananTelunjukSaktiDesa.findMany.load(); // refresh list
} else { } else {
toast.error(result.message || "Gagal menghapus telunjuk sakti desa"); toast.error(result.message || "Gagal menghapus telunjuk sakti desa");
@@ -501,7 +512,9 @@ const pelayananTelunjukSaktiDesa = proxy({
} }
const result = await response.json(); const result = await response.json();
if (result.success) { if (result.success) {
toast.success(result.message || "Telunjuk Sakti Desa berhasil diupdate"); toast.success(
result.message || "Telunjuk Sakti Desa berhasil diupdate"
);
await pelayananTelunjukSaktiDesa.findMany.load(); // refresh list await pelayananTelunjukSaktiDesa.findMany.load(); // refresh list
return true; return true;
} else { } else {
@@ -522,7 +535,7 @@ const pelayananTelunjukSaktiDesa = proxy({
} }
}, },
}, },
}) });
const pelayananPerizinanBerusaha = proxy({ const pelayananPerizinanBerusaha = proxy({
findById: { findById: {
@@ -596,9 +609,7 @@ const pelayananPerizinanBerusaha = proxy({
} catch (error) { } catch (error) {
console.error("Error fetching pelayanan perizinan berusaha:", error); console.error("Error fetching pelayanan perizinan berusaha:", error);
toast.error( toast.error(
error instanceof Error error instanceof Error ? error.message : "Gagal memuat data"
? error.message
: "Gagal memuat data"
); );
return null; return null;
} }
@@ -713,9 +724,7 @@ const pelayananPendudukNonPermanen = proxy({
} catch (error) { } catch (error) {
console.error("Error fetching pelayanan penduduk non permanen:", error); console.error("Error fetching pelayanan penduduk non permanen:", error);
toast.error( toast.error(
error instanceof Error error instanceof Error ? error.message : "Gagal memuat data"
? error.message
: "Gagal memuat data"
); );
return null; return null;
} }

View File

@@ -56,14 +56,19 @@ const penghargaanState = proxy({
totalPages: 1, totalPages: 1,
total: 0, total: 0,
loading: false, loading: false,
load: async (page = 1, limit = 10) => { // Change to arrow function search: "",
load: async (page = 1, limit = 10, search = "") => {
// Change to arrow function
penghargaanState.findMany.loading = true; // Use the full path to access the property penghargaanState.findMany.loading = true; // Use the full path to access the property
penghargaanState.findMany.page = page; penghargaanState.findMany.page = page;
penghargaanState.findMany.search = search;
try { try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.desa.penghargaan[ const res = await ApiFetch.api.desa.penghargaan[
"find-many" "find-many"
].get({ ].get({
query: { page, limit }, query,
}); });
if (res.status === 200 && res.data?.success) { if (res.status === 200 && res.data?.success) {

View File

@@ -55,11 +55,39 @@ const category = proxy({
pengumumans: number; pengumumans: number;
}; };
})[], })[],
page: 1,
totalPages: 1,
total: 0,
loading: false, loading: false,
async load() { search: "",
const res = await ApiFetch.api.desa.kategoripengumuman["findMany"].get(); load: async (page = 1, limit = 10, search = "") => { // Change to arrow function
if (res.status === 200) { category.findMany.loading = true; // Use the full path to access the property
category.findMany.data = res.data?.data ?? []; category.findMany.page = page;
category.findMany.search = search;
try {
const res = await ApiFetch.api.desa.kategoripengumuman[
"findMany"
].get({
query: { page, limit },
});
if (res.status === 200 && res.data?.success) {
category.findMany.data = res.data.data || [];
category.findMany.total = res.data.total || 0;
category.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error("Failed to load potensi desa:", res.data?.message);
category.findMany.data = [];
category.findMany.total = 0;
category.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading potensi desa:", error);
category.findMany.data = [];
category.findMany.total = 0;
category.findMany.totalPages = 1;
} finally {
category.findMany.loading = false;
} }
}, },
}, },

View File

@@ -56,9 +56,11 @@ const potensiDesa = proxy({
totalPages: 1, totalPages: 1,
total: 0, total: 0,
loading: false, loading: false,
load: async (page = 1, limit = 10) => { // Change to arrow function search: "",
load: async (page = 1, limit = 10, search = "") => { // Change to arrow function
potensiDesa.findMany.loading = true; // Use the full path to access the property potensiDesa.findMany.loading = true; // Use the full path to access the property
potensiDesa.findMany.page = page; potensiDesa.findMany.page = page;
potensiDesa.findMany.search = search;
try { try {
const res = await ApiFetch.api.desa.potensi[ const res = await ApiFetch.api.desa.potensi[
"find-many" "find-many"
@@ -298,11 +300,34 @@ const kategoriPotensi = proxy({
isActive: true; isActive: true;
}; };
}>[], }>[],
page: 1,
totalPages: 1,
loading: false, loading: false,
async load() { search: "",
const res = await ApiFetch.api.desa.kategoripotensi["findMany"].get(); load: async (page = 1, limit = 10, search = "") => {
if (res.status === 200) { kategoriPotensi.findMany.loading = true; // ✅ Akses langsung via nama path
kategoriPotensi.findMany.data = res.data?.data ?? []; kategoriPotensi.findMany.page = page;
kategoriPotensi.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.desa.kategoripotensi["findMany"].get({ query });
if (res.status === 200 && res.data?.success) {
kategoriPotensi.findMany.data = res.data.data ?? [];
kategoriPotensi.findMany.totalPages = res.data.totalPages ?? 1;
} else {
kategoriPotensi.findMany.data = [];
kategoriPotensi.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch kategori potensi paginated:", err);
kategoriPotensi.findMany.data = [];
kategoriPotensi.findMany.totalPages = 1;
} finally {
kategoriPotensi.findMany.loading = false;
} }
}, },
}, },

View File

@@ -115,27 +115,38 @@ const artikelKesehatanState = proxy({
}; };
}>[] }>[]
| null, | null,
page: 1,
totalPages: 1,
loading: false, loading: false,
async load() { search: "",
load: async (page = 1, limit = 10, search = "") => {
artikelKesehatanState.findMany.loading = true; // ✅ Akses langsung via nama path
artikelKesehatanState.findMany.page = page;
artikelKesehatanState.findMany.search = search;
try { try {
this.loading = true; const query: any = { page, limit };
const res = await (ApiFetch.api.kesehatan as any)["artikel-kesehatan"][ if (search) query.search = search;
const res = await ApiFetch.api.kesehatan["artikel-kesehatan"][
"find-many" "find-many"
].get(); ].get({ query });
if (res.status === 200) { if (res.status === 200 && res.data?.success) {
this.data = res.data?.data ?? []; artikelKesehatanState.findMany.data =
res.data.data ?? [];
artikelKesehatanState.findMany.totalPages =
res.data.totalPages ?? 1;
} else { } else {
toast.error("Gagal memuat data artikel kesehatan"); artikelKesehatanState.findMany.data = [];
artikelKesehatanState.findMany.totalPages = 1;
} }
return res;
} catch (err) { } catch (err) {
toast.error("Terjadi error saat load data"); console.error("Gagal fetch artikel kesehatan paginated:", err);
console.error("LOAD ERROR:", err); artikelKesehatanState.findMany.data = [];
throw err; artikelKesehatanState.findMany.totalPages = 1;
} finally { } finally {
this.loading = false; artikelKesehatanState.findMany.loading = false;
} }
}, },
}, },
@@ -280,12 +291,9 @@ const artikelKesehatanState = proxy({
async byId(id: string) { async byId(id: string) {
try { try {
artikelKesehatanState.delete.loading = true; artikelKesehatanState.delete.loading = true;
const res = await fetch( const res = await fetch(`/api/kesehatan/artikel-kesehatan/del/${id}`, {
`/api/kesehatan/artikel-kesehatan/del/${id}`,
{
method: "DELETE", method: "DELETE",
} });
);
const result = await res.json(); const result = await res.json();
if (res.ok && result.success) { if (res.ok && result.success) {

View File

@@ -116,27 +116,38 @@ const fasilitasKesehatan = proxy({
}; };
}>[] }>[]
| null, | null,
page: 1,
totalPages: 1,
loading: false, loading: false,
async load() { search: "",
load: async (page = 1, limit = 10, search = "") => {
fasilitasKesehatanState.fasilitasKesehatan.findMany.loading = true; // ✅ Akses langsung via nama path
fasilitasKesehatanState.fasilitasKesehatan.findMany.page = page;
fasilitasKesehatanState.fasilitasKesehatan.findMany.search = search;
try { try {
this.loading = true; const query: any = { page, limit };
const res = await (ApiFetch.api.kesehatan as any)[ if (search) query.search = search;
"fasilitas-kesehatan"
]["find-many"].get();
if (res.status === 200) { const res = await ApiFetch.api.kesehatan["fasilitas-kesehatan"][
this.data = res.data?.data ?? []; "find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
fasilitasKesehatanState.fasilitasKesehatan.findMany.data =
res.data.data ?? [];
fasilitasKesehatanState.fasilitasKesehatan.findMany.totalPages =
res.data.totalPages ?? 1;
} else { } else {
toast.error("Gagal memuat data fasilitas kesehatan"); fasilitasKesehatanState.fasilitasKesehatan.findMany.data = [];
fasilitasKesehatanState.fasilitasKesehatan.findMany.totalPages = 1;
} }
return res;
} catch (err) { } catch (err) {
toast.error("Terjadi error saat load data"); console.error("Gagal fetch fasilitas kesehatan paginated:", err);
console.error("LOAD ERROR:", err); fasilitasKesehatanState.fasilitasKesehatan.findMany.data = [];
throw err; fasilitasKesehatanState.fasilitasKesehatan.findMany.totalPages = 1;
} finally { } finally {
this.loading = false; fasilitasKesehatanState.fasilitasKesehatan.findMany.loading = false;
} }
}, },
}, },
@@ -558,7 +569,7 @@ const dokter = proxy({
const fasilitasKesehatanState = proxy({ const fasilitasKesehatanState = proxy({
fasilitasKesehatan, fasilitasKesehatan,
dokter dokter,
}); });
export default fasilitasKesehatanState; export default fasilitasKesehatanState;

View File

@@ -120,27 +120,36 @@ const jadwalkegiatanState = proxy({
}; };
}>[] }>[]
| null, | null,
page: 1,
totalPages: 1,
loading: false, loading: false,
async load() { search: "",
load: async (page = 1, limit = 10, search = "") => {
jadwalkegiatanState.findMany.loading = true; // ✅ Akses langsung via nama path
jadwalkegiatanState.findMany.page = page;
jadwalkegiatanState.findMany.search = search;
try { try {
this.loading = true; const query: any = { page, limit };
const res = await (ApiFetch.api.kesehatan as any)[ if (search) query.search = search;
"jadwal-kegiatan"
]["find-many"].get();
if (res.status === 200) { const res = await ApiFetch.api.kesehatan["jadwal-kegiatan"][
this.data = res.data?.data ?? []; "find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
jadwalkegiatanState.findMany.data = res.data.data ?? [];
jadwalkegiatanState.findMany.totalPages = res.data.totalPages ?? 1;
} else { } else {
toast.error("Gagal memuat data jadwal kegiatan"); jadwalkegiatanState.findMany.data = [];
jadwalkegiatanState.findMany.totalPages = 1;
} }
return res;
} catch (err) { } catch (err) {
toast.error("Terjadi error saat load data"); console.error("Gagal fetch jadwal kegiatan paginated:", err);
console.error("LOAD ERROR:", err); jadwalkegiatanState.findMany.data = [];
throw err; jadwalkegiatanState.findMany.totalPages = 1;
} finally { } finally {
this.loading = false; jadwalkegiatanState.findMany.loading = false;
} }
}, },
}, },
@@ -227,29 +236,42 @@ const jadwalkegiatanState = proxy({
content: jadwalkegiatanState.edit.form.content, content: jadwalkegiatanState.edit.form.content,
informasiJadwalKegiatan: { informasiJadwalKegiatan: {
name: jadwalkegiatanState.edit.form.informasiJadwalKegiatan.name, name: jadwalkegiatanState.edit.form.informasiJadwalKegiatan.name,
tanggal: jadwalkegiatanState.edit.form.informasiJadwalKegiatan.tanggal, tanggal:
jadwalkegiatanState.edit.form.informasiJadwalKegiatan.tanggal,
waktu: jadwalkegiatanState.edit.form.informasiJadwalKegiatan.waktu, waktu: jadwalkegiatanState.edit.form.informasiJadwalKegiatan.waktu,
lokasi: jadwalkegiatanState.edit.form.informasiJadwalKegiatan.lokasi, lokasi:
jadwalkegiatanState.edit.form.informasiJadwalKegiatan.lokasi,
}, },
layananJadwalKegiatan: { layananJadwalKegiatan: {
content: jadwalkegiatanState.edit.form.layananJadwalKegiatan.content, content:
jadwalkegiatanState.edit.form.layananJadwalKegiatan.content,
}, },
deskripsiJadwalKegiatan: { deskripsiJadwalKegiatan: {
deskripsi: jadwalkegiatanState.edit.form.deskripsiJadwalKegiatan.deskripsi, deskripsi:
jadwalkegiatanState.edit.form.deskripsiJadwalKegiatan.deskripsi,
}, },
syaratKetentuanJadwalKegiatan: { syaratKetentuanJadwalKegiatan: {
content: jadwalkegiatanState.edit.form.syaratKetentuanJadwalKegiatan.content, content:
jadwalkegiatanState.edit.form.syaratKetentuanJadwalKegiatan
.content,
}, },
dokumenJadwalKegiatan: { dokumenJadwalKegiatan: {
content: jadwalkegiatanState.edit.form.dokumenJadwalKegiatan.content, content:
jadwalkegiatanState.edit.form.dokumenJadwalKegiatan.content,
}, },
pendaftaranJadwalKegiatan: { pendaftaranJadwalKegiatan: {
name: jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.name, name: jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.name,
tanggal: jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.tanggal, tanggal:
namaOrangtua: jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.namaOrangtua, jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.tanggal,
nomor: jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.nomor, namaOrangtua:
alamat: jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.alamat, jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan
catatan: jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.catatan, .namaOrangtua,
nomor:
jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.nomor,
alamat:
jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.alamat,
catatan:
jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.catatan,
}, },
}; };
@@ -305,7 +327,7 @@ const jadwalkegiatanState = proxy({
} finally { } finally {
jadwalkegiatanState.delete.loading = false; jadwalkegiatanState.delete.loading = false;
} }
} },
}, },
}); });

View File

@@ -90,6 +90,32 @@ const userState = proxy({
} }
}, },
}, },
updateActive: {
loading: false,
async submit(id: string, isActive: boolean) {
this.loading = true;
try {
const res = await fetch(`/api/user/updt`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id, isActive }),
});
const data = await res.json();
if (res.status === 200 && data.success) {
toast.success(data.message);
userState.findMany.load(userState.findMany.page, 10, userState.findMany.search);
} else {
toast.error(data.message || "Gagal update status user");
}
} catch (e) {
console.error(e);
toast.error("Gagal update status user");
} finally {
this.loading = false;
}
},
},
}); });
const templateRole = z.object({ const templateRole = z.object({

View File

@@ -0,0 +1,111 @@
'use client'
import { apiFetchLogin } from '@/app/admin/auth/_lib/api_fetch_auth';
import colors from '@/con/colors';
import { Box, Button, Center, Flex, Image, Paper, Stack, Text, Title } from '@mantine/core';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { PhoneInput } from "react-international-phone";
import "react-international-phone/style.css";
import { toast } from 'react-toastify';
function Login() {
const router = useRouter()
const [phone, setPhone] = useState("")
const [isError, setError] = useState(false)
const [loading, setLoading] = useState(false)
async function onLogin() {
const nomor = phone.substring(1);
if (nomor.length <= 4) return setError(true)
try {
setLoading(true);
const response = await apiFetchLogin({ nomor: nomor })
if (response && response.success) {
localStorage.setItem("hipmi_auth_code_id", response.kodeId);
toast.success(response.message);
router.push("/validasi", { scroll: false });
} else {
setLoading(false);
toast.error(response?.message);
}
} catch (error) {
setLoading(false)
console.log("Error Login", error)
toast.error("Terjadi kesalahan saat login")
}
}
return (
<Stack pos={"relative"} bg={colors.Bg}>
<Box px={{ base: 'md', md: 100 }} pb={50}>
<Stack align='center' justify='center' h={"100vh"}>
<Paper p={'xl'} radius={'md'} bg={colors['white-trans-1']}>
<Stack align='center' gap={"lg"}>
<Box>
<Title ta={"center"} order={2} fw={'bold'} c={colors['blue-button']}>
Login
</Title>
<Center>
<Image src={"/darmasaba-icon.png"} alt="" w={80} />
</Center>
</Box>
<Box>
{/* <Box mb={10}>
<Text c={colors['blue-button']} ta={"center"} fz={"sm"} fw={'bold'}>Masuk Untuk Akses Admin</Text>
<TextInput
label='Username'
placeholder='Username'
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
</Box> */}
<PhoneInput
countrySelectorStyleProps={{
buttonStyle: {
backgroundColor: colors['blue-button'],
},
}}
inputStyle={{ width: "100%"}}
defaultCountry="id"
onChange={(val) => {
setPhone(val);
}}
/>
{isError ? (
toast.error("Masukan nomor telepon anda")
) : (
""
)}
<Box py={20} >
<Button
fullWidth
bg={colors['blue-button']}
radius={'xl'}
onClick={onLogin}
loading={loading ? true : false}
>Masuk
</Button>
</Box>
<Flex justify={'center'} align={'center'}>
<Text>Belum punya akun? </Text>
<Button variant='transparent' component={Link} href={'/registrasi'}>
<Text c={colors['blue-button']} fw={'bold'}>Registrasi</Text>
</Button>
</Flex>
</Box>
</Stack>
</Paper>
</Stack>
</Box>
</Stack>
);
}
export default Login;

View File

@@ -0,0 +1,121 @@
/* eslint-disable @typescript-eslint/no-unused-expressions */
'use client'
import { apiFetchRegister } from '@/app/admin/auth/_lib/api_fetch_auth';
import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
import colors from '@/con/colors';
import { Box, Button, Center, Checkbox, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { PhoneInput } from "react-international-phone";
import "react-international-phone/style.css";
import { toast } from 'react-toastify';
function Registrasi() {
const [phone, setPhone] = useState("")
const router = useRouter()
const [value, setValue] = useState("")
const [isValue, setIsValue] = useState(false);
const [loading, setLoading] = useState(false);
async function onRegistarsi() {
if (value.length < 5) {
toast.error("Username minimal 5 karakter!");
return;
}
if (value.includes(" ")) {
toast.error("Username tidak boleh ada spasi!");
return;
}
if (!phone) {
toast.error("Nomor telepon wajib diisi!");
return;
}
try {
setLoading(true);
const respone = await apiFetchRegister({ nomor: phone, username: value });
if (respone.success) {
router.push("/login", { scroll: false });
toast.success(respone.message);
} else {
setLoading(false);
toast.error(respone.message);
}
} catch (error) {
setLoading(false);
console.log("Error Registrasi", error);
}
}
return (
<Stack pos={"relative"} bg={colors.Bg} gap={"22"} py={"xl"} h={"100vh"}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }}>
<Stack justify='center' align='center' h={"80vh"}>
<Paper p={'xl'} radius={'md'} bg={colors['white-trans-1']}>
<Stack align='center'>
<Title order={2} fw={'bold'} c={colors['blue-button']}>
Registrasi
</Title>
<Center>
<Image src={"/darmasaba-icon.png"} alt="" w={80} />
</Center>
<Box>
<TextInput placeholder='Username'
label='Username'
maxLength={50}
error={
value.length > 0 && value.length < 5
? "Minimal 5 karakter !"
: value.includes(" ")
? "Tidak boleh ada spasi"
: isValue
? "Masukan username anda"
: ""
}
onChange={(val) => {
val.currentTarget.value.length > 0 ? setIsValue(false) : "";
setValue(val.currentTarget.value);
}}
required
/>
<Box py={10}>
<Text fz={"sm"} >Nomor Telepon</Text>
<PhoneInput
countrySelectorStyleProps={{
buttonStyle: {
backgroundColor: colors['blue-button'],
},
}}
inputStyle={{ width: "100%" }}
defaultCountry="id"
onChange={(val) => {
setPhone(val);
}}
/>
</Box>
<Box pb={10}>
<Checkbox
label="Saya menyetujui syarat dan ketentuan yang berlaku"
/>
</Box>
<Box pb={20} >
<Button fullWidth bg={colors['blue-button']} radius={'xl'} onClick={onRegistarsi} loading={loading ? true : false}>Daftar</Button>
</Box>
</Box>
</Stack>
</Paper>
</Stack>
</Box>
</Stack>
);
}
export default Registrasi;

View File

@@ -0,0 +1,38 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, PinInput, Stack, Text, Title } from '@mantine/core';
import { useRouter } from 'next/navigation';
function Validasi() {
const router = useRouter()
return (
<Stack pos={"relative"} bg={colors.Bg}>
<Box px={{ base: 'md', md: 100 }} pb={50}>
<Stack align='center' justify='center' h={"100vh"}>
<Paper p={'xl'} radius={'md'} bg={colors['white-trans-1']}>
<Stack align='center' gap={"lg"}>
<Box>
<Title ta={"center"} order={2} fw={'bold'} c={colors['blue-button']}>
Kode Verifikasi
</Title>
</Box>
<Box>
<Box mb={10}>
<Text c={colors['blue-button']} ta={"center"} fz={"sm"} fw={'bold'}>Masukkan Kode Verifikasi</Text>
<PinInput type={/^[0-9]*$/} inputType="tel" inputMode="numeric" />
</Box>
<Box py={20} >
<Button onClick={() => router.push("/admin/landing-page/profile/program-inovasi")}>
Page
</Button>
</Box>
</Box>
</Stack>
</Paper>
</Stack>
</Box>
</Stack>
);
}
export default Validasi;

View File

@@ -1,9 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core'; import { 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 { IconFileText, IconBuildingStore, IconSparkles, IconUsers } from '@tabler/icons-react';
function LayoutTabsLayanan({ children }: { children: React.ReactNode }) { function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
const router = useRouter() const router = useRouter()
@@ -12,26 +13,35 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
{ {
label: "Pelayanan Surat Keterangan", label: "Pelayanan Surat Keterangan",
value: "pelayanansuratketerangan", value: "pelayanansuratketerangan",
href: "/admin/desa/layanan/pelayanan_surat_keterangan" href: "/admin/desa/layanan/pelayanan_surat_keterangan",
icon: <IconFileText size={18} stroke={1.8} />,
tooltip: "Layanan terkait surat keterangan resmi desa"
}, },
{ {
label: "Pelayanan Perizinan Berusaha", label: "Pelayanan Perizinan Berusaha",
value: "pelayananperizinanusaha", value: "pelayananperizinanusaha",
href: "/admin/desa/layanan/pelayanan_perizinan_berusaha" href: "/admin/desa/layanan/pelayanan_perizinan_berusaha",
icon: <IconBuildingStore size={18} stroke={1.8} />,
tooltip: "Layanan untuk izin usaha masyarakat"
}, },
{ {
label: "Pelayanan Telunjuk Sakti Desa", label: "Pelayanan Telunjuk Sakti Desa",
value: "pelayanantelunjuksaktidesa", value: "pelayanantelunjuksaktidesa",
href: "/admin/desa/layanan/pelayanan_telunjuk_sakti_desa" href: "/admin/desa/layanan/pelayanan_telunjuk_sakti_desa",
icon: <IconSparkles size={18} stroke={1.8} />,
tooltip: "Layanan inovasi khusus desa"
}, },
{ {
label: "Pelayanan Penduduk Non-Permanent", label: "Pelayanan Penduduk Non-Permanent",
value: "pelayanantelunjuknonpermanent", value: "pelayanannonpermanent",
href: "/admin/desa/layanan/pelayanan_penduduk_non_permanent" href: "/admin/desa/layanan/pelayanan_penduduk_non_permanent",
icon: <IconUsers size={18} stroke={1.8} />,
tooltip: "Pendataan penduduk non-permanent"
} }
]; ];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value); const currentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => { const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value) const tab = tabs.find(t => t.value === value)
@@ -49,22 +59,63 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
}, [pathname]) }, [pathname])
return ( return (
<Stack> <Stack gap="lg">
<Title order={3}>Layanan</Title> <Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Layanan</Title>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}> <Tabs
<TabsList p={"xs"} bg={"#BBC8E7FF"}> color={colors['blue-button']}
{tabs.map((e, i) => ( variant='pills'
<TabsTab key={i} value={e.value}>{e.label}</TabsTab> value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
}}
>
{tabs.map((tab, i) => (
<Tooltip
key={i}
label={tab.tooltip}
position="bottom"
withArrow
transitionProps={{ transition: 'pop', duration: 200 }}
>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))} ))}
</TabsList> </TabsList>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value}> {tabs.map((tab, i) => (
{/* Konten dummy, bisa diganti tergantung routing */} <TabsPanel
<></> key={i}
value={tab.value}
style={{
padding: "1.5rem",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
borderRadius: "1rem",
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
}}
>
{/* Konten dummy, bisa diganti sesuai routing */}
<>{children}</>
</TabsPanel> </TabsPanel>
))} ))}
</Tabs> </Tabs>
{children}
</Stack> </Stack>
); );
} }

View File

@@ -1,61 +1,108 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core'; import { 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 { IconNews, IconCategory } from '@tabler/icons-react';
function LayoutTabsBerita({ children }: { children: React.ReactNode }) { function LayoutTabsBerita({ children }: { children: React.ReactNode }) {
const router = useRouter() const router = useRouter();
const pathname = usePathname() const pathname = usePathname();
const tabs = [ const tabs = [
{ {
label: "List Berita", label: "List Berita",
value: "list_berita", value: "list_berita",
href: "/admin/desa/berita/list-berita" href: "/admin/desa/berita/list-berita",
icon: <IconNews size={18} stroke={1.8} />,
tooltip: "Lihat dan kelola semua berita desa"
}, },
{ {
label: "Kategori Berita", label: "Kategori Berita",
value: "kategori_berita", value: "kategori_berita",
href: "/admin/desa/berita/kategori-berita" href: "/admin/desa/berita/kategori-berita",
icon: <IconCategory size={18} stroke={1.8} />,
tooltip: "Kelola kategori berita desa"
}, },
]; ];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value); const currentTab = tabs.find(tab => tab.href === pathname);
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => { const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value) const tab = tabs.find(t => t.value === value);
if (tab) { if (tab) {
router.push(tab.href) router.push(tab.href);
}
setActiveTab(value)
} }
setActiveTab(value);
};
useEffect(() => { useEffect(() => {
const match = tabs.find(tab => tab.href === pathname) const match = tabs.find(tab => tab.href === pathname);
if (match) { if (match) {
setActiveTab(match.value) setActiveTab(match.value);
} }
}, [pathname]) }, [pathname]);
return ( return (
<Stack> <Stack gap="lg">
<Title order={3}>Gallery</Title> <Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Berita Desa</Title>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}> <Tabs
<TabsList p={"xs"} bg={"#BBC8E7FF"}> color={colors['blue-button']}
{tabs.map((e, i) => ( variant="pills"
<TabsTab key={i} value={e.value}>{e.label}</TabsTab> value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
}}
>
{tabs.map((tab, i) => (
<Tooltip
key={i}
label={tab.tooltip}
position="bottom"
withArrow
transitionProps={{ transition: 'pop', duration: 200 }}
>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))} ))}
</TabsList> </TabsList>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value}> {tabs.map((tab, i) => (
{/* Konten dummy, bisa diganti tergantung routing */} <TabsPanel
<></> key={i}
value={tab.value}
style={{
padding: "1.5rem",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
borderRadius: "1rem",
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
}}
>
{/* Konten dummy, bisa diganti sesuai routing */}
<>{children}</>
</TabsPanel> </TabsPanel>
))} ))}
</Tabs> </Tabs>
{children}
</Stack> </Stack>
); );
} }

View File

@@ -2,7 +2,16 @@
'use client' 'use client'
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita'; import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
TextInput,
Title,
Tooltip
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -10,9 +19,10 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function EditKategoriBerita() { function EditKategoriBerita() {
const editState = useProxy(stateDashboardBerita.kategoriBerita) const editState = useProxy(stateDashboardBerita.kategoriBerita);
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: editState.update.form.name || '', name: editState.update.form.name || '',
}); });
@@ -23,15 +33,15 @@ function EditKategoriBerita() {
if (!id) return; if (!id) return;
try { try {
const data = await editState.update.load(id); // akses langsung, bukan dari proxy const data = await editState.update.load(id);
if (data) { if (data) {
setFormData({ setFormData({
name: data.name || '', name: data.name || '',
}); });
} }
} catch (error) { } catch (error) {
console.error("Error loading kategori Berita:", error); console.error('Error loading kategori Berita:', error);
toast.error("Gagal memuat data kategori Berita"); toast.error('Gagal memuat data kategori Berita');
} }
}; };
@@ -44,6 +54,7 @@ function EditKategoriBerita() {
...editState.update.form, ...editState.update.form,
name: formData.name, name: formData.name,
}; };
await editState.update.update(); await editState.update.update();
toast.success('Kategori Berita berhasil diperbarui!'); toast.success('Kategori Berita berhasil diperbarui!');
router.push('/admin/desa/berita/kategori-berita'); router.push('/admin/desa/berita/kategori-berita');
@@ -54,23 +65,56 @@ function EditKategoriBerita() {
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Back Button + Title */}
<Button variant="subtle" onClick={() => router.back()}> <Group mb="md">
<IconArrowBack color={colors["blue-button"]} size={30} /> <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> </Button>
</Box> </Tooltip>
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}> <Title order={4} ml="sm" c="dark">
<Stack gap={"xs"}> Edit Kategori Berita
<Title order={3}>Edit Kategori Berita</Title> </Title>
</Group>
{/* Form Wrapper */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label="Nama Kategori Berita"
placeholder="Masukkan nama kategori berita"
value={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Nama Kategori Berita</Text>} required
placeholder="masukkan nama kategori Berita"
/> />
<Button onClick={handleSubmit}>Simpan</Button> <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> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

@@ -1,50 +1,87 @@
'use client' 'use client';
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita'; import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function CreateKategoriBerita() { function CreateKategoriBerita() {
const createState = useProxy(stateDashboardBerita.kategoriBerita) const createState = useProxy(stateDashboardBerita.kategoriBerita);
const router = useRouter(); const router = useRouter();
const resetForm = () => { const resetForm = () => {
createState.create.form = { createState.create.form = {
name: "", name: '',
}; };
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
await createState.create.create(); await createState.create.create();
resetForm(); resetForm();
router.push("/admin/desa/berita/kategori-berita") router.push('/admin/desa/berita/kategori-berita');
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header dengan back button */}
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <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> </Button>
</Box> </Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Kategori Berita
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> {/* Form utama */}
<Stack gap={"xs"}> <Paper
<Title order={4}>Create Kategori Berita</Title> w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Berita</Text>} label={<Text fw="bold" fz="sm">Nama Kategori Berita</Text>}
placeholder='Masukkan nama kategori Berita' placeholder="Masukkan nama kategori berita"
value={createState.create.form.name} value={createState.create.form.name || ''}
onChange={(val) => { onChange={(e) => (createState.create.form.name = e.target.value)}
createState.create.form.name = val.target.value; required
}}
/> />
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button> <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> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -1,25 +1,40 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import {
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react'; Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip
} from '@mantine/core';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import stateDashboardBerita from '../../../_state/desa/berita'; import stateDashboardBerita from '../../../_state/desa/berita';
function KategoriBerita() { function KategoriBerita() {
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Kategori Berita' title="Kategori Berita"
placeholder='pencarian' placeholder="Cari nama kategori berita..."
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -30,99 +45,155 @@ function KategoriBerita() {
} }
function ListKategoriBerita({ search }: { search: string }) { function ListKategoriBerita({ search }: { search: string }) {
const listDataState = useProxy(stateDashboardBerita.kategoriBerita) const listDataState = useProxy(stateDashboardBerita.kategoriBerita);
const router = useRouter(); const router = useRouter();
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null);
const {
data,
loading,
load,
page,
totalPages,
} = listDataState.findMany;
useEffect(() => { useEffect(() => {
listDataState.findMany.load() load(page, 10, search);
}, []) }, [page, search]);
const handleDelete = () => { const handleDelete = () => {
if (selectedId) { if (selectedId) {
listDataState.delete.delete(selectedId) listDataState.delete.delete(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
load(page, 10, search);
listDataState.findMany.load()
}
} }
};
const filteredData = (listDataState.findMany.data || []).filter(item => { const filteredData = data || [];
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword)
);
});
if (!listDataState.findMany.data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
) );
} }
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p="md"> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Stack> <Group justify="space-between" mb="md">
<JudulList <Title order={4}>Daftar Kategori Berita</Title>
title='List Kategori Berita' <Tooltip label="Tambah Kategori Berita" withArrow>
href='/admin/desa/berita/kategori-berita/create' <Button
/> leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push('/admin/desa/berita/kategori-berita/create')
}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}> <Box style={{ overflowX: 'auto' }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}> <Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>No</TableTh> <TableTh style={{ width: '10%' }}>No</TableTh>
<TableTh>Nama</TableTh> <TableTh style={{ width: '50%' }}>Nama</TableTh>
<TableTh>Edit</TableTh> <TableTh style={{ width: '20%' }}>Edit</TableTh>
<TableTh>Hapus</TableTh> <TableTh style={{ width: '20%' }}>Hapus</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item, index) => ( {filteredData.length > 0 ? (
filteredData.map((item, index) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Box w={100}> <Text fz="sm">{index + 1}</Text>
<Text truncate="end" fz={"sm"}>{index + 1}</Text>
</Box>
</TableTd>
<TableTd>{item.name}</TableTd>
<TableTd>
<Button color='green' onClick={() => router.push(`/admin/pendidikan/perpustakaan-digital/kategori-Berita/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name}
</Text>
</TableTd>
<TableTd>
<Tooltip label="Edit Kategori Berita" withArrow>
<Button <Button
color='red' variant="light"
color="green"
onClick={() =>
router.push(
`/admin/desa/berita/kategori-berita/${item.id}`
)
}
>
<IconEdit size={18} />
</Button>
</Tooltip>
</TableTd>
<TableTd>
<Tooltip label="Hapus Kategori Berita" withArrow>
<Button
variant="light"
color="red"
disabled={listDataState.delete.loading} disabled={listDataState.delete.loading}
onClick={() => { onClick={() => {
setSelectedId(item.id) setSelectedId(item.id);
setModalHapus(true) setModalHapus(true);
}}> }}
<IconTrash size={20} /> >
<IconTrash size={18} />
</Button> </Button>
</Tooltip>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} ))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">
Tidak ada data kategori berita yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
</Stack>
</Paper> </Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10, search);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
{/* Modal Konfirmasi Hapus */} {/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleDelete} onConfirm={handleDelete}
text='Apakah anda yakin ingin menghapus kategori Berita ini?' text="Apakah anda yakin ingin menghapus kategori berita ini?"
/> />
</Box> </Box>
) );
} }
export default KategoriBerita; export default KategoriBerita;

View File

@@ -15,7 +15,8 @@ import {
Stack, Stack,
Text, Text,
TextInput, TextInput,
Title Title,
Tooltip,
} from "@mantine/core"; } from "@mantine/core";
import { Dropzone } from "@mantine/dropzone"; import { Dropzone } from "@mantine/dropzone";
import { IconArrowBack, IconPhoto, IconUpload, IconX } from "@tabler/icons-react"; import { IconArrowBack, IconPhoto, IconUpload, IconX } from "@tabler/icons-react";
@@ -24,7 +25,6 @@ import { useEffect, useState } from "react";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { useProxy } from "valtio/utils"; import { useProxy } from "valtio/utils";
function EditBerita() { function EditBerita() {
const beritaState = useProxy(stateDashboardBerita); const beritaState = useProxy(stateDashboardBerita);
const router = useRouter(); const router = useRouter();
@@ -33,29 +33,29 @@ function EditBerita() {
const [previewImage, setPreviewImage] = useState<string | null>(null); const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
judul: beritaState.berita.edit.form.judul || '', judul: beritaState.berita.edit.form.judul || "",
deskripsi: beritaState.berita.edit.form.deskripsi || '', deskripsi: beritaState.berita.edit.form.deskripsi || "",
kategoriBeritaId: beritaState.berita.edit.form.kategoriBeritaId || '', kategoriBeritaId: beritaState.berita.edit.form.kategoriBeritaId || "",
content: beritaState.berita.edit.form.content || '', content: beritaState.berita.edit.form.content || "",
imageId: beritaState.berita.edit.form.imageId || '' imageId: beritaState.berita.edit.form.imageId || "",
}); });
// Load berita by id saat pertama kali // Load berita by id saat pertama kali
useEffect(() => { useEffect(() => {
beritaState.kategoriBerita.findMany.load() beritaState.kategoriBerita.findMany.load();
const loadBerita = async () => { const loadBerita = async () => {
const id = params?.id as string; const id = params?.id as string;
if (!id) return; if (!id) return;
try { try {
const data = await stateDashboardBerita.berita.edit.load(id); // akses langsung, bukan dari proxy const data = await stateDashboardBerita.berita.edit.load(id);
if (data) { if (data) {
setFormData({ setFormData({
judul: data.judul || '', judul: data.judul || "",
deskripsi: data.deskripsi || '', deskripsi: data.deskripsi || "",
kategoriBeritaId: data.kategoriBeritaId || '', kategoriBeritaId: data.kategoriBeritaId || "",
content: data.content || '', content: data.content || "",
imageId: data.imageId || '', imageId: data.imageId || "",
}); });
if (data?.image?.link) { if (data?.image?.link) {
@@ -69,31 +69,26 @@ function EditBerita() {
}; };
loadBerita(); loadBerita();
}, [params?.id]); // ✅ hapus beritaState dari dependency }, [params?.id]);
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
// Update global state with form data
beritaState.berita.edit.form = { beritaState.berita.edit.form = {
...beritaState.berita.edit.form, ...beritaState.berita.edit.form,
judul: formData.judul, ...formData,
deskripsi: formData.deskripsi,
content: formData.content,
kategoriBeritaId: formData.kategoriBeritaId || '',
imageId: formData.imageId // Keep existing imageId if not changed
}; };
// Jika ada file baru, upload
if (file) { if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name }); const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data; const uploaded = res.data?.data;
if (!uploaded?.id) { if (!uploaded?.id) {
return toast.error("Gagal upload gambar"); return toast.error("Gagal upload gambar");
} }
// Update imageId in global state
beritaState.berita.edit.form.imageId = uploaded.id; beritaState.berita.edit.form.imageId = uploaded.id;
} }
@@ -107,87 +102,111 @@ function EditBerita() {
}; };
return ( return (
<Box> <Box px={{ base: "sm", md: "lg" }} py="md">
<Box mb={10}> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()}> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<IconArrowBack color={colors["blue-button"]} size={30} /> <Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors["blue-button"]} size={24} />
</Button> </Button>
</Box> </Tooltip>
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}> <Title order={4} ml="sm" c="dark">
<Stack gap={"xs"}> Edit Berita
<Title order={3}>Edit Berita</Title> </Title>
</Group>
<Paper
w={{ base: "100%", md: "50%" }}
bg={colors["white-1"]}
p="lg"
radius="md"
shadow="sm"
style={{ border: "1px solid #e0e0e0" }}
>
<Stack gap="md">
<TextInput <TextInput
label="Judul"
placeholder="Masukkan judul"
value={formData.judul} value={formData.judul}
onChange={(e) => setFormData({ ...formData, judul: e.target.value })} onChange={(e) =>
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>} setFormData({ ...formData, judul: e.target.value })
placeholder="masukkan judul" }
required
/> />
<TextInput <TextInput
label="Deskripsi"
placeholder="Masukkan deskripsi"
value={formData.deskripsi} value={formData.deskripsi}
onChange={(e) => setFormData({ ...formData, deskripsi: e.target.value })} onChange={(e) =>
label={<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>} setFormData({ ...formData, deskripsi: e.target.value })
placeholder="masukkan deskripsi" }
required
/> />
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text> <Text fw="bold" fz="sm" mb={6}>
<Box> Gambar Berita
</Text>
<Dropzone <Dropzone
onDrop={(files) => { onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama const selectedFile = files[0];
if (selectedFile) { if (selectedFile) {
setFile(selectedFile); setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview setPreviewImage(URL.createObjectURL(selectedFile));
} }
}} }}
onReject={() => toast.error('File tidak valid.')} onReject={() => toast.error("File tidak valid, gunakan format gambar")}
maxSize={5 * 1024 ** 2} // Maks 5MB maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }} accept={{ "image/*": [] }}
radius="md"
p="xl"
> >
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}> <Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept> <Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /> <IconUpload size={48} color={colors["blue-button"]} stroke={1.5} />
</Dropzone.Accept> </Dropzone.Accept>
<Dropzone.Reject> <Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} /> <IconX size={48} color="red" stroke={1.5} />
</Dropzone.Reject> </Dropzone.Reject>
<Dropzone.Idle> <Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} /> <IconPhoto size={48} color="#868e96" stroke={1.5} />
</Dropzone.Idle> </Dropzone.Idle>
<Stack gap="xs" align="center">
<div> <Text size="md" fw={500}>
<Text size="xl" inline> Seret gambar atau klik untuk memilih file
Drag gambar ke sini atau klik untuk pilih file
</Text> </Text>
<Text size="sm" c="dimmed" inline mt={7}> <Text size="sm" c="dimmed">
Maksimal 5MB dan harus format gambar Maksimal 5MB, format gambar wajib
</Text> </Text>
</div> </Stack>
</Group> </Group>
</Dropzone> </Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && ( {previewImage && (
<Box mt="sm"> <Box mt="sm" style={{ display: "flex", justifyContent: "center" }}>
<Image <Image
src={previewImage} src={previewImage}
alt="Preview" alt="Preview Gambar"
radius="md"
style={{ style={{
maxWidth: '100%', maxHeight: 220,
maxHeight: '200px', objectFit: "contain",
objectFit: 'contain', border: `1px solid ${colors["blue-button"]}`,
borderRadius: '8px',
border: '1px solid #ddd',
}} }}
/> />
</Box> </Box>
)} )}
</Box>
</Box>
</Box>
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Konten</Text> <Text fz="sm" fw="bold">
Konten
</Text>
<EditEditor <EditEditor
value={formData.content} value={formData.content}
onChange={(htmlContent) => { onChange={(htmlContent) => {
@@ -199,13 +218,15 @@ function EditBerita() {
<Select <Select
value={formData.kategoriBeritaId} value={formData.kategoriBeritaId}
onChange={(val) => setFormData({ ...formData, kategoriBeritaId: val || "" })} onChange={(val) =>
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>} setFormData({ ...formData, kategoriBeritaId: val || "" })
placeholder='Pilih kategori' }
label="Kategori"
placeholder="Pilih kategori"
data={ data={
beritaState.kategoriBerita.findMany.data?.map((v) => ({ beritaState.kategoriBerita.findMany.data?.map((v) => ({
value: v.id, value: v.id,
label: v.name label: v.name,
})) || [] })) || []
} }
clearable clearable
@@ -214,7 +235,20 @@ function EditBerita() {
error={!formData.kategoriBeritaId ? "Pilih kategori" : undefined} error={!formData.kategoriBeritaId ? "Pilih kategori" : undefined}
/> />
<Button onClick={handleSubmit}>Edit Berita</Button> <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> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

@@ -1,9 +1,8 @@
'use client' 'use client'
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -12,104 +11,143 @@ import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirma
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita'; import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
function DetailBerita() { function DetailBerita() {
const beritaState = useProxy(stateDashboardBerita) const beritaState = useProxy(stateDashboardBerita);
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null);
const params = useParams() const params = useParams();
const router = useRouter() const router = useRouter();
useShallowEffect(() => { useShallowEffect(() => {
beritaState.berita.findUnique.load(params?.id as string) beritaState.berita.findUnique.load(params?.id as string);
}, []) }, []);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
beritaState.berita.delete.byId(selectedId) beritaState.berita.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/desa/berita/list-berita") router.push("/admin/desa/berita/list-berita");
}
} }
};
if (!beritaState.berita.findUnique.data) { if (!beritaState.berita.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={40} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) );
} }
const data = beritaState.berita.findUnique.data;
return ( return (
<Box> <Box py={10}>
<Box mb={10}> {/* Tombol Back */}
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Berita</Text>
{beritaState.berita.findUnique.data ? (
<Paper key={beritaState.berita.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Kategori</Text>
<Text fz={"lg"}>{beritaState.berita.findUnique.data?.kategoriBerita?.name}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Judul</Text>
<Text fz={"lg"}>{beritaState.berita.findUnique.data?.judul}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
<Text fz={"lg"} >{beritaState.berita.findUnique.data?.deskripsi}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
<Image w={{ base: 150, md: 150, lg: 150 }} src={beritaState.berita.findUnique.data?.image?.link} alt="gambar" />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Konten</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: beritaState.berita.findUnique.data?.content }} />
</Box>
<Flex gap={"xs"} mt={10}>
<Button <Button
onClick={() => { variant="subtle"
if (beritaState.berita.findUnique.data) { onClick={() => router.back()}
setSelectedId(beritaState.berita.findUnique.data.id); leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
setModalHapus(true); mb={15}
}
}}
disabled={beritaState.berita.delete.loading || !beritaState.berita.findUnique.data}
color={"red"}
> >
<IconX size={20} /> Kembali
</Button> </Button>
{/* Detail Berita */}
<Paper
withBorder
w={{ base: "100%", md: "70%" }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Berita
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz="lg" fw="bold">Kategori</Text>
<Text fz="md" c="dimmed">{data.kategoriBerita?.name || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Judul</Text>
<Text fz="md" c="dimmed">{data.judul || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Deskripsi</Text>
<Text fz="md" c="dimmed">{data.deskripsi || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Gambar</Text>
{data.image?.link ? (
<Image
src={data.image.link}
alt={data.judul || 'Gambar Berita'}
w={200}
h={200}
radius="md"
fit="cover"
/>
) : (
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
)}
</Box>
<Box>
<Text fz="lg" fw="bold">Konten</Text>
<Text
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{ __html: data.content || '-' }}
/>
</Box>
{/* Action Button */}
<Group gap="sm">
<Tooltip label="Hapus Berita" withArrow position="top">
<Button <Button
color="red"
onClick={() => { onClick={() => {
if (beritaState.berita.findUnique.data) { setSelectedId(data.id);
router.push(`/admin/desa/berita/list-berita/${beritaState.berita.findUnique.data.id}/edit`); setModalHapus(true);
}
}} }}
disabled={!beritaState.berita.findUnique.data} variant="light"
color={"green"} radius="md"
size="md"
>
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Berita" withArrow position="top">
<Button
color="green"
onClick={() => router.push(`/admin/desa/berita/list-berita/${data.id}/edit`)}
variant="light"
radius="md"
size="md"
> >
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Group>
</Stack> </Stack>
</Paper> </Paper>
) : null}
</Stack> </Stack>
</Paper> </Paper>
{/* Modal Konfirmasi Hapus */} {/* Modal Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus berita ini?' text="Apakah Anda yakin ingin menghapus berita ini?"
/> />
</Box> </Box>
); );

View File

@@ -3,7 +3,19 @@ import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita'; import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Image,
Paper,
Select,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
@@ -12,38 +24,33 @@ import { useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
export default function CreateBerita() { export default function CreateBerita() {
const beritaState = useProxy(stateDashboardBerita); const beritaState = useProxy(stateDashboardBerita);
const [previewImage, setPreviewImage] = useState<string | null>(null); const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const router = useRouter() const router = useRouter();
useShallowEffect(() => { useShallowEffect(() => {
beritaState.kategoriBerita.findMany.load() beritaState.kategoriBerita.findMany.load();
}, []); }, []);
const resetForm = () => { const resetForm = () => {
// Reset state di valtio
beritaState.berita.create.form = { beritaState.berita.create.form = {
judul: "", judul: '',
deskripsi: "", deskripsi: '',
kategoriBeritaId: "", kategoriBeritaId: '',
imageId: "", imageId: '',
content: "", content: '',
}; };
// Reset state lokal
setPreviewImage(null); setPreviewImage(null);
setFile(null); setFile(null);
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
if (!file) { if (!file) {
return toast.warn("Pilih file gambar terlebih dahulu"); return toast.warn('Silakan pilih file gambar terlebih dahulu');
} }
// Upload gambar dulu
const res = await ApiFetch.api.fileStorage.create.post({ const res = await ApiFetch.api.fileStorage.create.post({
file, file,
name: file.name, name: file.name,
@@ -51,40 +58,55 @@ export default function CreateBerita() {
const uploaded = res.data?.data; const uploaded = res.data?.data;
if (!uploaded?.id) { if (!uploaded?.id) {
return toast.error("Gagal upload gambar"); return toast.error('Gagal mengunggah gambar, silakan coba lagi');
} }
// Simpan ID gambar ke form
beritaState.berita.create.form.imageId = uploaded.id; beritaState.berita.create.form.imageId = uploaded.id;
// Submit data berita
await beritaState.berita.create.create(); await beritaState.berita.create.create();
// Reset form setelah submit
resetForm(); resetForm();
router.push("/admin/desa/berita/list-berita") router.push('/admin/desa/berita/list-berita');
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header dengan tombol kembali */}
<Button variant="subtle" onClick={() => router.back()}> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <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> </Button>
</Box> </Tooltip>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}> <Title order={4} ml="sm" c="dark">
<Stack gap={"xs"}> Tambah Berita
<Title order={3}>Create Berita</Title> </Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label="Judul"
placeholder="Masukkan judul berita"
value={beritaState.berita.create.form.judul} value={beritaState.berita.create.form.judul}
onChange={(val) => { onChange={(e) => (beritaState.berita.create.form.judul = e.target.value)}
beritaState.berita.create.form.judul = val.target.value; required
}}
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
placeholder="masukkan judul"
/> />
<Select <Select
label={<Text fz={"sm"} fw={"bold"}>Kategori</Text>} label="Kategori"
placeholder="Pilih kategori" placeholder="Pilih kategori"
data={beritaState.kategoriBerita.findMany.data.map((item) => ({ data={beritaState.kategoriBerita.findMany.data.map((item) => ({
label: item.name, label: item.name,
@@ -93,85 +115,83 @@ export default function CreateBerita() {
value={beritaState.berita.create.form.kategoriBeritaId || null} value={beritaState.berita.create.form.kategoriBeritaId || null}
onChange={(val: string | null) => { onChange={(val: string | null) => {
if (val) { if (val) {
const selected = beritaState.kategoriBerita.findMany.data?.find((item) => item.id === val); const selected = beritaState.kategoriBerita.findMany.data?.find(
(item) => item.id === val
);
if (selected) { if (selected) {
beritaState.berita.create.form.kategoriBeritaId = selected.id; beritaState.berita.create.form.kategoriBeritaId = selected.id;
} }
} else { } else {
beritaState.berita.create.form.kategoriBeritaId = ""; beritaState.berita.create.form.kategoriBeritaId = '';
} }
}} }}
searchable searchable
clearable clearable
nothingFoundMessage="Tidak ditemukan" nothingFoundMessage="Tidak ditemukan"
required
/> />
<TextInput <TextInput
label="Deskripsi Singkat"
placeholder="Masukkan deskripsi berita"
value={beritaState.berita.create.form.deskripsi} value={beritaState.berita.create.form.deskripsi}
onChange={(val) => { onChange={(e) => (beritaState.berita.create.form.deskripsi = e.target.value)}
beritaState.berita.create.form.deskripsi = val.target.value;
}}
label={<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>}
placeholder="masukkan deskripsi"
/> />
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text> <Text fw="bold" fz="sm" mb={6}>
<Box> Gambar Berita
</Text>
<Dropzone <Dropzone
onDrop={(files) => { onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama const selectedFile = files[0];
if (selectedFile) { if (selectedFile) {
setFile(selectedFile); setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview setPreviewImage(URL.createObjectURL(selectedFile));
} }
}} }}
onReject={() => toast.error('File tidak valid.')} onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2} // Maks 5MB maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }} accept={{ 'image/*': [] }}
radius="md"
p="xl"
> >
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}> <Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept> <Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /> <IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept> </Dropzone.Accept>
<Dropzone.Reject> <Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} /> <IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject> </Dropzone.Reject>
<Dropzone.Idle> <Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} /> <IconPhoto size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle> </Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group> </Group>
<Text ta="center" mt="sm" size="sm" color="dimmed">
Seret gambar atau klik untuk memilih file (maks 5MB)
</Text>
</Dropzone> </Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && ( {previewImage && (
<Box mt="sm"> <Box mt="sm" style={{ textAlign: 'center' }}>
<Image <Image
src={previewImage} src={previewImage}
alt="Preview" alt="Preview Gambar"
radius="md"
style={{ style={{
maxWidth: '100%', maxHeight: 200,
maxHeight: '200px',
objectFit: 'contain', objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd', border: '1px solid #ddd',
}} }}
/> />
</Box> </Box>
)} )}
</Box>
</Box>
</Box>
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Konten</Text> <Text fz="sm" fw="bold" mb={6}>
Konten
</Text>
<CreateEditor <CreateEditor
value={beritaState.berita.create.form.content} value={beritaState.berita.create.form.content}
onChange={(htmlContent) => { onChange={(htmlContent) => {
@@ -179,7 +199,21 @@ export default function CreateBerita() {
}} }}
/> />
</Box> </Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan Berita</Button>
<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> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

@@ -1,6 +1,25 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Grid, GridCol, Image, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import {
Box,
Button,
Center,
Group,
Image,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@@ -9,15 +28,13 @@ import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import stateDashboardBerita from '../../../_state/desa/berita'; import stateDashboardBerita from '../../../_state/desa/berita';
function Berita() { function Berita() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Berita' title="Berita"
placeholder='pencarian' placeholder="Cari judul atau kategori berita..."
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -28,103 +45,125 @@ function Berita() {
} }
function ListBerita({ search }: { search: string }) { function ListBerita({ search }: { search: string }) {
const beritaState = useProxy(stateDashboardBerita) const beritaState = useProxy(stateDashboardBerita);
const router = useRouter() const router = useRouter();
const {
data,
page,
totalPages,
loading,
load,
} = beritaState.berita.findMany;
const { data, page, totalPages, loading, load } = beritaState.berita.findMany;
// Fetch data when page or search changes
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search); load(page, 10, search);
}, [page, search]); }, [page, search]);
if (loading || !data) { if (loading || !data) {
return <Skeleton h={500} />; return (
<Stack py={10}>
<Skeleton height={600} radius="md" />
</Stack>
);
} }
const filteredData = data || []; const filteredData = data || [];
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors["white-1"]} p={"md"}> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Stack> <Group justify="space-between" mb="md">
<Grid> <Title order={4}>Daftar Berita</Title>
<GridCol span={{ base: 12, md: 11 }}> <Tooltip label="Tambah Berita" withArrow>
<Text fz={"xl"} fw={"bold"}>
List Berita
</Text>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button <Button
onClick={() => router.push("/admin/desa/berita/list-berita/create")} leftSection={<IconCircleDashedPlus size={18} />}
bg={colors["blue-button"]} color="blue"
variant="light"
onClick={() => router.push('/admin/desa/berita/list-berita/create')}
> >
<IconCircleDashedPlus size={25} /> Tambah Baru
</Button> </Button>
</GridCol> </Tooltip>
</Grid> </Group>
<Box style={{ overflowX: "auto" }}>
<Table <Box style={{ overflowX: 'auto' }}>
striped <Table highlightOnHover>
withRowBorders
withTableBorder
style={{ minWidth: "700px" }}
>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh w={250}>Judul</TableTh> <TableTh style={{ width: '30%' }}>Judul</TableTh>
<TableTh w={250}>Kategori</TableTh> <TableTh style={{ width: '20%' }}>Kategori</TableTh>
<TableTh w={250}>Image</TableTh> <TableTh style={{ width: '25%' }}>Gambar</TableTh>
<TableTh w={200}>Detail</TableTh> <TableTh style={{ width: '15%' }}>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item) => ( {filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd style={{ width: '30%' }}>
<Box w={100}> <Text fw={500} truncate="end" lineClamp={1}>
<Text truncate="end" fz={"sm"}>
{item.judul} {item.judul}
</Text> </Text>
</TableTd>
<TableTd style={{ width: '20%' }}>
<Text fz="sm" c="dimmed">
{item.kategoriBerita?.name || '-'}
</Text>
</TableTd>
<TableTd style={{ width: '25%' }}>
<Box
w={80}
h={80}
style={{ borderRadius: 8, overflow: 'hidden' }}
>
{item.image?.link ? (
<Image src={item.image.link} alt="gambar" fit="cover" />
) : (
<Box bg={colors['blue-button']} w="100%" h="100%" />
)}
</Box> </Box>
</TableTd> </TableTd>
<TableTd>{item.kategoriBerita?.name}</TableTd> <TableTd style={{ width: '15%' }}>
<TableTd>
<Image w={100} src={item.image?.link} alt="gambar" />
</TableTd>
<TableTd>
<Button <Button
bg={"green"} variant="light"
color="blue"
onClick={() => onClick={() =>
router.push(`/admin/desa/berita/list-berita/${item.id}`) router.push(`/admin/desa/berita/list-berita/${item.id}`)
} }
> >
<IconDeviceImacCog size={25} /> <IconDeviceImacCog size={20} />
<Text ml={5}>Detail</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} ))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">
Tidak ada data berita yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
</Stack>
</Paper> </Paper>
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => load(newPage)} // ini penting! onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages} total={totalPages}
mt="md" mt="md"
mb="md" mb="md"
color="blue"
radius="md"
/> />
</Center> </Center>
</Box> </Box>
); );
} }
export default Berita; export default Berita;

View File

@@ -1,161 +0,0 @@
'use client'
/* eslint-disable react-hooks/exhaustive-deps */
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconImageInPicture, IconPhoto, IconUpload, IconX } 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';
function EditFoto() {
const fotoState = useProxy(stateGallery.foto)
const router = useRouter();
const params = useParams();
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
name: fotoState.update.form.name || '',
deskripsi: fotoState.update.form.deskripsi || '',
imagesId: fotoState.update.form.imagesId || ''
});
useEffect(() => {
const loadFoto = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await fotoState.update.load(id);
if (data) {
setFormData({
name: data.name || '',
deskripsi: data.deskripsi || '',
imagesId: data.imageGalleryFoto?.id || ''
});
if (data?.imageGalleryFoto?.link) {
setPreviewImage(data.imageGalleryFoto.link);
}
}
} catch (error) {
console.error('Error loading foto:', error);
toast.error('Gagal memuat data foto');
}
};
loadFoto();
}, [params?.id]);
const handleSubmit = async () => {
try {
fotoState.update.form = {
...fotoState.update.form,
name: formData.name,
deskripsi: formData.deskripsi,
imagesId: formData.imagesId
};
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
fotoState.update.form.imagesId = uploaded.id;
}
await fotoState.update.update();
toast.success('Foto berhasil diperbarui!');
router.push('/admin/desa/gallery/foto');
} catch (error) {
console.error('Error updating foto:', error);
toast.error('Terjadi kesalahan saat memperbarui foto');
}
};
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Foto</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Judul Foto</Text>}
placeholder='Masukkan judul foto'
value={formData.name}
onChange={(e) =>
(formData.name = e.target.value)
}
/>
<Box>
<Text>Upload Foto</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
</Center>
)}
</Box>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Foto</Text>
<EditEditor
value={fotoState.update.form.deskripsi}
onChange={(val) => {
fotoState.update.form.deskripsi = val;
}}
/>
</Box>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditFoto;

View File

@@ -1,112 +0,0 @@
'use client'
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
import React from 'react';
import { useProxy } from 'valtio/utils';
import { useState } from 'react';
import { useParams, useRouter } from 'next/navigation';
import { useShallowEffect } from '@mantine/hooks';
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import colors from '@/con/colors';
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
function DetailFoto() {
const fotoState = useProxy(stateGallery.foto)
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
useShallowEffect(() => {
fotoState.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
fotoState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/desa/gallery/foto")
}
}
if (!fotoState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Foto</Text>
{fotoState.findUnique.data ? (
<Paper key={fotoState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Judul</Text>
<Text fz={"lg"}>{fotoState.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Tanggal Foto</Text>
<Text fz={"lg"}>{new Date(fotoState.findUnique.data?.createdAt).toDateString()}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: fotoState.findUnique.data?.deskripsi }} />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
<Image w={{ base: 300, md: 350}} src={fotoState.findUnique.data?.imageGalleryFoto?.link} alt="gambar" />
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (fotoState.findUnique.data) {
setSelectedId(fotoState.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={fotoState.delete.loading || !fotoState.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (fotoState.findUnique.data) {
router.push(`/admin/desa/gallery/foto/${fotoState.findUnique.data.id}/edit`);
}
}}
disabled={!fotoState.findUnique.data}
color={"green"}
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus berita ini?'
/>
</Box>
);
}
export default DetailFoto;

View File

@@ -1,147 +0,0 @@
'use client'
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function CreateFoto() {
const fotoState = useProxy(stateGallery.foto)
const router = useRouter();
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const resetForm = () => {
fotoState.create.form = {
name: "",
deskripsi: "",
imagesId: "",
};
setPreviewImage(null)
setFile(null)
};
const handleSubmit = async () => {
if (!file) {
return toast.warn("Pilih file gambar terlebih dahulu");
}
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
fotoState.create.form.imagesId = uploaded.id;
await fotoState.create.create();
resetForm();
router.push("/admin/desa/gallery/foto")
};
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Foto</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Judul Foto</Text>}
placeholder='Masukkan judul foto'
value={fotoState.create.form.name}
onChange={(val) => {
fotoState.create.form.name = val.target.value;
}}
/>
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Foto</Text>
<CreateEditor
value={fotoState.create.form.deskripsi}
onChange={(val) => {
fotoState.create.form.deskripsi = val;
}}
/>
</Box>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateFoto;

View File

@@ -1,9 +1,9 @@
"use client"; "use client";
import colors from "@/con/colors";
import stateFileStorage from "@/state/state-list-image"; import stateFileStorage from "@/state/state-list-image";
import { import {
ActionIcon, ActionIcon,
Box, Box,
Card,
Flex, Flex,
Group, Group,
Image, Image,
@@ -13,7 +13,8 @@ import {
Stack, Stack,
Text, Text,
TextInput, TextInput,
Title Title,
Tooltip,
} from "@mantine/core"; } from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks"; import { useShallowEffect } from "@mantine/hooks";
import { IconSearch, IconTrash, IconX } from "@tabler/icons-react"; import { IconSearch, IconTrash, IconX } from "@tabler/icons-react";
@@ -29,95 +30,128 @@ export default function ListImage() {
}, []); }, []);
let timeOut: NodeJS.Timer; let timeOut: NodeJS.Timer;
return ( return (
<Stack p={"lg"}> <Stack p="lg" gap="lg">
<Flex justify="space-between"> <Flex justify="space-between" align="center" wrap="wrap" gap="md">
<Title order={3}>List Foto</Title> <Title order={2} fw={700}>
Galeri Foto
</Title>
<TextInput <TextInput
radius={"lg"} radius="xl"
leftSection={<IconSearch />} size="md"
placeholder="Cari foto berdasarkan nama..."
leftSection={<IconSearch size={18} />}
rightSection={ rightSection={
<ActionIcon <ActionIcon
variant="transparent" variant="light"
onClick={() => { color="gray"
stateFileStorage.load(); radius="xl"
}} onClick={() => stateFileStorage.load()}
> >
<IconX /> <IconX size={18} />
</ActionIcon> </ActionIcon>
} }
placeholder="Pencarian"
onChange={(e) => { onChange={(e) => {
if (timeOut) clearTimeout(timeOut); if (timeOut) clearTimeout(timeOut);
timeOut = setTimeout(() => { timeOut = setTimeout(() => {
stateFileStorage.load({ search: e.target.value }); stateFileStorage.load({ search: e.target.value });
}, 200); }, 300);
}} }}
/> />
</Flex> </Flex>
<Paper bg={colors['white-1']} p={'md'}>
<Paper withBorder radius="lg" p="md" shadow="sm">
{list && list.length > 0 ? (
<SimpleGrid <SimpleGrid
cols={{ cols={{ base: 2, sm: 3, md: 5, lg: 8 }}
base: 3, spacing="md"
md: 5, verticalSpacing="md"
lg: 10,
}}
> >
{list && {list.map((v, k) => (
list.map((v, k) => { <Card
return ( key={k}
<Paper key={k} shadow="sm"> withBorder
<Stack pos={"relative"} gap={0} justify="space-between"> radius="md"
shadow="sm"
className="hover:shadow-md transition-all duration-200"
>
<Stack gap="xs">
<motion.div <motion.div
onClick={() => { onClick={() => {
// copy to clipboard
navigator.clipboard.writeText(v.url); navigator.clipboard.writeText(v.url);
toast("Berhasil disalin"); toast("Tautan foto berhasil disalin");
}} }}
whileHover={{ scale: 1.05 }} whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.8 }} whileTap={{ scale: 0.95 }}
style={{ cursor: "pointer" }}
> >
<Image <Image
h={100} src={`${v.url}?size=200`}
src={v.url + "?size=100"}
alt={v.name} alt={v.name}
radius="md"
h={120}
fit="cover" fit="cover"
loading="lazy" loading="lazy"
style={{
objectFit: "cover",
objectPosition: "center",
}}
/> />
</motion.div> </motion.div>
<Box p={"md"} h={54}>
<Text lineClamp={2} fz={"xs"}> <Box>
<Text size="sm" fw={500} lineClamp={2}>
{v.name} {v.name}
</Text> </Text>
</Box> </Box>
<Group justify="end">
<IconTrash <Group justify="space-between" align="center" pt="xs">
<Tooltip label="Hapus foto" withArrow>
<ActionIcon
variant="subtle"
color="red" color="red"
radius="md"
onClick={() => { onClick={() => {
stateFileStorage.del({ name: v.name }).finally(() => { stateFileStorage
toast("Berhasil dihapus"); .del({ name: v.name })
}); .finally(() => toast("Foto berhasil dihapus"));
}} }}
/> >
<IconTrash size={18} />
</ActionIcon>
</Tooltip>
</Group> </Group>
</Stack> </Stack>
</Paper> </Card>
); ))}
})}
</SimpleGrid> </SimpleGrid>
) : (
<Stack align="center" justify="center" py="xl" gap="sm">
<Image
src="https://cdn-icons-png.flaticon.com/512/4076/4076549.png"
alt="Kosong"
w={120}
h={120}
fit="contain"
opacity={0.7}
/>
<Text c="dimmed" ta="center">
Belum ada foto yang tersedia
</Text>
</Stack>
)}
</Paper> </Paper>
{total && (
{total && total > 1 && (
<Flex justify="center">
<Pagination <Pagination
total={total} total={total}
onChange={(e) => { size="md"
stateFileStorage.page = e; radius="md"
withEdges
onChange={(page) => {
stateFileStorage.page = page;
stateFileStorage.load(); stateFileStorage.load();
}} }}
/> />
</Flex>
)} )}
</Stack> </Stack>
); );

View File

@@ -1,9 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core'; import { 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 { IconPhoto, IconVideo } from '@tabler/icons-react';
function LayoutTabsGallery({ children }: { children: React.ReactNode }) { function LayoutTabsGallery({ children }: { children: React.ReactNode }) {
const router = useRouter() const router = useRouter()
@@ -12,16 +13,21 @@ function LayoutTabsGallery({ children }: { children: React.ReactNode }) {
{ {
label: "Foto", label: "Foto",
value: "foto", value: "foto",
href: "/admin/desa/gallery/foto" href: "/admin/desa/gallery/foto",
icon: <IconPhoto size={18} stroke={1.8} />,
tooltip: "Kelola foto-foto galeri desa"
}, },
{ {
label: "Video", label: "Video",
value: "video", value: "video",
href: "/admin/desa/gallery/video" href: "/admin/desa/gallery/video",
icon: <IconVideo size={18} stroke={1.8} />,
tooltip: "Kelola video galeri desa"
}, },
]; ];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value); const currentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => { const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value) const tab = tabs.find(t => t.value === value)
@@ -39,22 +45,62 @@ function LayoutTabsGallery({ children }: { children: React.ReactNode }) {
}, [pathname]) }, [pathname])
return ( return (
<Stack> <Stack gap="lg">
<Title order={3}>Gallery</Title> <Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Gallery</Title>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}> <Tabs
<TabsList p={"xs"} bg={"#BBC8E7FF"}> color={colors['blue-button']}
{tabs.map((e, i) => ( variant='pills'
<TabsTab key={i} value={e.value}>{e.label}</TabsTab> value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
}}
>
{tabs.map((tab, i) => (
<Tooltip
key={i}
label={tab.tooltip}
position="bottom"
withArrow
transitionProps={{ transition: 'pop', duration: 200 }}
>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))} ))}
</TabsList> </TabsList>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value}> {tabs.map((tab, i) => (
{/* Konten dummy, bisa diganti tergantung routing */} <TabsPanel
<></> key={i}
value={tab.value}
style={{
padding: "1.5rem",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
borderRadius: "1rem",
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
}}
>
<>{children}</>
</TabsPanel> </TabsPanel>
))} ))}
</Tabs> </Tabs>
{children}
</Stack> </Stack>
); );
} }

View File

@@ -3,7 +3,16 @@
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery'; import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
TextInput,
Title,
Tooltip
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -13,8 +22,8 @@ import { convertYoutubeUrlToEmbed } from '../../../lib/youtube-utils';
function EditVideo() { function EditVideo() {
const router = useRouter(); const router = useRouter();
const videoState = useProxy(stateGallery.video) const videoState = useProxy(stateGallery.video);
const params = useParams() const params = useParams();
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: '', name: '',
@@ -66,27 +75,36 @@ function EditVideo() {
console.error('Error updating video:', error); console.error('Error updating video:', error);
toast.error('Terjadi kesalahan saat memperbarui video'); toast.error('Terjadi kesalahan saat memperbarui video');
} }
} };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> <Group mb="md">
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<IconArrowBack color={colors['blue-button']} size={25} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
</Box> </Tooltip>
<Title order={4} ml="sm" c="dark">
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> Edit Video
<Stack gap={"xs"}> </Title>
<Title order={4}>Edit Video</Title> </Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label={<Text fw={"bold"} fz={"sm"}>Judul Video</Text>} label="Judul Video"
placeholder='Masukkan judul video' placeholder="Masukkan judul video"
value={formData.name} value={formData.name}
onChange={(val) => { onChange={(e) => setFormData({ ...formData, name: e.target.value })}
setFormData({ ...formData, name: val.target.value }); required
}}
/> />
<Box> <Box>
@@ -94,36 +112,46 @@ function EditVideo() {
label="Link Video YouTube" label="Link Video YouTube"
placeholder="https://www.youtube.com/watch?v=abc123" placeholder="https://www.youtube.com/watch?v=abc123"
value={formData.linkVideo} value={formData.linkVideo}
onChange={(e) => { onChange={(e) => setFormData({ ...formData, linkVideo: e.currentTarget.value })}
setFormData({ ...formData, linkVideo: e.currentTarget.value });
}}
required required
/> />
{embedLink && ( {embedLink && (
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
<iframe <iframe
className="rounded" className="rounded"
width="100%" width="100%"
height="200" height="220"
src={embedLink} src={embedLink}
title="Preview Video" title="Preview Video"
allowFullScreen allowFullScreen
></iframe> ></iframe>
</Box>
)} )}
</Box> </Box>
<Box> <Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Video</Text> <Title order={6} fw="bold" fz="sm" mb={6}>
Deskripsi Video
</Title>
<EditEditor <EditEditor
value={formData.deskripsi} value={formData.deskripsi}
onChange={(val) => { onChange={(val) => setFormData({ ...formData, deskripsi: val })}
setFormData({ ...formData, deskripsi: val });
}}
/> />
</Box> </Box>
<Group> <Group justify="right">
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button> <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> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -2,107 +2,145 @@
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery'; import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function DetailVideo() { function DetailVideo() {
const videoState = useProxy(stateGallery.video) const videoState = useProxy(stateGallery.video);
const [modalHapus, setModalHapus] = useState(false); const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null);
const params = useParams() const params = useParams();
const router = useRouter() const router = useRouter();
useShallowEffect(() => { useShallowEffect(() => {
videoState.findUnique.load(params?.id as string) videoState.findUnique.load(params?.id as string);
}, []) }, []);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
videoState.delete.byId(selectedId) videoState.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/desa/gallery/video") router.push("/admin/desa/gallery/video");
}
} }
};
if (!videoState.findUnique.data) { if (!videoState.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) );
} }
const data = videoState.findUnique.data;
return ( return (
<Box> <Box py={10}>
<Box mb={10}> {/* Tombol Kembali */}
<Button variant="subtle" onClick={() => router.back()}> <Button
<IconArrowBack color={colors['blue-button']} size={25} /> variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15}
>
Kembali
</Button> </Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}> {/* Detail Video */}
<Stack> <Paper
<Text fz={"xl"} fw={"bold"}>Detail Video</Text> withBorder
{videoState.findUnique.data ? ( w={{ base: "100%", md: "50%" }}
<Paper key={videoState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}> bg={colors['white-1']}
<Stack gap={"xs"}> p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Video
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box> <Box>
<Text fw={"bold"} fz={"lg"}>Judul</Text> <Text fz="lg" fw="bold">Judul</Text>
<Text fz={"lg"}>{videoState.findUnique.data?.name}</Text> <Text fz="md" c="dimmed">{data?.name || '-'}</Text>
</Box> </Box>
<Box> <Box>
<Text fw={"bold"} fz={"lg"}>Video</Text> <Text fz="lg" fw="bold">Video</Text>
<Box component="iframe" {data?.linkVideo ? (
src={convertToEmbedUrl(videoState.findUnique.data?.linkVideo)} <Box
component="iframe"
src={convertToEmbedUrl(data.linkVideo)}
width="100%" width="100%"
height={300} height={300}
allowFullScreen allowFullScreen
style={{ borderRadius: 8 }} style={{ borderRadius: 8 }}
/> />
) : (
<Text fz="sm" c="dimmed">Tidak ada video</Text>
)}
</Box> </Box>
<Box> <Box>
<Text fw={"bold"} fz={"lg"}>Tanggal Video</Text> <Text fz="lg" fw="bold">Tanggal Video</Text>
<Text fz={"lg"}>{new Date(videoState.findUnique.data?.createdAt).toDateString()}</Text> <Text fz="md" c="dimmed">
{data?.createdAt ? new Date(data.createdAt).toDateString() : '-'}
</Text>
</Box> </Box>
<Box> <Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text> <Text fz="lg" fw="bold">Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: videoState.findUnique.data?.deskripsi }} /> {data?.deskripsi ? (
<Text
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
/>
) : (
<Text fz="sm" c="dimmed">Tidak ada deskripsi</Text>
)}
</Box> </Box>
<Flex gap={"xs"} mt={10}>
{/* Tombol Aksi */}
<Group gap="sm">
<Tooltip label="Hapus Video" withArrow position="top">
<Button <Button
color="red"
onClick={() => { onClick={() => {
if (videoState.findUnique.data) { setSelectedId(data.id);
setSelectedId(videoState.findUnique.data.id);
setModalHapus(true); setModalHapus(true);
}
}} }}
disabled={videoState.delete.loading || !videoState.findUnique.data} variant="light"
color={"red"} radius="md"
size="md"
> >
<IconX size={20} /> <IconTrash size={20} />
</Button> </Button>
</Tooltip>
<Tooltip label="Edit Video" withArrow position="top">
<Button <Button
onClick={() => { color="green"
if (videoState.findUnique.data) { onClick={() =>
router.push(`/admin/desa/gallery/video/${videoState.findUnique.data.id}/edit`); router.push(`/admin/desa/gallery/video/${data.id}/edit`)
} }
}} variant="light"
disabled={!videoState.findUnique.data} radius="md"
color={"green"} size="md"
> >
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Group>
</Stack> </Stack>
</Paper> </Paper>
) : null}
</Stack> </Stack>
</Paper> </Paper>
@@ -111,17 +149,16 @@ function DetailVideo() {
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus berita ini?' text="Apakah Anda yakin ingin menghapus video ini?"
/> />
</Box> </Box>
); );
function convertToEmbedUrl(youtubeUrl: string): string { function convertToEmbedUrl(youtubeUrl: string): string {
try { try {
const url = new URL(youtubeUrl); const url = new URL(youtubeUrl);
const videoId = url.searchParams.get("v"); const videoId = url.searchParams.get("v");
if (!videoId) return youtubeUrl; if (!videoId) return youtubeUrl;
return `https://www.youtube.com/embed/${videoId}`; return `https://www.youtube.com/embed/${videoId}`;
} catch (err) { } catch (err) {
console.error("Error converting YouTube URL to embed:", err); console.error("Error converting YouTube URL to embed:", err);

View File

@@ -1,8 +1,18 @@
'use client' 'use client';
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery'; import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -10,77 +20,104 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { convertYoutubeUrlToEmbed } from '../../lib/youtube-utils'; import { convertYoutubeUrlToEmbed } from '../../lib/youtube-utils';
function CreateVideo() { function CreateVideo() {
const videoState = useProxy(stateGallery.video) const videoState = useProxy(stateGallery.video);
const router = useRouter(); const router = useRouter();
const [link, setLink] = useState(""); const [link, setLink] = useState('');
const embedLink = convertYoutubeUrlToEmbed(link); const embedLink = convertYoutubeUrlToEmbed(link);
const resetForm = () => { const resetForm = () => {
videoState.create.form = { videoState.create.form = {
name: "", name: '',
deskripsi: "", deskripsi: '',
linkVideo: "", linkVideo: '',
}; };
setLink('');
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
if (!embedLink) { if (!embedLink) {
toast.error("Link YouTube tidak valid. Pastikan formatnya benar."); toast.error('Link YouTube tidak valid. Pastikan formatnya benar.');
return; return;
} }
videoState.create.form.linkVideo = embedLink; // pastikan diset di sini juga (jaga-jaga) videoState.create.form.linkVideo = embedLink;
await videoState.create.create(); await videoState.create.create();
resetForm(); resetForm();
router.push("/admin/desa/gallery/video"); router.push('/admin/desa/gallery/video');
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header Back Button + Title */}
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <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> </Button>
</Box> </Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Video
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> {/* Card Form */}
<Stack gap={"xs"}> <Paper
<Title order={4}>Create Video</Title> w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Judul */}
<TextInput <TextInput
label={<Text fw={"bold"} fz={"sm"}>Judul Video</Text>} label="Judul Video"
placeholder='Masukkan judul video' placeholder="Masukkan judul video"
value={videoState.create.form.name} value={videoState.create.form.name}
onChange={(val) => {
videoState.create.form.name = val.target.value;
}}
/>
<Box>
<Stack gap={"xs"}>
<TextInput
label="Link Video YouTube"
placeholder="https://www.youtube.com/watch?v=abc123"
value={link}
onChange={(e) => { onChange={(e) => {
setLink(e.currentTarget.value); videoState.create.form.name = e.currentTarget.value;
}} }}
required required
/> />
{/* Link YouTube */}
<TextInput
label="Link Video YouTube"
placeholder="https://www.youtube.com/watch?v=abc123"
value={link}
onChange={(e) => setLink(e.currentTarget.value)}
required
/>
{/* Preview Video */}
{embedLink && ( {embedLink && (
<Box mt="sm">
<iframe <iframe
style={{ borderRadius: 10, width: "100%", height: 400 }} style={{
borderRadius: 10,
width: '100%',
height: 400,
border: '1px solid #ddd',
}}
src={embedLink} src={embedLink}
title="Preview Video" title="Preview Video"
allowFullScreen allowFullScreen
></iframe> ></iframe>
)}
</Stack>
</Box> </Box>
)}
{/* Deskripsi */}
<Box> <Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Video</Text> <Text fw="bold" fz="sm" mb={6}>
Deskripsi Video
</Text>
<CreateEditor <CreateEditor
value={videoState.create.form.deskripsi} value={videoState.create.form.deskripsi}
onChange={(val) => { onChange={(val) => {
@@ -88,8 +125,21 @@ function CreateVideo() {
}} }}
/> />
</Box> </Box>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button> {/* Button Submit */}
<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> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -1,13 +1,30 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import {
Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import stateGallery from '../../../_state/desa/gallery'; import stateGallery from '../../../_state/desa/gallery';
function Video() { function Video() {
@@ -15,8 +32,8 @@ function Video() {
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Posisi Organisasi' title='Video'
placeholder='pencarian' placeholder='Cari judul atau deskripsi video...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -29,6 +46,7 @@ function Video() {
function ListVideo({ search }: { search: string }) { function ListVideo({ search }: { search: string }) {
const videoState = useProxy(stateGallery.video) const videoState = useProxy(stateGallery.video)
const router = useRouter(); const router = useRouter();
const { const {
data, data,
page, page,
@@ -41,72 +59,104 @@ function ListVideo({ search }: { search: string }) {
load(page, 10, search) load(page, 10, search)
}, [page, search]) }, [page, search])
const filteredData = (data || []) const filteredData = data || []
if (loading || !data) { if (loading || !data) {
return ( return (
<Box py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={600} radius="md" />
</Box> </Stack>
) )
} }
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<JudulList <Group justify="space-between" mb="md">
title='List Video' <Title order={4}>Daftar Video</Title>
href='/admin/desa/gallery/video/create' <Tooltip label="Tambah Video Baru" withArrow>
/> <Button
<Table striped withTableBorder withRowBorders> leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/desa/gallery/video/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Judul Video</TableTh> <TableTh style={{ width: '25%' }}>Judul Video</TableTh>
<TableTh>Tanggal Video</TableTh> <TableTh style={{ width: '20%' }}>Tanggal</TableTh>
<TableTh>Deskripsi Video</TableTh> <TableTh style={{ width: '30%' }}>Deskripsi</TableTh>
<TableTh>Detail</TableTh> <TableTh style={{ width: '15%' }}>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item) => ( {filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd style={{ width: '25%' }}>
<Box w={200}> <Box w={200}>
<Text lineClamp={1}>{item.name}</Text> <Text fw={500} truncate="end" lineClamp={1}>{item.name}</Text>
</Box> </Box>
</TableTd> </TableTd>
<TableTd style={{ width: '20%' }}>
<TableTd>
<Box w={200}> <Box w={200}>
<Text fz="sm" c="dimmed">
{new Date(item.createdAt).toLocaleDateString('id-ID', { {new Date(item.createdAt).toLocaleDateString('id-ID', {
day: 'numeric', day: 'numeric',
month: 'long', month: 'long',
year: 'numeric', year: 'numeric',
})} })}
</Text>
</Box> </Box>
</TableTd> </TableTd>
<TableTd> <TableTd style={{ width: '30%' }}>
<Box w={200}> <Box w={200}>
<Text lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} /> <Text fz="sm" truncate="end" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</Box> </Box>
</TableTd> </TableTd>
<TableTd> <TableTd style={{ width: '15%' }}>
<Button onClick={() => router.push(`/admin/desa/gallery/video/${item.id}`)}> <Button
variant="light"
color="blue"
onClick={() => router.push(`/admin/desa/gallery/video/${item.id}`)}
>
<IconDeviceImac size={20} /> <IconDeviceImac size={20} />
<Text ml={5}>Detail</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} ))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text c="dimmed">Tidak ada video yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody> </TableTbody>
</Table> </Table>
</Box>
</Paper> </Paper>
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => load(newPage)} // ini penting! onChange={(newPage) => {
load(newPage, 10)
window.scrollTo({ top: 0, behavior: 'smooth' })
}}
total={totalPages} total={totalPages}
mt="md" mt="md"
mb="md" mb="md"
color="blue"
radius="md"
/> />
</Center> </Center>
</Box> </Box>

View File

@@ -3,7 +3,7 @@
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa'; import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -49,52 +49,74 @@ function EditPelayananPendudukNonPermanent() {
} }
return ( return (
<Box> <Box>
<Stack gap={'xs'}> <Stack gap="xs">
<Box> <Group mb="md">
<Button <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
variant={'subtle'} <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
onClick={() => router.back()} <IconArrowBack color={colors['blue-button']} size={24} />
>
<IconArrowBack color={colors['blue-button']} size={20} />
</Button> </Button>
</Box> </Tooltip>
<Box> <Title order={4} ml="sm" c="dark">
<Paper bg={colors['white-1']} p={'md'} radius={10} w={{ base: '100%', md: '50%' }}> Edit Pelayanan Penduduk Non Permanent
<Stack gap={'xs'}> </Title>
</Group>
<Paper
w={{ base: "100%", md: "50%" }}
bg={colors['white-1']}
p="md"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="xs">
<Title order={3}>Edit Pelayanan Penduduk Non Permanent</Title> <Title order={3}>Edit Pelayanan Penduduk Non Permanent</Title>
<Text fw={"bold"}>Judul</Text>
{/* Nama Field */}
<TextInput <TextInput
label="Judul"
placeholder="Masukkan judul"
value={formData.name} value={formData.name}
onChange={(val) => { onChange={(e) =>
setFormData({ setFormData({ ...formData, name: e.target.value })
...formData, }
name: val.target.value, required
})
}}
/>
<Text fw={"bold"}>Deskripsi</Text>
<EditEditor
value={formData.deskripsi}
onChange={(val) => {
setFormData({
...formData,
deskripsi: val,
})
}}
/> />
{/* Posisi Field */}
<Box>
<Text fz="sm" fw="bold">
Deskripsi
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }));
}}
/>
</Box>
{/* Submit Button */}
<Group> <Group>
<Button <Button
bg={colors['blue-button']} bg={colors['blue-button']}
onClick={handleSubmit} onClick={handleSubmit}
loading={statePendudukNonPermanent.update.loading} loading={statePendudukNonPermanent.update.loading}
disabled={!formData.name}
> >
Submit {statePendudukNonPermanent.update.loading ? 'Menyimpan...' : 'Simpan Perubahan'}
</Button>
<Button
variant="outline"
onClick={() => router.back()}
disabled={statePendudukNonPermanent.update.loading}
>
Batal
</Button> </Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>
</Box>
</Stack> </Stack>
</Box> </Box>
); );

View File

@@ -1,51 +1,103 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Grid, GridCol, Paper, Skeleton, Stack, Text } from '@mantine/core'; import {
Box,
Button,
Center,
Divider,
Grid,
GridCol,
Paper,
Skeleton,
Stack,
Text,
Title,
Tooltip,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconEdit } from '@tabler/icons-react'; import { IconEdit } 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 stateLayananDesa from '../../../_state/desa/layananDesa'; import stateLayananDesa from '../../../_state/desa/layananDesa';
function SuratKeterangan() { function PelayananPendudukNonPermanent() {
const router = useRouter() const router = useRouter();
const pelayananPendudukNonPermanen = useProxy(stateLayananDesa.pelayananPendudukNonPermanen) const pelayananPendudukNonPermanen = useProxy(
stateLayananDesa.pelayananPendudukNonPermanen
);
useShallowEffect(() => { useShallowEffect(() => {
pelayananPendudukNonPermanen.findById.load('1') pelayananPendudukNonPermanen.findById.load('1');
}, []) }, []);
if (!pelayananPendudukNonPermanen.findById.data) { if (!pelayananPendudukNonPermanen.findById.data) {
return ( return (
<Stack> <Stack align="center" justify="center" py="xl">
<Skeleton radius={10} h={800} /> <Skeleton radius="md" height={800} />
</Stack> </Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Paper bg={colors['BG-trans']} p={'md'}>
<Box py={15}>
<Stack gap={"xs"}>
<Grid>
<GridCol span={{ base: 12, md: 11 }}>
<Text fz={"h4"} fw={"bold"}>Preview Pelayanan Perizinan Berusaha</Text>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button bg={colors['blue-button']} onClick={() => router.push('/admin/desa/layanan/pelayanan_penduduk_non_permanent/edit')}>
<IconEdit size={16} />
</Button>
</GridCol>
</Grid>
</Stack>
</Box>
<Text fz={{ base: "h4", md: 'h2' }} fw={"bold"}>{pelayananPendudukNonPermanen.findById.data.name}</Text>
<Text py={10} ta={"justify"} fz={{ base: "sm", md: 'h3' }} dangerouslySetInnerHTML={{ __html: pelayananPendudukNonPermanen.findById.data.deskripsi }} />
</Paper>
</Paper>
</Box>
); );
} }
export default SuratKeterangan; const data = pelayananPendudukNonPermanen.findById.data;
return (
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
<Stack gap="md">
{/* Header */}
<Grid align="center">
<GridCol span={{ base: 12, md: 11 }}>
<Title order={3} c={colors['blue-button']}>
Preview Pelayanan Penduduk Non Permanen
</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Tooltip label="Edit Data Pelayanan" withArrow>
<Button
c="green"
variant="light"
leftSection={<IconEdit size={18} stroke={2} />}
radius="md"
onClick={() =>
router.push(
'/admin/desa/layanan/pelayanan_penduduk_non_permanent/edit'
)
}
>
Edit
</Button>
</Tooltip>
</GridCol>
</Grid>
{/* Content */}
<Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">
<Box px={{ base: 0, md: 50 }} pb="xl">
<Center>
<Text
ta="center"
fz={{ base: '1.2rem', md: '1.8rem' }}
fw="bold"
c={colors['blue-button']}
>
{data.name}
</Text>
</Center>
<Divider my="md" color={colors['blue-button']} />
<Box mt="lg">
<Text
py={10}
ta="justify"
fz={{ base: '1rem', md: '1.2rem' }}
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
/>
</Box>
</Box>
</Paper>
</Stack>
</Paper>
);
}
export default PelayananPendudukNonPermanent;

View File

@@ -3,7 +3,7 @@
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa'; import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Paper, Stack, TextInput, Title, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -14,6 +14,7 @@ function EditPelayananPerizinanBerusaha() {
const router = useRouter(); const router = useRouter();
const params = useParams() const params = useParams()
const statePerizinanBerusaha = useProxy(stateLayananDesa.pelayananPerizinanBerusaha) const statePerizinanBerusaha = useProxy(stateLayananDesa.pelayananPerizinanBerusaha)
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: statePerizinanBerusaha.findById.data?.name || '', name: statePerizinanBerusaha.findById.data?.name || '',
deskripsi: statePerizinanBerusaha.findById.data?.deskripsi || '', deskripsi: statePerizinanBerusaha.findById.data?.deskripsi || '',
@@ -50,64 +51,81 @@ function EditPelayananPerizinanBerusaha() {
} }
router.push('/admin/desa/layanan/pelayanan_perizinan_berusaha') router.push('/admin/desa/layanan/pelayanan_perizinan_berusaha')
} }
return ( return (
<Box> <Box>
<Stack gap={'xs'}> <Stack gap="xs">
<Box> {/* Header Section */}
<Button <Group mb="md">
variant={'subtle'} <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
onClick={() => router.back()} <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
> <IconArrowBack color={colors['blue-button']} size={24} />
<IconArrowBack color={colors['blue-button']} size={20} />
</Button> </Button>
</Box> </Tooltip>
<Box> <Title order={4} ml="sm" c="dark">
<Paper bg={colors['white-1']} p={'md'} radius={10} w={{ base: '100%', md: '50%' }}> Edit Pelayanan Perizinan Berusaha
<Stack gap={'xs'}> </Title>
</Group>
{/* Form Section */}
<Paper
w={{ base: "100%", md: "50%" }}
bg={colors['white-1']}
p="md"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="xs">
<Title order={3}>Edit Pelayanan Perizinan Berusaha</Title> <Title order={3}>Edit Pelayanan Perizinan Berusaha</Title>
<Text fw={"bold"}>Judul</Text>
{/* Nama Field */}
<TextInput <TextInput
label="Judul"
placeholder="Masukkan judul"
value={formData.name} value={formData.name}
onChange={(val) => { onChange={(e) => setFormData({ ...formData, name: e.target.value })}
setFormData({ required
...formData,
name: val.target.value,
})
}}
/>
<Text fw={"bold"}>Link</Text>
<TextInput
value={formData.link}
onChange={(val) => {
setFormData({
...formData,
link: val.target.value,
})
}}
/>
<Text fw={"bold"}>Deskripsi</Text>
<EditEditor
value={formData.deskripsi}
onChange={(val) => {
setFormData({
...formData,
deskripsi: val,
})
}}
/> />
{/* Link Field */}
<TextInput
label="Link"
placeholder="Masukkan link terkait"
value={formData.link}
onChange={(e) => setFormData({ ...formData, link: e.target.value })}
/>
{/* Deskripsi Field */}
<Box>
<Title order={6}>Deskripsi</Title>
<EditEditor
value={formData.deskripsi}
onChange={(val) => setFormData({ ...formData, deskripsi: val })}
/>
</Box>
{/* Action Buttons */}
<Group> <Group>
<Button <Button
bg={colors['blue-button']} bg={colors['blue-button']}
onClick={handleSubmit} onClick={handleSubmit}
loading={statePerizinanBerusaha.update.loading} loading={statePerizinanBerusaha.update.loading}
disabled={!formData.name}
> >
Submit {statePerizinanBerusaha.update.loading ? 'Menyimpan...' : 'Simpan Perubahan'}
</Button>
<Button
variant="outline"
onClick={() => router.back()}
disabled={statePerizinanBerusaha.update.loading}
>
Batal
</Button> </Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>
</Box>
</Stack> </Stack>
</Box> </Box>
); );

View File

@@ -1,6 +1,23 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Grid, GridCol, Group, Paper, Skeleton, Stack, Stepper, StepperCompleted, StepperStep, Text } from '@mantine/core'; import {
Box,
Button,
Center,
Divider,
Grid,
GridCol,
Group,
Paper,
Skeleton,
Stack,
Stepper,
StepperCompleted,
StepperStep,
Text,
Title,
Tooltip,
} from '@mantine/core';
import { IconEdit } from '@tabler/icons-react'; import { IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -9,54 +26,103 @@ import { useProxy } from 'valtio/utils';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
function PerizinanBerusaha() { function PerizinanBerusaha() {
const router = useRouter() const router = useRouter();
const pelayananPerizinanBerusaha = useProxy(stateLayananDesa.pelayananPerizinanBerusaha) const pelayananPerizinanBerusaha = useProxy(
stateLayananDesa.pelayananPerizinanBerusaha
);
const [active, setActive] = useState(1); const [active, setActive] = useState(1);
const nextStep = () => setActive((current) => (current < 6 ? current + 1 : current)); const nextStep = () =>
const prevStep = () => setActive((current) => (current > 0 ? current - 1 : current)); setActive((current) => (current < 6 ? current + 1 : current));
const prevStep = () =>
setActive((current) => (current > 0 ? current - 1 : current));
useShallowEffect(() => { useShallowEffect(() => {
pelayananPerizinanBerusaha.findById.load('1') pelayananPerizinanBerusaha.findById.load('1');
}, []) }, []);
if (!pelayananPerizinanBerusaha.findById.data) { if (!pelayananPerizinanBerusaha.findById.data) {
return ( return (
<Stack> <Stack align="center" justify="center" py="xl">
<Skeleton radius={10} h={800} /> <Skeleton radius="md" height={800} />
</Stack> </Stack>
) );
} }
const data = pelayananPerizinanBerusaha.findById.data;
return ( return (
<Box py={10}> <Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
<Paper bg={colors['white-1']} p={'md'}> <Stack gap="md">
<Paper bg={colors['BG-trans']} p={'md'}> {/* Header */}
<Box py={15}> <Grid align="center">
<Stack gap={"xs"}>
<Grid>
<GridCol span={{ base: 12, md: 11 }}> <GridCol span={{ base: 12, md: 11 }}>
<Text fz={"h4"} fw={"bold"}>Preview Pelayanan Perizinan Berusaha</Text> <Title order={3} c={colors['blue-button']}>
Preview Pelayanan Perizinan Berusaha
</Title>
</GridCol> </GridCol>
<GridCol span={{ base: 12, md: 1 }}> <GridCol span={{ base: 12, md: 1 }}>
<Button bg={colors['blue-button']} onClick={() => router.push('/admin/desa/layanan/pelayanan_perizinan_berusaha/edit')}> <Tooltip label="Edit Data Perizinan" withArrow>
<IconEdit size={16} /> <Button
c="green"
variant="light"
leftSection={<IconEdit size={18} stroke={2} />}
radius="md"
onClick={() =>
router.push(
'/admin/desa/layanan/pelayanan_perizinan_berusaha/edit'
)
}
>
Edit
</Button> </Button>
</Tooltip>
</GridCol> </GridCol>
</Grid> </Grid>
</Stack>
</Box>
<Text fz={{ base: "h4", md: 'h2' }} fw={"bold"}>{pelayananPerizinanBerusaha.findById.data.name}</Text>
<Text py={10} ta={"justify"} fz={{ base: "sm", md: 'h3' }} dangerouslySetInnerHTML={{__html: pelayananPerizinanBerusaha.findById.data.deskripsi}} />
<Text py={10} fz={{ base: "sm", md: 'h3' }}>Proses pendaftaran NIB melalui OSS mencakup beberapa langkah umum, seperti:</Text>
<Box p={"xl"} w={{ base: "100%", md: "100%" }} >
<Stepper active={active} onStepClick={setActive} orientation="vertical"
styles={{
separator: {
marginLeft: 25
},
step: { {/* Content */}
padding: '12px 0' <Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">
} <Box px={{ base: 0, md: 50 }} pb="xl">
}}> <Center>
<Text
ta="center"
fz={{ base: '1.2rem', md: '1.8rem' }}
fw="bold"
c={colors['blue-button']}
>
{data.name}
</Text>
</Center>
<Divider my="md" color={colors['blue-button']} />
<Box mt="lg">
<Text
py={10}
ta="justify"
fz={{ base: '1rem', md: '1.2rem' }}
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
/>
<Text
py={10}
fz={{ base: '1rem', md: '1.2rem' }}
fw="bold"
c={colors['blue-button']}
>
Proses pendaftaran NIB melalui OSS mencakup beberapa langkah
umum:
</Text>
<Box p="xl" w="100%">
<Stepper
active={active}
onStepClick={setActive}
orientation="vertical"
styles={{
separator: { marginLeft: 25 },
step: { padding: '12px 0' },
}}
>
<StepperStep label="Langkah Pertama" description="Pendaftaran Akun"> <StepperStep label="Langkah Pertama" description="Pendaftaran Akun">
Pendaftaran akun pada portal OSS Pendaftaran akun pada portal OSS
</StepperStep> </StepperStep>
@@ -81,16 +147,37 @@ function PerizinanBerusaha() {
</Stepper> </Stepper>
<Group justify="center" mt="xl"> <Group justify="center" mt="xl">
<Button variant="default" onClick={prevStep}>Back</Button> <Button variant="default" onClick={prevStep}>
Back
</Button>
<Button onClick={nextStep}>Next step</Button> <Button onClick={nextStep}>Next step</Button>
</Group> </Group>
<Text py={35} ta={"justify"} fz={{ base: "sm", md: 'h3' }}>Penting untuk diingat bahwa prosedur dan persyaratan dapat berubah </Box>
seiring waktu. Untuk informasi yang lebih akurat dan terkini, saya sarankan untuk mengunjungi situs
resmi OSS <a href={pelayananPerizinanBerusaha.findById.data.link}>{pelayananPerizinanBerusaha.findById.data.link}</a> atau menghubungi instansi terkait di pemerintah Indonesia yang bertanggung jawab atas urusan perizinan usaha.</Text> <Text
py={35}
ta="justify"
fz={{ base: '1rem', md: '1.2rem' }}
>
Penting untuk diingat bahwa prosedur dan persyaratan dapat
berubah seiring waktu. Untuk informasi yang lebih akurat dan
terkini, silakan kunjungi situs resmi OSS{' '}
<a
href={data.link}
target="_blank"
rel="noopener noreferrer"
style={{ color: colors['blue-button'] }}
>
{data.link}
</a>{' '}
atau hubungi instansi terkait di pemerintah Indonesia yang
bertanggung jawab atas urusan perizinan usaha.
</Text>
</Box>
</Box> </Box>
</Paper> </Paper>
</Stack>
</Paper> </Paper>
</Box>
); );
} }

View File

@@ -4,7 +4,18 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa'; import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Image,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
@@ -13,9 +24,10 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function EditSuratKeterangan() { function EditSuratKeterangan() {
const router = useRouter() const router = useRouter();
const params = useParams() const params = useParams();
const stateSurat = useProxy(stateLayananDesa.suratKeterangan) const stateSurat = useProxy(stateLayananDesa.suratKeterangan);
const [previewImage, setPreviewImage] = useState<string | null>(null); const [previewImage, setPreviewImage] = useState<string | null>(null);
const [previewImage2, setPreviewImage2] = useState<string | null>(null); const [previewImage2, setPreviewImage2] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
@@ -25,39 +37,32 @@ function EditSuratKeterangan() {
deskripsi: stateSurat.edit.form.deskripsi, deskripsi: stateSurat.edit.form.deskripsi,
imageId: stateSurat.edit.form.imageId, imageId: stateSurat.edit.form.imageId,
image2Id: stateSurat.edit.form.image2Id, image2Id: stateSurat.edit.form.image2Id,
}) });
useEffect(() => { useEffect(() => {
const loadSurat = async () => { const loadSurat = async () => {
const id = params?.id as string; const id = params?.id as string;
if (!id) return; if (!id) return;
try { try {
const data = await stateSurat.edit.load(id); const data = await stateSurat.edit.load(id);
if (data) { if (data) {
setFormData({ setFormData({
name: data.name || "", name: data.name || '',
deskripsi: data.deskripsi || "", deskripsi: data.deskripsi || '',
imageId: data.imageId || "", imageId: data.imageId || '',
image2Id: data.image2Id || "", image2Id: data.image2Id || '',
}); });
if (data.image?.link) { setPreviewImage(data.image?.link || null);
setPreviewImage(data.image.link); setPreviewImage2(data.image2?.link || null);
} else {
setPreviewImage(null);
}
if (data.image2?.link) {
setPreviewImage2(data.image2.link);
} else {
setPreviewImage2(null);
}
} }
} catch (error) { } catch (error) {
console.error("Error loading surat:", error); console.error('Error loading surat:', error);
toast.error("Gagal memuat data surat"); toast.error('Gagal memuat data surat');
} }
}; };
loadSurat(); loadSurat();
}, [params?.id]); }, [params?.id]);
@@ -65,171 +70,199 @@ function EditSuratKeterangan() {
try { try {
stateSurat.edit.form = { stateSurat.edit.form = {
...stateSurat.edit.form, ...stateSurat.edit.form,
name: formData.name, ...formData,
deskripsi: formData.deskripsi, };
imageId: formData.imageId,
image2Id: formData.image2Id,
}
if (file) { if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name }); const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data; const uploaded = res.data?.data;
if (!uploaded?.id) return toast.error('Gagal upload gambar');
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
stateSurat.edit.form.imageId = uploaded.id; stateSurat.edit.form.imageId = uploaded.id;
} }
if (file2) { if (file2) {
const res = await ApiFetch.api.fileStorage.create.post({ file: file2, name: file2.name }); const res = await ApiFetch.api.fileStorage.create.post({ file: file2, name: file2.name });
const uploaded = res.data?.data; const uploaded = res.data?.data;
if (!uploaded?.id) return toast.error('Gagal upload gambar');
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
stateSurat.edit.form.image2Id = uploaded.id; stateSurat.edit.form.image2Id = uploaded.id;
} }
await stateSurat.edit.update() await stateSurat.edit.update();
toast.success("Surat berhasil diperbarui!") toast.success('Surat berhasil diperbarui!');
router.push("/admin/desa/layanan/pelayanan_surat_keterangan") router.push('/admin/desa/layanan/pelayanan_surat_keterangan');
} catch (error) { } catch (error) {
console.error("Error updating surat:", error); console.error('Error updating surat:', error);
toast.error("Terjadi kesalahan saat memperbarui surat"); toast.error('Terjadi kesalahan saat memperbarui surat');
} }
} };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Back Button */}
<Button variant="subtle" onClick={() => router.back()}> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <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> </Button>
</Box> </Tooltip>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}> <Title order={4} ml="sm" c="dark">
<Stack gap={"xs"}> Edit Surat Keterangan
<Title order={3}>Edit Surat Keterangan</Title> </Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label="Nama Surat Keterangan"
placeholder="Masukkan nama surat keterangan"
value={formData.name} value={formData.name}
onChange={(val) => { onChange={(e) => setFormData({ ...formData, name: e.target.value })}
setFormData({ ...formData, name: val.target.value }); required
}}
label={<Text fz={"sm"} fw={"bold"}>Nama Surat Keterangan</Text>}
placeholder="masukkan nama surat keterangan"
/> />
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Konten</Text> <Text fz="sm" fw="bold" mb={6}>
Konten
</Text>
<EditEditor <EditEditor
value={formData.deskripsi} value={formData.deskripsi}
onChange={(htmlContent) => { onChange={(htmlContent) => setFormData({ ...formData, deskripsi: htmlContent })}
setFormData({ ...formData, deskripsi: htmlContent });
}}
/> />
</Box> </Box>
{/* Upload Gambar 1 */}
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text> <Text fw="bold" fz="sm" mb={6}>
<Box > Gambar Konten Pelayanan
</Text>
<Dropzone <Dropzone
onDrop={(files) => { onDrop={(files) => {
const file = files[0]; // Hanya ambil file pertama const selectedFile = files[0];
if (file) { if (selectedFile) {
setFile(file); setFile(selectedFile);
setPreviewImage(URL.createObjectURL(file)); // Buat preview setPreviewImage(URL.createObjectURL(selectedFile));
} }
}} }}
maxSize={5 * 1024 ** 2} // 5MB onReject={() => toast.error('File tidak valid, gunakan format gambar')}
accept={{ maxSize={5 * 1024 ** 2}
'image/*': ['.jpeg', '.jpg', '.png', '.webp'] accept={{ 'image/*': [] }}
}} radius="md"
p="xl"
> >
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}> <Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept> <Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /> <IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
</Dropzone.Accept> </Dropzone.Accept>
<Dropzone.Reject> <Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} /> <IconX size={48} color="red" stroke={1.5} />
</Dropzone.Reject> </Dropzone.Reject>
<Dropzone.Idle> <Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} /> <IconPhoto size={48} color="#868e96" stroke={1.5} />
</Dropzone.Idle> </Dropzone.Idle>
<Stack gap="xs" align="center">
<div> <Text size="md" fw={500}>
<Text size="xl" inline> Seret gambar atau klik untuk memilih file
Drag images here or click to select files
</Text> </Text>
<Text size="sm" c="dimmed" inline mt={7}> <Text size="sm" c="dimmed">
Attach as many files as you like, each file should not exceed 5mb Maksimal 5MB, format gambar wajib
</Text> </Text>
</div> </Stack>
</Group> </Group>
</Dropzone> </Dropzone>
{previewImage && ( {previewImage && (
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
<Image <Image
src={previewImage} src={previewImage}
alt="Preview" alt="Preview Gambar 1"
width={280} radius="md"
height={180} style={{
fit="cover" maxHeight: 220,
radius="sm" objectFit: 'contain',
mt="md" border: `1px solid ${colors['blue-button']}`,
}}
/> />
</Box>
)} )}
</Box> </Box>
</Box>
<Box> {/* Upload Gambar 2 */}
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box> <Box>
<Text fw="bold" fz="sm" mb={6}>
Gambar Alur Pelayanan Surat
</Text>
<Dropzone <Dropzone
onDrop={(files) => { onDrop={(files) => {
const file = files[0]; // Hanya ambil file pertama const selectedFile = files[0];
if (file) { if (selectedFile) {
setFile2(file); setFile2(selectedFile);
setPreviewImage2(URL.createObjectURL(file)); // Buat preview setPreviewImage2(URL.createObjectURL(selectedFile));
} }
}} }}
maxSize={5 * 1024 ** 2} // 5MB onReject={() => toast.error('File tidak valid, gunakan format gambar')}
accept={{ maxSize={5 * 1024 ** 2}
'image/*': ['.jpeg', '.jpg', '.png', '.webp'] accept={{ 'image/*': [] }}
}} radius="md"
p="xl"
> >
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}> <Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept> <Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /> <IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
</Dropzone.Accept> </Dropzone.Accept>
<Dropzone.Reject> <Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} /> <IconX size={48} color="red" stroke={1.5} />
</Dropzone.Reject> </Dropzone.Reject>
<Dropzone.Idle> <Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} /> <IconPhoto size={48} color="#868e96" stroke={1.5} />
</Dropzone.Idle> </Dropzone.Idle>
<Stack gap="xs" align="center">
<div> <Text size="md" fw={500}>
<Text size="xl" inline> Seret gambar atau klik untuk memilih file
Drag images here or click to select files
</Text> </Text>
<Text size="sm" c="dimmed" inline mt={7}> <Text size="sm" c="dimmed">
Attach as many files as you like, each file should not exceed 5mb Maksimal 5MB, format gambar wajib
</Text> </Text>
</div> </Stack>
</Group> </Group>
</Dropzone> </Dropzone>
{previewImage2 && ( {previewImage2 && (
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
<Image <Image
src={previewImage2} src={previewImage2}
alt="Preview" alt="Preview Gambar 2"
width={280} radius="md"
height={180} style={{
fit="cover" maxHeight: 220,
radius="sm" objectFit: 'contain',
mt="md" border: `1px solid ${colors['blue-button']}`,
}}
/> />
</Box>
)} )}
</Box> </Box>
</Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button> <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> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

@@ -2,100 +2,177 @@
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa'; import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import {
Box,
Button,
Group,
Image,
Paper,
Skeleton,
Stack,
Text,
Tooltip,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function DetailSuratKeterangan() { function DetailSuratKeterangan() {
const suratKeteranganState = useProxy(stateLayananDesa.suratKeterangan) const suratKeteranganState = useProxy(stateLayananDesa.suratKeterangan);
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null);
const params = useParams() const params = useParams();
const router = useRouter() const router = useRouter();
useShallowEffect(() => { useShallowEffect(() => {
suratKeteranganState.findUnique.load(params?.id as string) suratKeteranganState.findUnique.load(params?.id as string);
}, []) }, []);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
suratKeteranganState.delete.byId(selectedId) suratKeteranganState.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/desa/layanan/pelayanan_surat_keterangan") router.push('/admin/desa/layanan/pelayanan_surat_keterangan');
}
} }
};
if (!suratKeteranganState.findUnique.data) { if (!suratKeteranganState.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
{Array.from({ length: 10 }).map((_, k) => ( <Skeleton height={500} radius="md" />
<Skeleton key={k} h={40} />
))}
</Stack> </Stack>
) );
} }
const data = suratKeteranganState.findUnique.data;
return ( return (
<Box> <Box py={10}>
<Box mb={10}> {/* Tombol Kembali */}
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Surat Keterangan</Text>
{suratKeteranganState.findUnique.data ? (
<Paper key={suratKeteranganState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Nama</Text>
<Text fz={"lg"}>{suratKeteranganState.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
<Text fz={"lg"}dangerouslySetInnerHTML={{ __html: suratKeteranganState.findUnique.data?.deskripsi }}></Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
<Image w={{ base: 150, md: 150, lg: 150 }} src={suratKeteranganState.findUnique.data?.image?.link} alt="gambar" />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
<Image w={{ base: 150, md: 150, lg: 150 }} src={suratKeteranganState.findUnique.data?.image2?.link} alt="gambar" />
</Box>
<Flex gap={"xs"} mt={10}>
<Button <Button
onClick={() => { variant="subtle"
if (suratKeteranganState.findUnique.data) { onClick={() => router.back()}
setSelectedId(suratKeteranganState.findUnique.data.id); leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
setModalHapus(true); mb={15}
}
}}
disabled={suratKeteranganState.delete.loading || !suratKeteranganState.findUnique.data}
color={"red"}
> >
<IconX size={20} /> Kembali
</Button> </Button>
<Button
onClick={() => { <Paper
if (suratKeteranganState.findUnique.data) { withBorder
router.push(`/admin/desa/layanan/pelayanan_surat_keterangan/${suratKeteranganState.findUnique.data.id}/edit`); 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 Surat Keterangan
</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?.name || '-'}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">
Deskripsi
</Text>
<Text
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{
__html: data?.deskripsi || '-',
}} }}
disabled={!suratKeteranganState.findUnique.data} />
color={"green"} </Box>
<Box>
<Text fz="lg" fw="bold">
Gambar Konten Pelayanan
</Text>
{data?.image?.link ? (
<Image
src={data.image.link}
alt="gambar"
w={200}
h={200}
radius="md"
fit="cover"
/>
) : (
<Text fz="sm" c="dimmed">
Tidak ada gambar
</Text>
)}
</Box>
<Box>
<Text fz="lg" fw="bold">
Gambar Alur Pelayanan Surat
</Text>
{data?.image2?.link ? (
<Image
src={data.image2.link}
alt="gambar"
w={200}
h={200}
radius="md"
fit="cover"
/>
) : (
<Text fz="sm" c="dimmed">
Tidak ada gambar
</Text>
)}
</Box>
<Group gap="sm">
<Tooltip label="Hapus Surat" withArrow position="top">
<Button
color="red"
onClick={() => {
setSelectedId(data.id);
setModalHapus(true);
}}
variant="light"
radius="md"
size="md"
disabled={suratKeteranganState.delete.loading}
>
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Surat" withArrow position="top">
<Button
color="green"
onClick={() =>
router.push(
`/admin/desa/layanan/pelayanan_surat_keterangan/${data.id}/edit`
)
}
variant="light"
radius="md"
size="md"
> >
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Group>
</Stack> </Stack>
</Paper> </Paper>
) : null}
</Stack> </Stack>
</Paper> </Paper>
@@ -104,7 +181,7 @@ function DetailSuratKeterangan() {
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus berita ini?' text="Apakah Anda yakin ingin menghapus surat keterangan ini?"
/> />
</Box> </Box>
); );

View File

@@ -1,9 +1,21 @@
'use client' 'use client';
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa'; import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Image,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@@ -12,25 +24,25 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function CreateSuratKeterangan() { function CreateSuratKeterangan() {
const stateSurat = useProxy(stateLayananDesa.suratKeterangan) const stateSurat = useProxy(stateLayananDesa.suratKeterangan);
const [previewImage2, setPreviewImage2] = useState<{ preview: string; file: File } | null>(null); const [previewImage2, setPreviewImage2] = useState<{ preview: string; file: File } | null>(null);
const [previewImage, setPreviewImage] = useState<{ preview: string; file: File } | null>(null); const [previewImage, setPreviewImage] = useState<{ preview: string; file: File } | null>(null);
const router = useRouter() const router = useRouter();
const resetForm = () => { const resetForm = () => {
stateSurat.create.form = { stateSurat.create.form = {
name: "", name: '',
deskripsi: "", deskripsi: '',
imageId: "", imageId: '',
image2Id: "" image2Id: '',
} };
setPreviewImage(null) setPreviewImage(null);
setPreviewImage2(null) setPreviewImage2(null);
} };
const handleSubmit = async () => { const handleSubmit = async () => {
if (!previewImage) { if (!previewImage) {
return toast.warn("Pilih file gambar utama terlebih dahulu"); return toast.warn('Pilih file gambar utama terlebih dahulu');
} }
try { try {
@@ -42,11 +54,10 @@ function CreateSuratKeterangan() {
const uploadedImage1 = res1.data?.data; const uploadedImage1 = res1.data?.data;
if (!uploadedImage1?.id) { if (!uploadedImage1?.id) {
return toast.error("Gagal upload gambar utama"); return toast.error('Gagal upload gambar utama');
} }
let uploadedImage2 = null; let uploadedImage2 = null;
// Upload gambar kedua jika ada
if (previewImage2) { if (previewImage2) {
const res2 = await ApiFetch.api.fileStorage.create.post({ const res2 = await ApiFetch.api.fileStorage.create.post({
file: previewImage2.file, file: previewImage2.file,
@@ -55,44 +66,58 @@ function CreateSuratKeterangan() {
uploadedImage2 = res2.data?.data; uploadedImage2 = res2.data?.data;
} }
// Set form data
stateSurat.create.form.imageId = uploadedImage1.id; stateSurat.create.form.imageId = uploadedImage1.id;
if (uploadedImage2?.id) { if (uploadedImage2?.id) {
stateSurat.create.form.image2Id = uploadedImage2.id; stateSurat.create.form.image2Id = uploadedImage2.id;
} }
// Create the record
await stateSurat.create.create(); await stateSurat.create.create();
// Reset form dan redirect
resetForm(); resetForm();
toast.success("Data surat keterangan berhasil ditambahkan"); toast.success('Data surat keterangan berhasil ditambahkan');
router.push("/admin/desa/layanan/pelayanan_surat_keterangan"); router.push('/admin/desa/layanan/pelayanan_surat_keterangan');
} catch (error) { } catch (error) {
console.error("Error creating surat keterangan:", error); console.error('Error creating surat keterangan:', error);
toast.error("Terjadi kesalahan saat menambahkan surat keterangan"); toast.error('Terjadi kesalahan saat menambahkan surat keterangan');
} }
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header */}
<Button variant="subtle" onClick={() => router.back()}> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <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> </Button>
</Box> </Tooltip>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}> <Title order={4} ml="sm" c="dark">
<Stack gap={"xs"}> Tambah Surat Keterangan
<Title order={3}>Create Surat Keterangan</Title> </Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Nama Surat */}
<TextInput <TextInput
value={stateSurat.create.form.name} value={stateSurat.create.form.name}
onChange={(val) => { onChange={(val) => (stateSurat.create.form.name = val.target.value)}
stateSurat.create.form.name = val.target.value; label={<Text fz="sm" fw="bold">Nama Surat Keterangan</Text>}
}} placeholder="Masukkan nama surat keterangan"
label={<Text fz={"sm"} fw={"bold"}>Nama Surat Keterangan</Text>} required
placeholder="masukkan nama surat keterangan"
/> />
{/* Konten */}
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Konten</Text> <Text fz="sm" fw="bold" mb={6}>
Konten
</Text>
<CreateEditor <CreateEditor
value={stateSurat.create.form.deskripsi} value={stateSurat.create.form.deskripsi}
onChange={(htmlContent) => { onChange={(htmlContent) => {
@@ -100,106 +125,124 @@ function CreateSuratKeterangan() {
}} }}
/> />
</Box> </Box>
{/* Gambar Konten Pelayanan */}
<Box> <Box>
<Text fz={"md"} fw={"bold"} mb="sm">Gambar Utama</Text> <Text fw="bold" fz="sm" mb={6}>
Gambar Konten Pelayanan
</Text>
<Dropzone <Dropzone
onDrop={(files) => { onDrop={(files) => {
const file = files[0]; const file = files[0];
if (file) { if (file) {
setPreviewImage({ setPreviewImage({
file, file,
preview: URL.createObjectURL(file) preview: URL.createObjectURL(file),
}); });
} }
}} }}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2} maxSize={5 * 1024 ** 2}
accept={{ accept={{ 'image/*': [] }}
'image/*': ['.jpeg', '.jpg', '.png', '.webp'] radius="md"
}} p="xl"
> >
<Group justify="center" gap="xl" mih={120} style={{ pointerEvents: 'none' }}> <Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept> <Dropzone.Accept>
<IconUpload size={32} color="var(--mantine-color-blue-6)" stroke={1.5} /> <IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept> </Dropzone.Accept>
<Dropzone.Reject> <Dropzone.Reject>
<IconX size={32} color="var(--mantine-color-red-6)" stroke={1.5} /> <IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject> </Dropzone.Reject>
<Dropzone.Idle> <Dropzone.Idle>
<IconPhoto size={32} color="var(--mantine-color-dimmed)" stroke={1.5} /> <IconPhoto size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle> </Dropzone.Idle>
<div>
<Text size="md" inline>Seret gambar ke sini atau klik untuk memilih</Text>
<Text size="sm" c="dimmed" inline mt={7} display="block">
Ukuran maksimal 5MB (JPEG, JPG, PNG, WebP)
</Text>
</div>
</Group> </Group>
<Text ta="center" mt="sm" size="sm" color="dimmed">
Seret gambar atau klik untuk memilih file (maks 5MB)
</Text>
</Dropzone> </Dropzone>
{previewImage && ( {previewImage && (
<Box mt="sm" style={{ textAlign: 'center' }}>
<Image <Image
src={previewImage.preview} src={previewImage.preview}
alt="Preview Gambar Utama" alt="Preview Gambar Utama"
width={280} radius="md"
height={180} style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
fit="cover"
radius="sm"
mt="md"
/> />
</Box>
)} )}
</Box> </Box>
<Box mt="lg"> {/* Gambar Alur Pelayanan Surat */}
<Text fz={"md"} fw={"bold"} mb="sm">Gambar Tambahan (Opsional)</Text> <Box>
<Text fw="bold" fz="sm" mb={6}>
Gambar Alur Pelayanan Surat
</Text>
<Dropzone <Dropzone
onDrop={(files) => { onDrop={(files) => {
const file = files[0]; const file = files[0];
if (file) { if (file) {
setPreviewImage2({ setPreviewImage2({
file, file,
preview: URL.createObjectURL(file) preview: URL.createObjectURL(file),
}); });
} }
}} }}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2} maxSize={5 * 1024 ** 2}
accept={{ accept={{ 'image/*': [] }}
'image/*': ['.jpeg', '.jpg', '.png', '.webp'] radius="md"
}} p="xl"
> >
<Group justify="center" gap="xl" mih={120} style={{ pointerEvents: 'none' }}> <Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept> <Dropzone.Accept>
<IconUpload size={32} color="var(--mantine-color-blue-6)" stroke={1.5} /> <IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept> </Dropzone.Accept>
<Dropzone.Reject> <Dropzone.Reject>
<IconX size={32} color="var(--mantine-color-red-6)" stroke={1.5} /> <IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject> </Dropzone.Reject>
<Dropzone.Idle> <Dropzone.Idle>
<IconPhoto size={32} color="var(--mantine-color-dimmed)" stroke={1.5} /> <IconPhoto size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle> </Dropzone.Idle>
<div>
<Text size="md" inline>Seret gambar ke sini atau klik untuk memilih</Text>
<Text size="sm" c="dimmed" inline mt={7} display="block">
Ukuran maksimal 5MB (JPEG, JPG, PNG, WebP)
</Text>
</div>
</Group> </Group>
<Text ta="center" mt="sm" size="sm" color="dimmed">
Seret gambar atau klik untuk memilih file (maks 5MB)
</Text>
</Dropzone> </Dropzone>
{previewImage2 ? ( {previewImage2 ? (
<Box mt="sm" style={{ textAlign: 'center' }}>
<Image <Image
src={previewImage2.preview} src={previewImage2.preview}
alt="Preview Gambar Tambahan" alt="Preview Gambar Tambahan"
width={280} radius="md"
height={180} style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
fit="cover"
radius="sm"
mt="md"
/> />
</Box>
) : ( ) : (
<Text size="sm" c="dimmed" mt="sm"> <Text size="sm" c="dimmed" mt="sm" ta="center">
Kosongkan jika tidak ada gambar tambahan Kosongkan jika tidak ada gambar tambahan
</Text> </Text>
)} )}
</Box> </Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
{/* Tombol Simpan */}
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

@@ -1,13 +1,30 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import {
import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip
} from '@mantine/core';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import stateLayananDesa from '../../../_state/desa/layananDesa'; import stateLayananDesa from '../../../_state/desa/layananDesa';
function SuratKeterangan() { function SuratKeterangan() {
@@ -16,7 +33,7 @@ function SuratKeterangan() {
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Pelayanan Surat Keterangan' title='Pelayanan Surat Keterangan'
placeholder='pencarian' placeholder='Cari nama atau deskripsi...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -27,8 +44,8 @@ function SuratKeterangan() {
} }
function ListSuratKeterangan({ search }: { search: string }) { function ListSuratKeterangan({ search }: { search: string }) {
const suratKeteranganState = useProxy(stateLayananDesa.suratKeterangan) const suratKeteranganState = useProxy(stateLayananDesa.suratKeterangan);
const router = useRouter() const router = useRouter();
const { const {
data, data,
@@ -39,102 +56,111 @@ function ListSuratKeterangan({ search }: { search: string }) {
} = suratKeteranganState.findMany; } = suratKeteranganState.findMany;
useEffect(() => { useEffect(() => {
load(page, 10) load(page, 10, search);
}, []) }, [page, search]);
const filteredData = useMemo(() => { const filteredData = useMemo(() => {
if (!data) return []; if (!data) return [];
return data.filter(item => {
const keyword = search.toLowerCase(); const keyword = search.toLowerCase();
return ( return data.filter(item =>
item.name?.toLowerCase().includes(keyword) || item.name?.toLowerCase().includes(keyword) ||
item.deskripsi?.toLowerCase().includes(keyword) item.deskripsi?.toLowerCase().includes(keyword)
); );
})
}, [data, search]); }, [data, search]);
// Handle loading state // Loading state
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton height={300} /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
); );
} }
if (data.length === 0) {
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<JudulList <Group justify="space-between" mb="md">
title='List Surat Keterangan' <Title order={4}>List Surat Keterangan</Title>
href='/admin/desa/layanan/pelayanan_surat_keterangan/create' <Tooltip label="Tambah Surat Keterangan" withArrow>
/> <Button
<Box style={{ overflowX: "auto" }}> leftSection={<IconPlus size={18} />}
<Table striped withTableBorder withRowBorders> color="blue"
<TableThead> variant="light"
<TableTr> onClick={() =>
<TableTh>Nama</TableTh> router.push('/admin/desa/layanan/pelayanan_surat_keterangan/create')
<TableTh>Deskripsi</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
</Table>
</Box>
</Paper>
</Box>
);
} }
return ( >
<Box py={10}> Tambah Baru
<Paper bg={colors['white-1']} p={'md'}> </Button>
<JudulList </Tooltip>
title='List Surat Keterangan' </Group>
href='/admin/desa/layanan/pelayanan_surat_keterangan/create' <Box style={{ overflowX: "auto" }}>
/> <Table highlightOnHover>
<Table striped withTableBorder withRowBorders>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Nama</TableTh> <TableTh style={{ width: '30%' }}>Nama</TableTh>
<TableTh>Deskripsi</TableTh> <TableTh style={{ width: '45%' }}>Deskripsi</TableTh>
<TableTh>Detail</TableTh> <TableTh style={{ width: '15%' }}>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item) => ( {filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd style={{ width: '30%' }}>
<Box w={200}> <Box w={200}>
<Text truncate="end" fz={"sm"}>{item.name}</Text> <Text fw={500} truncate="end" lineClamp={1}>
</Box> {item.name}
</TableTd>
<TableTd>
<Box w={300}>
<Text truncate="end" lineClamp={1} fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</Box>
</TableTd>
<TableTd>
<Text>
<Button onClick={() => router.push(`/admin/desa/layanan/pelayanan_surat_keterangan/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</Text> </Text>
</Box>
</TableTd>
<TableTd style={{ width: '45%' }}>
<Box w={200}>
<Text truncate="end" lineClamp={1} fz="sm" c="dimmed"
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
/>
</Box>
</TableTd>
<TableTd style={{ width: '15%' }}>
<Button
variant="light"
color="blue"
onClick={() =>
router.push(`/admin/desa/layanan/pelayanan_surat_keterangan/${item.id}`)
}
>
<IconDeviceImac size={20} />
<Text ml={5}>Detail</Text>
</Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} ))
) : (
<TableTr>
<TableTd colSpan={3}>
<Center py={20}>
<Text color="dimmed">Tidak ada data surat keterangan yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody> </TableTbody>
</Table> </Table>
</Box>
</Paper> </Paper>
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => { onChange={(newPage) => {
load(newPage, 10); load(newPage, 10, search);
window.scrollTo(0, 0); window.scrollTo({ top: 0, behavior: 'smooth' });
}} }}
total={totalPages} total={totalPages}
mt="md" mt="md"
mb="md" mb="md"
color="blue"
radius="md"
/> />
</Center> </Center>
</Box> </Box>

View File

@@ -2,22 +2,34 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa'; import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
function EditPelayananTelunjukSakti() { function EditPelayananTelunjukSakti() {
const stateTelunjukDesa = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa) const stateTelunjukDesa = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa);
const router = useRouter() const router = useRouter();
const params = useParams() const params = useParams();
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: stateTelunjukDesa.edit.form.name, name: stateTelunjukDesa.edit.form.name,
deskripsi: stateTelunjukDesa.edit.form.deskripsi, deskripsi: stateTelunjukDesa.edit.form.deskripsi,
link: stateTelunjukDesa.edit.form.link, link: stateTelunjukDesa.edit.form.link,
}) });
useEffect(() => { useEffect(() => {
const loadPelayananTelunjukSakti = async () => { const loadPelayananTelunjukSakti = async () => {
@@ -27,14 +39,14 @@ function EditPelayananTelunjukSakti() {
const data = await stateTelunjukDesa.edit.load(id); const data = await stateTelunjukDesa.edit.load(id);
if (data) { if (data) {
setFormData({ setFormData({
name: data.name, name: data.name || '',
deskripsi: data.deskripsi, deskripsi: data.deskripsi || '',
link: data.link, link: data.link || '',
}); });
} }
} catch (error) { } catch (error) {
console.error("Error loading pelayanan telunjuk sakti:", error); console.error('Error loading pelayanan telunjuk sakti:', error);
toast.error("Gagal memuat data pelayanan telunjuk sakti"); toast.error('Gagal memuat data pelayanan telunjuk sakti');
} }
}; };
loadPelayananTelunjukSakti(); loadPelayananTelunjukSakti();
@@ -44,54 +56,83 @@ function EditPelayananTelunjukSakti() {
try { try {
stateTelunjukDesa.edit.form = { stateTelunjukDesa.edit.form = {
...stateTelunjukDesa.edit.form, ...stateTelunjukDesa.edit.form,
name: formData.name, ...formData,
deskripsi: formData.deskripsi, };
link: formData.link, await stateTelunjukDesa.edit.update();
} toast.success('Pelayanan telunjuk sakti berhasil diperbarui!');
await stateTelunjukDesa.edit.update() router.push('/admin/desa/layanan/pelayanan_telunjuk_sakti_desa');
toast.success("Pelayanan telunjuk sakti berhasil diperbarui!")
router.push("/admin/desa/layanan/pelayanan_telunjuk_sakti_desa")
} catch (error) { } catch (error) {
console.error("Error updating pelayanan telunjuk sakti:", error); console.error('Error updating pelayanan telunjuk sakti:', error);
toast.error("Terjadi kesalahan saat memperbarui pelayanan telunjuk sakti"); toast.error('Terjadi kesalahan saat memperbarui pelayanan telunjuk sakti');
}
} }
};
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Back Button + Title */}
<Button variant="subtle" onClick={() => router.back()}> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <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> </Button>
</Box> </Tooltip>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}> <Title order={4} ml="sm" c="dark">
<Stack gap={"xs"}> Edit Pelayanan Telunjuk Sakti Desa
<Title order={3}>Edit Surat Keterangan</Title> </Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Nama */}
<TextInput <TextInput
label="Nama Pelayanan"
placeholder="Masukkan nama pelayanan"
value={formData.name} value={formData.name}
onChange={(val) => { onChange={(e) => setFormData({ ...formData, name: e.target.value })}
setFormData({ ...formData, name: val.target.value }); required
}}
label={<Text fz={"sm"} fw={"bold"}>Nama Surat Keterangan</Text>}
placeholder="masukkan nama surat keterangan"
/> />
<TextInput
{/* Deskripsi pakai editor */}
<Box>
<Text fz="sm" fw="bold" mb={6}>
Deskripsi
</Text>
<EditEditor
value={formData.deskripsi} value={formData.deskripsi}
onChange={(val) => { onChange={(htmlContent) => setFormData({ ...formData, deskripsi: htmlContent })}
setFormData({ ...formData, deskripsi: val.target.value });
}}
label={<Text fz={"sm"} fw={"bold"}>Tautan Link</Text>}
placeholder="masukkan tautan link"
/> />
</Box>
{/* Link */}
<TextInput <TextInput
label="Link"
placeholder="Masukkan link terkait"
value={formData.link} value={formData.link}
onChange={(val) => { onChange={(e) => setFormData({ ...formData, link: e.target.value })}
setFormData({ ...formData, link: val.target.value });
}}
label={<Text fz={"sm"} fw={"bold"}>Link</Text>}
placeholder="masukkan link"
/> />
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
{/* Tombol Simpan */}
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

@@ -2,109 +2,166 @@
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa'; import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Skeleton,
Stack,
Text,
Tooltip,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function DetailPelayananTelunjukSakti() { function DetailPelayananTelunjukSakti() {
const telunjukSaktiState = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa) const telunjukSaktiState = useProxy(
const [modalHapus, setModalHapus] = useState(false) stateLayananDesa.pelayananTelunjukSaktiDesa
const [selectedId, setSelectedId] = useState<string | null>(null) );
const params = useParams() const [modalHapus, setModalHapus] = useState(false);
const router = useRouter() const [selectedId, setSelectedId] = useState<string | null>(null);
const params = useParams();
const router = useRouter();
useShallowEffect(() => { useShallowEffect(() => {
telunjukSaktiState.findUnique.load(params?.id as string) telunjukSaktiState.findUnique.load(params?.id as string);
}, []) }, []);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
telunjukSaktiState.delete.byId(selectedId) telunjukSaktiState.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/desa/layanan/pelayanan_telunjuk_sakti_desa") router.push('/admin/desa/layanan/pelayanan_telunjuk_sakti_desa');
}
} }
};
if (!telunjukSaktiState.findUnique.data) { if (!telunjukSaktiState.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
{Array.from({ length: 10 }).map((_, k) => ( <Skeleton height={500} radius="md" />
<Skeleton key={k} h={40} />
))}
</Stack> </Stack>
) );
} }
const data = telunjukSaktiState.findUnique.data;
return ( return (
<Box> <Box py={10}>
<Box mb={10}> {/* Tombol Kembali */}
<Button variant="subtle" onClick={() => router.back()}> <Button
<IconArrowBack color={colors['blue-button']} size={25} /> variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15}
>
Kembali
</Button> </Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}> <Paper
<Stack> withBorder
<Text fz={"xl"} fw={"bold"}>Detail Pelayanan Telunjuk Sakti Desa</Text> w={{ base: '100%', md: '60%' }}
{telunjukSaktiState.findUnique.data ? ( bg={colors['white-1']}
<Paper key={telunjukSaktiState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}> p="lg"
<Stack gap={"xs"}> radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Pelayanan Telunjuk Sakti Desa
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box> <Box>
<Text fw={"bold"} fz={"lg"}>Nama</Text> <Text fz="lg" fw="bold">
<Text fz={"lg"}>{telunjukSaktiState.findUnique.data?.name}</Text> Nama
</Text>
<Text fz="md" c="dimmed">
{data?.name || '-'}
</Text>
</Box> </Box>
<Box> <Box>
<Text fw={"bold"} fz={"lg"}>Link</Text> <Text fz="lg" fw="bold">
Link
</Text>
{data?.link ? (
<Text <Text
fz="md"
component="a" component="a"
href={telunjukSaktiState.findUnique.data?.link} href={data.link}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
c="blue"
style={{ style={{
display: 'block', display: 'block',
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
whiteSpace: 'nowrap' whiteSpace: 'nowrap',
}} }}
> >
{telunjukSaktiState.findUnique.data?.link} {data.link}
</Text> </Text>
) : (
<Text fz="sm" c="dimmed">
Tidak ada link
</Text>
)}
</Box> </Box>
<Box> <Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text> <Text fz="lg" fw="bold">
<Text fz={"lg"}>{telunjukSaktiState.findUnique.data?.deskripsi}</Text> Deskripsi
</Text>
<Text
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{
__html: data?.deskripsi || '-',
}}
/>
</Box> </Box>
<Flex gap={"xs"} mt={10}>
<Group gap="sm">
<Tooltip label="Hapus Layanan" withArrow position="top">
<Button <Button
color="red"
onClick={() => { onClick={() => {
if (telunjukSaktiState.findUnique.data) { setSelectedId(data.id);
setSelectedId(telunjukSaktiState.findUnique.data.id);
setModalHapus(true); setModalHapus(true);
}
}} }}
disabled={telunjukSaktiState.delete.loading || !telunjukSaktiState.findUnique.data} variant="light"
color={"red"} radius="md"
size="md"
disabled={telunjukSaktiState.delete.loading}
> >
<IconX size={20} /> <IconTrash size={20} />
</Button> </Button>
</Tooltip>
<Tooltip label="Edit Layanan" withArrow position="top">
<Button <Button
onClick={() => { color="green"
if (telunjukSaktiState.findUnique.data) { onClick={() =>
router.push(`/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/${telunjukSaktiState.findUnique.data.id}/edit`); router.push(
`/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/${data.id}/edit`
)
} }
}} variant="light"
disabled={!telunjukSaktiState.findUnique.data} radius="md"
color={"green"} size="md"
> >
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Group>
</Stack> </Stack>
</Paper> </Paper>
) : null}
</Stack> </Stack>
</Paper> </Paper>
@@ -113,7 +170,7 @@ function DetailPelayananTelunjukSakti() {
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus berita ini?' text="Apakah Anda yakin ingin menghapus layanan ini?"
/> />
</Box> </Box>
); );

View File

@@ -1,64 +1,117 @@
'use client' 'use client';
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa'; import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } 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 { toast } from 'react-toastify';
function CreatePelayananTelunjukDesa() { function CreatePelayananTelunjukDesa() {
const stateTelunjukDesa = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa) const stateTelunjukDesa = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa);
const router = useRouter() const router = useRouter();
const resetForm = () => { const resetForm = () => {
stateTelunjukDesa.create.form = { stateTelunjukDesa.create.form = {
name: "", name: '',
deskripsi: "", deskripsi: '',
link: "", link: '',
} };
} };
const handleSubmit = async () => { const handleSubmit = async () => {
await stateTelunjukDesa.create.create() try {
resetForm() await stateTelunjukDesa.create.create();
router.push("/admin/desa/layanan/pelayanan_telunjuk_sakti_desa") resetForm();
toast.success('Data pelayanan telunjuk sakti berhasil ditambahkan');
router.push('/admin/desa/layanan/pelayanan_telunjuk_sakti_desa');
} catch (error) {
console.error('Error create pelayanan telunjuk sakti:', error);
toast.error('Terjadi kesalahan saat menambahkan data');
} }
};
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header */}
<Button variant="subtle" onClick={() => router.back()}> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <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> </Button>
</Box> </Tooltip>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}> <Title order={4} ml="sm" c="dark">
<Stack gap={"xs"}> Tambah Pelayanan Telunjuk Sakti Desa
<Title order={3}>Create Pelayanan Telunjuk Sakti Desa</Title> </Title>
</Group>
{/* Form */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Nama */}
<TextInput <TextInput
value={stateTelunjukDesa.create.form.name} value={stateTelunjukDesa.create.form.name}
onChange={(val) => { onChange={(val) => {
stateTelunjukDesa.create.form.name = val.target.value; stateTelunjukDesa.create.form.name = val.target.value;
}} }}
label={<Text fz={"sm"} fw={"bold"}>Nama Pelayanan Telunjuk Sakti Desa</Text>} label={<Text fz="sm" fw="bold">Nama Pelayanan</Text>}
placeholder="masukkan nama pelayanan telunjuk sakti desa" placeholder="Masukkan nama pelayanan telunjuk sakti desa"
required
/> />
{/* Deskripsi */}
<TextInput <TextInput
value={stateTelunjukDesa.create.form.deskripsi} value={stateTelunjukDesa.create.form.deskripsi}
onChange={(val) => { onChange={(val) => {
stateTelunjukDesa.create.form.deskripsi = val.target.value; stateTelunjukDesa.create.form.deskripsi = val.target.value;
}} }}
label={<Text fz={"sm"} fw={"bold"}>Tautan Link</Text>} label={<Text fz="sm" fw="bold">Deskripsi</Text>}
placeholder="masukkan tautan link" placeholder="Masukkan deskripsi pelayanan"
/> />
{/* Link */}
<TextInput <TextInput
value={stateTelunjukDesa.create.form.link} value={stateTelunjukDesa.create.form.link}
onChange={(val) => { onChange={(val) => {
stateTelunjukDesa.create.form.link = val.target.value; stateTelunjukDesa.create.form.link = val.target.value;
}} }}
label={<Text fz={"sm"} fw={"bold"}>Link</Text>} label={<Text fz="sm" fw="bold">Link</Text>}
placeholder="masukkan link" placeholder="Masukkan link pelayanan"
/> />
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
{/* Tombol Simpan */}
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

@@ -1,13 +1,187 @@
// /* eslint-disable react-hooks/exhaustive-deps */
// 'use client'
// import colors from '@/con/colors';
// import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
// import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
// import { useRouter } from 'next/navigation';
// import { useEffect, useMemo, useState } from 'react';
// import { useProxy } from 'valtio/utils';
// import HeaderSearch from '../../../_com/header';
// import JudulList from '../../../_com/judulList';
// import stateLayananDesa from '../../../_state/desa/layananDesa';
// function PelayananTelunjukSakti() {
// const [search, setSearch] = useState("");
// return (
// <Box>
// <HeaderSearch
// title='Posisi Organisasi'
// placeholder='pencarian'
// searchIcon={<IconSearch size={20} />}
// value={search}
// onChange={(e) => setSearch(e.currentTarget.value)}
// />
// <ListPelayananTelunjukSakti search={search} />
// </Box>
// );
// }
// function ListPelayananTelunjukSakti({ search }: { search: string }) {
// const telunjukSaktiState = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa)
// const router = useRouter()
// const {
// data,
// page,
// totalPages,
// loading,
// load,
// } = telunjukSaktiState.findMany;
// useEffect(() => {
// load(page, 10)
// }, [])
// const filteredData = useMemo(() => {
// if (!data) return [];
// return data.filter(item => {
// const keyword = search.toLowerCase();
// return (
// item.name?.toLowerCase().includes(keyword) ||
// item.link?.toLowerCase().includes(keyword) ||
// item.deskripsi?.toLowerCase().includes(keyword)
// );
// })
// .sort((a, b) => a.posisi?.hierarki - b.posisi?.hierarki);
// }, [data, search]);
// if (loading || !data) {
// return (
// <Stack py={10}>
// <Skeleton height={300} />
// </Stack>
// );
// }
// if (data.length === 0) {
// return (
// <Box py={10}>
// <Paper bg={colors['white-1']} p={'md'}>
// <JudulList
// title='List Pelayanan Telunjuk Sakti Desa'
// href='/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/create'
// />
// <Table striped withTableBorder withRowBorders>
// <TableThead>
// <TableTr>
// <TableTh>Nama</TableTh>
// <TableTh>Link</TableTh>
// <TableTh>Detail</TableTh>
// </TableTr>
// </TableThead>
// <TableTbody>
// <TableTr>
// <TableTd colSpan={3}>
// <Text fz={"sm"} color="gray.5">
// Tidak ada data
// </Text>
// </TableTd>
// </TableTr>
// </TableTbody>
// </Table>
// </Paper>
// </Box>
// );
// }
// return (
// <Box py={10}>
// <Paper bg={colors['white-1']} p={'md'}>
// <JudulList
// title='List Pelayanan Telunjuk Sakti Desa'
// href='/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/create'
// />
// <Table striped withTableBorder withRowBorders>
// <TableThead>
// <TableTr>
// <TableTh>Nama</TableTh>
// <TableTh>Link</TableTh>
// <TableTh>Detail</TableTh>
// </TableTr>
// </TableThead>
// <TableTbody>
// {filteredData.map((item) => (
// <TableTr key={item.id}>
// <TableTd>
// <Box w={100}>
// <Text truncate="end" lineClamp={1} fz={"sm"} dangerouslySetInnerHTML={{ __html: item.name }} />
// </Box>
// </TableTd>
// <TableTd>
// <Box w={100}>
// <a href={item.link} target="_blank" rel="noopener noreferrer">
// <Text dangerouslySetInnerHTML={{ __html: item.deskripsi }} truncate="end" fz={"sm"} />
// </a>
// </Box>
// </TableTd>
// <TableTd>
// <Text>
// <Button onClick={() => router.push(`/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/${item.id}`)}>
// <IconDeviceImac size={20} />
// </Button>
// </Text>
// </TableTd>
// </TableTr>
// ))}
// </TableTbody>
// </Table>
// </Paper>
// <Center>
// <Pagination
// value={page}
// onChange={(newPage) => {
// load(newPage, 10);
// window.scrollTo(0, 0);
// }}
// total={totalPages}
// mt="md"
// mb="md"
// />
// </Center>
// </Box>
// );
// }
// export default PelayananTelunjukSakti;
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import {
import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip,
} from '@mantine/core';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import stateLayananDesa from '../../../_state/desa/layananDesa'; import stateLayananDesa from '../../../_state/desa/layananDesa';
function PelayananTelunjukSakti() { function PelayananTelunjukSakti() {
@@ -15,8 +189,8 @@ function PelayananTelunjukSakti() {
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Posisi Organisasi' title="Pelayanan Telunjuk Sakti"
placeholder='pencarian' placeholder="Cari layanan..."
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -27,125 +201,113 @@ function PelayananTelunjukSakti() {
} }
function ListPelayananTelunjukSakti({ search }: { search: string }) { function ListPelayananTelunjukSakti({ search }: { search: string }) {
const telunjukSaktiState = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa) const telunjukSaktiState = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa);
const router = useRouter() const router = useRouter();
const { const { data, page, totalPages, loading, load } = telunjukSaktiState.findMany;
data,
page,
totalPages,
loading,
load,
} = telunjukSaktiState.findMany;
useEffect(() => { useEffect(() => {
load(page, 10) load(page, 10, search);
}, []) }, [page, search]);
const filteredData = useMemo(() => { const filteredData = data || [];
if (!data) return [];
return data.filter(item => {
const keyword = search.toLowerCase();
return (
item.name?.toLowerCase().includes(keyword) ||
item.link?.toLowerCase().includes(keyword) ||
item.deskripsi?.toLowerCase().includes(keyword)
);
})
.sort((a, b) => a.posisi?.hierarki - b.posisi?.hierarki);
}, [data, search]);
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton height={300} /> <Skeleton height={400} radius="md" />
</Stack> </Stack>
); );
} }
if (data.length === 0) {
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<JudulList <Group justify="space-between" mb="md">
title='List Pelayanan Telunjuk Sakti Desa' <Title order={4}>Daftar Pelayanan Telunjuk Sakti</Title>
href='/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/create' <Tooltip label="Tambah Layanan" withArrow>
/> <Button
<Table striped withTableBorder withRowBorders> leftSection={<IconPlus size={18} />}
<TableThead> color="blue"
<TableTr> variant="light"
<TableTh>Nama</TableTh> onClick={() =>
<TableTh>Link</TableTh> router.push('/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/create')
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd colSpan={3}>
<Text fz={"sm"} color="gray.5">
Tidak ada data
</Text>
</TableTd>
</TableTr>
</TableTbody>
</Table>
</Paper>
</Box>
);
} }
>
return ( Tambah Baru
<Box py={10}> </Button>
<Paper bg={colors['white-1']} p={'md'}> </Tooltip>
<JudulList </Group>
title='List Pelayanan Telunjuk Sakti Desa' <Box style={{ overflowX: 'auto' }}>
href='/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/create' <Table highlightOnHover style={{ minWidth: '700px' }}>
/>
<Table striped withTableBorder withRowBorders>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Nama</TableTh> <TableTh style={{ width: '30%' }}>Nama</TableTh>
<TableTh>Link</TableTh> <TableTh style={{ width: '40%' }}>Link</TableTh>
<TableTh>Detail</TableTh> <TableTh style={{ width: '30%' }}>Detail</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item) => ( {filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Box w={100}> <Box w={200}>
<Text truncate="end" lineClamp={1} fz={"sm"} dangerouslySetInnerHTML={{ __html: item.name }} /> <Text fw={500} truncate="end" lineClamp={1}>
</Box> {item.name}
</Text></Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={100}> <Box w={200}>
<a href={item.link} target="_blank" rel="noopener noreferrer"> <a href={item.link} target="_blank" rel="noopener noreferrer">
<Text dangerouslySetInnerHTML={{ __html: item.deskripsi }} truncate="end" fz={"sm"} /> <Text lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} truncate="end" fz={"sm"} />
</a> </a>
</Box> </Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Text> <Button
<Button onClick={() => router.push(`/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/${item.id}`)}> variant="light"
<IconDeviceImac size={20} /> color="blue"
onClick={() =>
router.push(
`/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/${item.id}`
)
}
>
<IconDeviceImacCog size={20} />
<Text ml={5}>Detail</Text>
</Button> </Button>
</Text>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} ))
) : (
<TableTr>
<TableTd colSpan={3}>
<Center py={20}>
<Text color="dimmed">
Tidak ada data layanan yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody> </TableTbody>
</Table> </Table>
</Box>
</Paper> </Paper>
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => { onChange={(newPage) => {
load(newPage, 10); load(newPage, 10, search);
window.scrollTo(0, 0); window.scrollTo({ top: 0, behavior: 'smooth' });
}} }}
total={totalPages} total={totalPages}
mt="md" mt="md"
mb="md" mb="md"
color="blue"
radius="md"
/> />
</Center> </Center>
</Box> </Box>
@@ -153,3 +315,4 @@ function ListPelayananTelunjukSakti({ search }: { search: string }) {
} }
export default PelayananTelunjukSakti; export default PelayananTelunjukSakti;

View File

@@ -4,7 +4,18 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import penghargaanState from '@/app/admin/(dashboard)/_state/desa/penghargaan'; import penghargaanState from '@/app/admin/(dashboard)/_state/desa/penghargaan';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Image,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
@@ -84,87 +95,104 @@ function EditPenghargaan() {
} }
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Tombol Back + Title */}
<Button variant="subtle" onClick={() => router.back()}> <Group mb="md">
<IconArrowBack color={colors["blue-button"]} size={30} /> <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> </Button>
</Box> </Tooltip>
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}> <Title order={4} ml="sm" c="dark">
<Stack gap={"xs"}> Edit Penghargaan
<Title order={3}>Edit Penghargaan</Title> </Title>
</Group>
{/* Card Form */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Input Judul */}
<TextInput <TextInput
label="Judul"
placeholder="Masukkan judul penghargaan"
value={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>} required
placeholder="masukkan judul"
/> />
{/* Input Juara */}
<TextInput <TextInput
label="Juara"
placeholder="Masukkan juara"
value={formData.juara} value={formData.juara}
onChange={(e) => setFormData({ ...formData, juara: e.target.value })} onChange={(e) => setFormData({ ...formData, juara: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Juara</Text>} required
placeholder="masukkan juara"
/> />
{/* Upload Gambar */}
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text> <Text fw="bold" fz="sm" mb={6}>
<Box> Gambar Penghargaan
</Text>
<Dropzone <Dropzone
onDrop={(files) => { onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama const selectedFile = files[0];
if (selectedFile) { if (selectedFile) {
setFile(selectedFile); setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview setPreviewImage(URL.createObjectURL(selectedFile));
} }
}} }}
onReject={() => toast.error('File tidak valid.')} onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2} // Maks 5MB maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }} accept={{ 'image/*': [] }}
radius="md"
p="xl"
> >
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}> <Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept> <Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /> <IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
</Dropzone.Accept> </Dropzone.Accept>
<Dropzone.Reject> <Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} /> <IconX size={48} color="red" stroke={1.5} />
</Dropzone.Reject> </Dropzone.Reject>
<Dropzone.Idle> <Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} /> <IconPhoto size={48} color="#868e96" stroke={1.5} />
</Dropzone.Idle> </Dropzone.Idle>
<Stack gap="xs" align="center">
<div> <Text size="md" fw={500}>
<Text size="xl" inline> Seret gambar atau klik untuk memilih file
Drag gambar ke sini atau klik untuk pilih file
</Text> </Text>
<Text size="sm" c="dimmed" inline mt={7}> <Text size="sm" c="dimmed">
Maksimal 5MB dan harus format gambar Maksimal 5MB, format gambar wajib
</Text> </Text>
</div> </Stack>
</Group> </Group>
</Dropzone> </Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && ( {previewImage && (
<Box mt="sm"> <Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
<Image <Image
src={previewImage} src={previewImage}
alt="Preview" alt="Preview Gambar"
style={{ radius="md"
maxWidth: '100%', style={{ maxHeight: 220, objectFit: 'contain', border: `1px solid ${colors['blue-button']}` }}
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/> />
</Box> </Box>
)} )}
</Box>
</Box> </Box>
{/* Deskripsi */}
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text> <Text fz="sm" fw="bold" mb={6}>
Deskripsi
</Text>
<EditEditor <EditEditor
value={formData.deskripsi} value={formData.deskripsi}
onChange={(htmlContent) => { onChange={(htmlContent) => {
@@ -174,7 +202,21 @@ function EditPenghargaan() {
/> />
</Box> </Box>
<Button onClick={handleSubmit}>Edit Penghargaan</Button> {/* Tombol Simpan */}
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

@@ -4,105 +4,166 @@ import penghargaanState from '../../../_state/desa/penghargaan';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import {
Box,
Button,
Group,
Image,
Paper,
Skeleton,
Stack,
Text,
Tooltip,
} from '@mantine/core';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { IconArrowBack, IconX, IconEdit } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailPenghargaan() { function DetailPenghargaan() {
const statePenghargaan = useProxy(penghargaanState) const statePenghargaan = useProxy(penghargaanState);
const [modalHapus, setModalHapus] = useState(false); const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null); const [selectedId, setSelectedId] = useState<string | null>(null);
const router = useRouter() const router = useRouter();
const params = useParams() const params = useParams();
useShallowEffect(() => { useShallowEffect(() => {
statePenghargaan.findUnique.load(params?.id as string) statePenghargaan.findUnique.load(params?.id as string);
}, []) }, []);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
statePenghargaan.delete.byId(selectedId) statePenghargaan.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/desa/penghargaan") router.push('/admin/desa/penghargaan');
}
} }
};
if (!statePenghargaan.findUnique.data) { if (!statePenghargaan.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) );
} }
const data = statePenghargaan.findUnique.data;
return ( return (
<Box> <Box py={10}>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Penghargaan</Text>
{statePenghargaan.findUnique.data ? (
<Paper key={statePenghargaan.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
<Image w={{ base: 400, md: 400, lg: 400 }} src={statePenghargaan.findUnique.data?.image?.link} alt="gambar" />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Judul</Text>
<Text fz={"lg"}>{statePenghargaan.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Juara</Text>
<Text fz={"lg"}>{statePenghargaan.findUnique.data?.juara}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: statePenghargaan.findUnique.data?.deskripsi }} />
</Box>
<Flex gap={"xs"} mt={10}>
<Button <Button
onClick={() => { variant="subtle"
if (statePenghargaan.findUnique.data) { onClick={() => router.back()}
setSelectedId(statePenghargaan.findUnique.data.id); leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
setModalHapus(true); mb={15}
}
}}
disabled={statePenghargaan.delete.loading || !statePenghargaan.findUnique.data}
color={"red"}
> >
<IconX size={20} /> Kembali
</Button> </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 Penghargaan
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz="lg" fw="bold">
Gambar
</Text>
{data.image?.link ? (
<Image
src={data.image.link}
alt={data.name || 'Gambar Penghargaan'}
w={200}
h={200}
radius="md"
fit="cover"
/>
) : (
<Text fz="sm" c="dimmed">
Tidak ada gambar
</Text>
)}
</Box>
<Box>
<Text fz="lg" fw="bold">
Judul
</Text>
<Text fz="md" c="dimmed">
{data.name || '-'}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">
Juara
</Text>
<Text fz="md" c="dimmed">
{data.juara || '-'}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">
Deskripsi
</Text>
<Text
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
/>
</Box>
<Group gap="sm" mt={10}>
<Tooltip label="Hapus Penghargaan" withArrow position="top">
<Button <Button
color="red"
onClick={() => { onClick={() => {
if (statePenghargaan.findUnique.data) { setSelectedId(data.id);
router.push(`/admin/desa/penghargaan/${statePenghargaan.findUnique.data.id}/edit`); setModalHapus(true);
}
}} }}
disabled={!statePenghargaan.findUnique.data} variant="light"
color={"green"} radius="md"
size="md"
>
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Penghargaan" withArrow position="top">
<Button
color="green"
onClick={() =>
router.push(`/admin/desa/penghargaan/${data.id}/edit`)
}
variant="light"
radius="md"
size="md"
> >
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Group>
</Stack> </Stack>
</Paper> </Paper>
) : null}
</Stack> </Stack>
</Paper> </Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus penghargaan ini?' text="Apakah Anda yakin ingin menghapus penghargaan ini?"
/> />
</Box> </Box>
); );

View File

@@ -1,7 +1,18 @@
'use client' 'use client';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Image,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@@ -11,74 +22,88 @@ import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor'; import CreateEditor from '../../../_com/createEditor';
import penghargaanState from '../../../_state/desa/penghargaan'; import penghargaanState from '../../../_state/desa/penghargaan';
function CreatePenghargaan() { function CreatePenghargaan() {
const statePenghargaan = useProxy(penghargaanState) const statePenghargaan = useProxy(penghargaanState);
const [previewImage, setPreviewImage] = useState<string | null>(null); const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const router = useRouter() const router = useRouter();
const resetForm = () => { const resetForm = () => {
statePenghargaan.create.form = { statePenghargaan.create.form = {
name: "", name: '',
juara: "", juara: '',
deskripsi: "", deskripsi: '',
imageId: "", imageId: '',
} };
setPreviewImage(null) setPreviewImage(null);
setFile(null) setFile(null);
} };
const handleSubmit = async () => { const handleSubmit = async () => {
if (!file) { if (!file) {
return toast.error("Silahkan pilih file gambar terlebih dahulu") return toast.warn('Silakan pilih file gambar terlebih dahulu');
} }
const res = await ApiFetch.api.fileStorage.create.post({ const res = await ApiFetch.api.fileStorage.create.post({
file: file, file,
name: file.name name: file.name,
}) });
const uploaded = res.data?.data;
const uploaded = res.data?.data
if (!uploaded?.id) { if (!uploaded?.id) {
return toast.error("Gagal upload gambar") return toast.error('Gagal mengunggah gambar, silakan coba lagi');
} }
statePenghargaan.create.form.imageId = uploaded.id statePenghargaan.create.form.imageId = uploaded.id;
await statePenghargaan.create.create() await statePenghargaan.create.create();
resetForm() resetForm();
router.push("/admin/desa/penghargaan") router.push('/admin/desa/penghargaan');
};
}
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header */}
<Button variant="subtle" onClick={() => router.back()}> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <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> </Button>
</Box> </Tooltip>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}> <Title order={4} ml="sm" c="dark">
<Stack gap={"xs"}> Tambah Penghargaan
<Title order={3}>Create Penghargaan</Title> </Title>
</Group>
{/* Form */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
value={statePenghargaan.create.form.name} value={statePenghargaan.create.form.name}
onChange={(val) => { onChange={(val) => (statePenghargaan.create.form.name = val.target.value)}
statePenghargaan.create.form.name = val.target.value; label={<Text fz="sm" fw="bold">Nama Penghargaan</Text>}
}} placeholder="Masukkan nama penghargaan"
label={<Text fz={"sm"} fw={"bold"}>Nama Penghargaan</Text>} required
placeholder="masukkan nama penghargaan"
/> />
<TextInput <TextInput
value={statePenghargaan.create.form.juara} value={statePenghargaan.create.form.juara}
onChange={(val) => { onChange={(val) => (statePenghargaan.create.form.juara = val.target.value)}
statePenghargaan.create.form.juara = val.target.value; label={<Text fz="sm" fw="bold">Juara</Text>}
}} placeholder="Masukkan juara"
label={<Text fz={"sm"} fw={"bold"}>Juara</Text>} required
placeholder="masukkan juara"
/> />
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text> <Text fz="sm" fw="bold" mb={6}>Deskripsi</Text>
<CreateEditor <CreateEditor
value={statePenghargaan.create.form.deskripsi} value={statePenghargaan.create.form.deskripsi}
onChange={(htmlContent) => { onChange={(htmlContent) => {
@@ -86,63 +111,67 @@ function CreatePenghargaan() {
}} }}
/> />
</Box> </Box>
{/* Dropzone Upload */}
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text> <Text fz="sm" fw="bold" mb={6}>Gambar</Text>
<Box>
<Dropzone <Dropzone
onDrop={(files) => { onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama const selectedFile = files[0];
if (selectedFile) { if (selectedFile) {
setFile(selectedFile); setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview setPreviewImage(URL.createObjectURL(selectedFile));
} }
}} }}
onReject={() => toast.error('File tidak valid.')} onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2} // Maks 5MB maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }} accept={{ 'image/*': [] }}
radius="md"
p="xl"
> >
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}> <Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept> <Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /> <IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept> </Dropzone.Accept>
<Dropzone.Reject> <Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} /> <IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject> </Dropzone.Reject>
<Dropzone.Idle> <Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} /> <IconPhoto size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle> </Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group> </Group>
<Text ta="center" mt="sm" size="sm" color="dimmed">
Seret gambar atau klik untuk memilih file (maks 5MB)
</Text>
</Dropzone> </Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && ( {previewImage && (
<Box mt="sm"> <Box mt="sm" style={{ textAlign: 'center' }}>
<Image <Image
src={previewImage} src={previewImage}
alt="Preview" alt="Preview Gambar"
style={{ radius="md"
maxWidth: '100%', style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/> />
</Box> </Box>
)} )}
</Box>
</Box> {/* Button Submit */}
</Box> <Group justify="right">
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button> <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> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

@@ -2,21 +2,38 @@
'use client' 'use client'
import penghargaanState from '@/app/admin/(dashboard)/_state/desa/penghargaan'; import penghargaanState from '@/app/admin/(dashboard)/_state/desa/penghargaan';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Image, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import {
import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip
} from '@mantine/core';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header'; import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
function Penghargaan() { function Penghargaan() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Penghargaan' title="Penghargaan"
placeholder='pencarian' placeholder="Cari nama atau deskripsi..."
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -27,125 +44,114 @@ function Penghargaan() {
} }
function ListPenghargaan({ search }: { search: string }) { function ListPenghargaan({ search }: { search: string }) {
const state = useProxy(penghargaanState) const state = useProxy(penghargaanState);
const router = useRouter() const router = useRouter();
const { const { data, page, totalPages, loading, load } = state.findMany;
data,
page,
totalPages,
loading,
load,
} = state.findMany;
useEffect(() => { useEffect(() => {
load(page, 10) load(page, 10, search);
}, []) }, [page, search]);
const filteredData = useMemo(() => { const filteredData = data || []
if (!data) return [];
return data.filter(item => {
const keyword = search.toLowerCase();
return (
item.name?.toLowerCase().includes(keyword) ||
item.deskripsi?.toLowerCase().includes(keyword)
);
})
}, [data, search]);
// Handle loading state // Loading state
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton height={300} /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
); );
} }
if (data.length === 0) {
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<JudulList <Group justify="space-between" mb="md">
title='List Penghargaan' <Title order={4}>List Penghargaan</Title>
href='/admin/desa/penghargaan/create' <Tooltip label="Tambah Penghargaan" withArrow>
/> <Button
<Table striped withTableBorder withRowBorders> leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/desa/penghargaan/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Nama</TableTh> <TableTh style={{ width: '35%' }}>Nama</TableTh>
<TableTh>Deskripsi</TableTh> <TableTh style={{ width: '35%' }}>Deskripsi</TableTh>
<TableTh>Image</TableTh> <TableTh style={{ width: '30%' }}>Aksi</TableTh>
<TableTh>Detail</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
<TableTr> {filteredData.length > 0 ? (
<TableTd colSpan={4}> filteredData.map((item) => (
<Text ta="center">Tidak ada data</Text>
</TableTd>
</TableTr>
</TableTbody>
</Table>
</Paper>
</Box>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Penghargaan'
href='/admin/desa/penghargaan/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Image</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Box w={100}> <Box w={200}>
<Text lineClamp={1} truncate="end" fz={"sm"}>{item.name}</Text> <Text fw={500} truncate="end" lineClamp={1}>
</Box> {item.name}
</TableTd>
<TableTd>
<Box w={100}>
<Text lineClamp={1} truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</Box>
</TableTd>
<TableTd>
<Image w={100} src={item.image?.link} alt="gambar" />
</TableTd>
<TableTd>
<Text>
<Button onClick={() => router.push(`/admin/desa/penghargaan/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</Text> </Text>
</Box>
</TableTd>
<TableTd>
<Box w={200}>
<Text
truncate="end"
lineClamp={1}
fz="sm"
c="dimmed"
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
/>
</Box>
</TableTd>
<TableTd>
<Button
variant="light"
color="blue"
onClick={() =>
router.push(`/admin/desa/penghargaan/${item.id}`)
}
>
<IconDeviceImac size={20} />
<Text ml={5}>Detail</Text>
</Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} ))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">
Tidak ada data penghargaan yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody> </TableTbody>
</Table> </Table>
</Box>
</Paper> </Paper>
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => { onChange={(newPage) => {
load(newPage, 10); load(newPage, 10, search);
window.scrollTo(0, 0); window.scrollTo({ top: 0, behavior: 'smooth' });
}} }}
total={totalPages} total={totalPages}
mt="md" mt="md"
mb="md" mb="md"
color="blue"
radius="md"
/> />
</Center> </Center>
</Box> </Box>

View File

@@ -1,9 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core'; import { 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 { IconListDetails, IconCategory } from '@tabler/icons-react';
function LayoutTabsLayanan({ children }: { children: React.ReactNode }) { function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
const router = useRouter() const router = useRouter()
@@ -12,16 +13,21 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
{ {
label: "List Pengumuman", label: "List Pengumuman",
value: "listpengumuman", value: "listpengumuman",
href: "/admin/desa/pengumuman/list-pengumuman" href: "/admin/desa/pengumuman/list-pengumuman",
icon: <IconListDetails size={18} stroke={1.8} />,
tooltip: "Lihat semua daftar pengumuman"
}, },
{ {
label: "Kategori Pengumuman", label: "Kategori Pengumuman",
value: "kategoripengumuman", value: "kategoripengumuman",
href: "/admin/desa/pengumuman/kategori-pengumuman" href: "/admin/desa/pengumuman/kategori-pengumuman",
icon: <IconCategory size={18} stroke={1.8} />,
tooltip: "Kelola kategori pengumuman"
}, },
]; ];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value); const currentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => { const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value) const tab = tabs.find(t => t.value === value)
@@ -39,22 +45,57 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
}, [pathname]) }, [pathname])
return ( return (
<Stack> <Stack gap="lg">
<Title order={3}>Pengumuman</Title> <Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Pengumuman</Title>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}> <Tabs
<TabsList p={"xs"} bg={"#BBC8E7FF"}> color={colors['blue-button']}
{tabs.map((e, i) => ( variant='pills'
<TabsTab key={i} value={e.value}>{e.label}</TabsTab> value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
}}
>
{tabs.map((tab, i) => (
<Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))} ))}
</TabsList> </TabsList>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value}> {tabs.map((tab, i) => (
{/* Konten dummy, bisa diganti tergantung routing */} <TabsPanel
<></> key={i}
value={tab.value}
style={{
padding: "1.5rem",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
borderRadius: "1rem",
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
}}
>
{/* Konten dummy, bisa diganti sesuai routing */}
<>{children}</>
</TabsPanel> </TabsPanel>
))} ))}
</Tabs> </Tabs>
{children}
</Stack> </Stack>
); );
} }

View File

@@ -1,8 +1,18 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client';
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman'; import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
TextInput,
Title,
Tooltip,
Text,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -10,9 +20,10 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function EditKategoriPengumuman() { function EditKategoriPengumuman() {
const editState = useProxy(stateDesaPengumuman.category) const editState = useProxy(stateDesaPengumuman.category);
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: editState.update.form.name || '', name: editState.update.form.name || '',
}); });
@@ -23,15 +34,15 @@ function EditKategoriPengumuman() {
if (!id) return; if (!id) return;
try { try {
const data = await editState.update.load(id); // akses langsung, bukan dari proxy const data = await editState.update.load(id);
if (data) { if (data) {
setFormData({ setFormData({
name: data.name || '', name: data.name || '',
}); });
} }
} catch (error) { } catch (error) {
console.error("Error loading kategori Pengumuman:", error); console.error('Error loading kategori Pengumuman:', error);
toast.error("Gagal memuat data kategori Pengumuman"); toast.error('Gagal memuat data kategori Pengumuman');
} }
}; };
@@ -44,6 +55,7 @@ function EditKategoriPengumuman() {
...editState.update.form, ...editState.update.form,
name: formData.name, name: formData.name,
}; };
await editState.update.update(); await editState.update.update();
toast.success('Kategori Pengumuman berhasil diperbarui!'); toast.success('Kategori Pengumuman berhasil diperbarui!');
router.push('/admin/desa/pengumuman/kategori-pengumuman'); router.push('/admin/desa/pengumuman/kategori-pengumuman');
@@ -54,23 +66,62 @@ function EditKategoriPengumuman() {
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header */}
<Button variant="subtle" onClick={() => router.back()}> <Group mb="md">
<IconArrowBack color={colors["blue-button"]} size={30} /> <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> </Button>
</Box> </Tooltip>
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}> <Title order={4} ml="sm" c="dark">
<Stack gap={"xs"}> Edit Kategori Pengumuman
<Title order={3}>Edit Kategori Pengumuman</Title> </Title>
</Group>
{/* Form */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label={
<Text fz="sm" fw="bold">
Nama Kategori Pengumuman
</Text>
}
placeholder="Masukkan nama kategori Pengumuman"
value={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) =>
label={<Text fz={"sm"} fw={"bold"}>Nama Kategori Pengumuman</Text>} setFormData({ ...formData, name: e.target.value })
placeholder="masukkan nama kategori Pengumuman" }
required
/> />
<Button onClick={handleSubmit}>Simpan</Button> <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> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

@@ -1,50 +1,87 @@
'use client' 'use client'
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman'; import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function CreateKategoriPengumuman() { function CreateKategoriPengumuman() {
const createState = useProxy(stateDesaPengumuman.category) const createState = useProxy(stateDesaPengumuman.category);
const router = useRouter(); const router = useRouter();
const resetForm = () => { const resetForm = () => {
createState.create.form = { createState.create.form = {
name: "", name: '',
}; };
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
await createState.create.create(); await createState.create.create();
resetForm(); resetForm();
router.push("/admin/desa/pengumuman/kategori-pengumuman") router.push('/admin/desa/pengumuman/kategori-pengumuman');
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header dengan back button */}
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <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> </Button>
</Box> </Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Kategori Pengumuman
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> {/* Form utama */}
<Stack gap={"xs"}> <Paper
<Title order={4}>Create Kategori Pengumuman</Title> w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Pengumuman</Text>} label={<Text fw="bold" fz="sm">Nama Kategori Pengumuman</Text>}
placeholder='Masukkan nama kategori Pengumuman' placeholder="Masukkan nama kategori pengumuman"
value={createState.create.form.name} value={createState.create.form.name || ''}
onChange={(val) => { onChange={(e) => (createState.create.form.name = e.target.value)}
createState.create.form.name = val.target.value; required
}}
/> />
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button> <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> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -1,25 +1,26 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import {
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react'; Box, Button, Center, Paper, Skeleton, Stack,
Table, TableTbody, TableTd, TableTh, TableThead, TableTr,
Text, Title, Tooltip, Pagination
} from '@mantine/core';
import { IconEdit, IconSearch, IconTrash, IconPlus } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import stateDesaPengumuman from '../../../_state/desa/pengumuman'; import stateDesaPengumuman from '../../../_state/desa/pengumuman';
function KategoriPengumuman() { function KategoriPengumuman() {
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Kategori Pengumuman' title='Kategori Pengumuman'
placeholder='pencarian' placeholder='Cari nama kategori...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -34,69 +35,84 @@ function ListKategoriPengumuman({ search }: { search: string }) {
const router = useRouter(); const router = useRouter();
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null)
const { data, page, totalPages, loading, load } = listDataState.findMany;
useEffect(() => { useEffect(() => {
listDataState.findMany.load() load(1, 10, search)
}, []) }, [search])
const handleDelete = () => { const handleDelete = () => {
if (selectedId) { if (selectedId) {
listDataState.delete.delete(selectedId) listDataState.delete.delete(selectedId)
setModalHapus(false) setModalHapus(false)
setSelectedId(null) setSelectedId(null)
load(page, 10, search)
listDataState.findMany.load()
} }
} }
const filteredData = (listDataState.findMany.data || []).filter(item => { const filteredData = data || []
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword)
);
});
if (!listDataState.findMany.data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) )
} }
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p="md"> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Stack> <Stack>
<JudulList <Box style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 15 }}>
title='List Kategori Pengumuman' <Title order={4}>List Kategori Pengumuman</Title>
href='/admin/desa/pengumuman/kategori-pengumuman/create' <Tooltip label="Tambah Kategori Pengumuman" withArrow>
/> <Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/desa/pengumuman/kategori-pengumuman/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Box>
<Box style={{ overflowX: 'auto' }}> <Box style={{ overflowX: 'auto' }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}> <Table highlightOnHover striped withRowBorders style={{ minWidth: '700px' }}>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>No</TableTh> <TableTh style={{ width: '10%' }}>No</TableTh>
<TableTh>Nama</TableTh> <TableTh style={{ width: '60%' }}>Nama</TableTh>
<TableTh>Edit</TableTh> <TableTh style={{ width: '15%' }}>Edit</TableTh>
<TableTh>Hapus</TableTh> <TableTh style={{ width: '15%' }}>Hapus</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item, index) => ( {filteredData.length > 0 ? (
filteredData.map((item, index) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Box w={100}> <Text fz="sm">{(page - 1) * 10 + index + 1}</Text>
<Text truncate="end" fz={"sm"}>{index + 1}</Text>
</Box>
</TableTd> </TableTd>
<TableTd>{item.name}</TableTd>
<TableTd> <TableTd>
<Button color='green' onClick={() => router.push(`/admin/desa/pengumuman/kategori-pengumuman/${item.id}`)}> <Text truncate lineClamp={1}>{item.name}</Text>
</TableTd>
<TableTd>
<Tooltip label="Edit Kategori Pengumuman" withArrow>
<Button
variant='light'
color='green'
onClick={() => router.push(`/admin/desa/pengumuman/kategori-pengumuman/${item.id}`)}
>
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Tooltip>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Tooltip label="Hapus Kategori Pengumuman" withArrow>
<Button <Button
variant='light'
color='red' color='red'
disabled={listDataState.delete.loading} disabled={listDataState.delete.loading}
onClick={() => { onClick={() => {
@@ -105,16 +121,35 @@ function ListKategoriPengumuman({ search }: { search: string }) {
}}> }}>
<IconTrash size={20} /> <IconTrash size={20} />
</Button> </Button>
</Tooltip>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} ))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text c="dimmed">Tidak ada data kategori pengumuman yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
</Stack> </Stack>
</Paper> </Paper>
{/* Modal Konfirmasi Hapus */} <Center mt="md">
<Pagination
value={page}
onChange={(newPage) => load(newPage, 10, search)}
total={totalPages}
color="blue"
radius="md"
/>
</Center>
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}

View File

@@ -7,12 +7,14 @@ import colors from "@/con/colors";
import { import {
Box, Box,
Button, Button,
Group,
Paper, Paper,
Select, Select,
Stack, Stack,
Text, Text,
TextInput, TextInput,
Title Title,
Tooltip,
} from "@mantine/core"; } from "@mantine/core";
import { IconArrowBack } from "@tabler/icons-react"; import { IconArrowBack } from "@tabler/icons-react";
import { useParams, useRouter } from "next/navigation"; import { useParams, useRouter } from "next/navigation";
@@ -20,34 +22,34 @@ import { useEffect, useState } from "react";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { useProxy } from "valtio/utils"; import { useProxy } from "valtio/utils";
function EditPengumuman() { function EditPengumuman() {
const editState = useProxy(stateDesaPengumuman); const editState = useProxy(stateDesaPengumuman);
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
judul: editState.pengumuman.edit.form.judul || '', judul: editState.pengumuman.edit.form.judul || "",
deskripsi: editState.pengumuman.edit.form.deskripsi || '', deskripsi: editState.pengumuman.edit.form.deskripsi || "",
categoryPengumumanId: editState.pengumuman.edit.form.categoryPengumumanId || '', categoryPengumumanId:
content: editState.pengumuman.edit.form.content || '' editState.pengumuman.edit.form.categoryPengumumanId || "",
content: editState.pengumuman.edit.form.content || "",
}); });
// Load pengumuman by id saat pertama kali // Load pengumuman by id saat pertama kali
useEffect(() => { useEffect(() => {
editState.category.findMany.load() editState.category.findMany.load();
const loadpengumuman = async () => { const loadpengumuman = async () => {
const id = params?.id as string; const id = params?.id as string;
if (!id) return; if (!id) return;
try { try {
const data = await stateDesaPengumuman.pengumuman.edit.load(id); // akses langsung, bukan dari proxy const data = await stateDesaPengumuman.pengumuman.edit.load(id);
if (data) { if (data) {
setFormData({ setFormData({
judul: data.judul || '', judul: data.judul || "",
deskripsi: data.deskripsi || '', deskripsi: data.deskripsi || "",
categoryPengumumanId: data.categoryPengumumanId || '', categoryPengumumanId: data.categoryPengumumanId || "",
content: data.content || '', content: data.content || "",
}); });
} }
} catch (error) { } catch (error) {
@@ -57,21 +59,18 @@ function EditPengumuman() {
}; };
loadpengumuman(); loadpengumuman();
}, [params?.id]); // ✅ hapus editState dari dependency }, [params?.id]);
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
// edit global state with form data // update global state
editState.pengumuman.edit.form = { editState.pengumuman.edit.form = {
...editState.pengumuman.edit.form, ...editState.pengumuman.edit.form,
judul: formData.judul, ...formData,
deskripsi: formData.deskripsi,
content: formData.content,
categoryPengumumanId: formData.categoryPengumumanId || ''
}; };
await editState.pengumuman.edit.update(); await editState.pengumuman.edit.update();
toast.success("pengumuman berhasil diperbarui!"); toast.success("Pengumuman berhasil diperbarui!");
router.push("/admin/desa/pengumuman/list-pengumuman"); router.push("/admin/desa/pengumuman/list-pengumuman");
} catch (error) { } catch (error) {
console.error("Error updating pengumuman:", error); console.error("Error updating pengumuman:", error);
@@ -80,57 +79,97 @@ function EditPengumuman() {
}; };
return ( return (
<Box> <Box px={{ base: "sm", md: "lg" }} py="md">
<Box mb={10}> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()}> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<IconArrowBack color={colors["blue-button"]} size={30} /> <Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors["blue-button"]} size={24} />
</Button> </Button>
</Box> </Tooltip>
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}> <Title order={4} ml="sm" c="dark">
<Stack gap={"xs"}> Edit Pengumuman
<Title order={3}>Edit pengumuman</Title> </Title>
</Group>
<Paper
w={{ base: "100%", md: "50%" }}
bg={colors["white-1"]}
p="lg"
radius="md"
shadow="sm"
style={{ border: "1px solid #e0e0e0" }}
>
<Stack gap="md">
<TextInput <TextInput
label="Judul Pengumuman"
placeholder="Masukkan judul"
value={formData.judul} value={formData.judul}
onChange={(e) => setFormData({ ...formData, judul: e.target.value })} onChange={(e) => setFormData({ ...formData, judul: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>} required
placeholder="masukkan judul"
/> />
<TextInput <TextInput
label="Deskripsi Singkat"
placeholder="Masukkan deskripsi"
value={formData.deskripsi} value={formData.deskripsi}
onChange={(e) => setFormData({ ...formData, deskripsi: e.target.value })} onChange={(e) =>
label={<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>} setFormData({ ...formData, deskripsi: e.target.value })
placeholder="masukkan deskripsi" }
required
/> />
<Box>
<Text fz={"sm"} fw={"bold"}>Konten</Text>
<EditEditor
value={formData.content}
onChange={(htmlContent) => {
setFormData((prev) => ({ ...prev, content: htmlContent }));
editState.pengumuman.edit.form.content = htmlContent;
}}
/>
</Box>
<Select <Select
value={formData.categoryPengumumanId} value={formData.categoryPengumumanId}
onChange={(val) => setFormData({ ...formData, categoryPengumumanId: val || "" })} onChange={(val) =>
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>} setFormData({ ...formData, categoryPengumumanId: val || "" })
placeholder='Pilih kategori' }
label="Kategori"
placeholder="Pilih kategori"
data={ data={
editState.category.findMany.data?.map((v) => ({ editState.category.findMany.data?.map((v) => ({
value: v.id, value: v.id,
label: v.name label: v.name,
})) || [] })) || []
} }
clearable clearable
searchable searchable
required required
error={!formData.categoryPengumumanId ? "Pilih kategori" : undefined} error={
!formData.categoryPengumumanId ? "Pilih kategori" : undefined
}
/> />
<Button onClick={handleSubmit}>Edit pengumuman</Button> <Box>
<Text fz="sm" fw="bold" mb={6}>
Konten Lengkap
</Text>
<EditEditor
value={formData.content}
onChange={(htmlContent) =>
setFormData({ ...formData, content: 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> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

@@ -1,116 +1,163 @@
'use client' 'use client'
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { useProxy } from 'valtio/utils'; import {
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman'; Box,
Button,
Group,
Paper,
Skeleton,
Stack,
Text,
Tooltip,
} from '@mantine/core';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useRouter, useParams } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { useParams } from 'next/navigation';
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
export default function DetailPengumuman() {
function DetailPengumuman() { const pengumumanState = useProxy(stateDesaPengumuman);
const pengumumanState = useProxy(stateDesaPengumuman) const [modalHapus, setModalHapus] = useState(false);
const [modalHapus, setModalHapus] = useState(false) const [selectedId, setSelectedId] = useState<string | null>(null);
const [selectedId, setSelectedId] = useState<string | null>(null) const params = useParams();
const params = useParams() const router = useRouter();
const router = useRouter()
useShallowEffect(() => { useShallowEffect(() => {
pengumumanState.pengumuman.findUnique.load(params?.id as string) pengumumanState.pengumuman.findUnique.load(params?.id as string);
}, []) }, []);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
pengumumanState.pengumuman.delete.byId(selectedId) pengumumanState.pengumuman.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/desa/pengumuman/list-pengumuman") router.push('/admin/desa/pengumuman/list-pengumuman');
}
} }
};
if (!pengumumanState.pengumuman.findUnique.data) { if (!pengumumanState.pengumuman.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={400} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) );
} }
const data = pengumumanState.pengumuman.findUnique.data;
return ( return (
<Box> <Box py={10}>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Pengumuman</Text>
{pengumumanState.pengumuman.findUnique.data ? (
<Paper key={pengumumanState.pengumuman.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Kategori</Text>
<Text fz={"lg"}>{pengumumanState.pengumuman.findUnique.data?.CategoryPengumuman?.name}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Judul</Text>
<Text fz={"lg"}>{pengumumanState.pengumuman.findUnique.data?.judul}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
<Text fz={"lg"}>{pengumumanState.pengumuman.findUnique.data?.deskripsi}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Konten</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: pengumumanState.pengumuman.findUnique.data?.content }} />
</Box>
<Flex gap={"xs"} mt={10}>
<Button <Button
onClick={() => { variant="subtle"
if (pengumumanState.pengumuman.findUnique.data) { onClick={() => router.back()}
setSelectedId(pengumumanState.pengumuman.findUnique.data.id); leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
setModalHapus(true); mb={15}
}
}}
disabled={pengumumanState.pengumuman.delete.loading || !pengumumanState.pengumuman.findUnique.data}
color={"red"}
> >
<IconX size={20} /> Kembali
</Button> </Button>
<Button
onClick={() => { <Paper
if (pengumumanState.pengumuman.findUnique.data) { withBorder
router.push(`/admin/desa/pengumuman/list-pengumuman/${pengumumanState.pengumuman.findUnique.data.id}/edit`); 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 Pengumuman
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz="lg" fw="bold">
Kategori
</Text>
<Text fz="md" c="dimmed">
{data?.CategoryPengumuman?.name || '-'}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">
Judul
</Text>
<Text fz="md" c="dimmed">
{data?.judul || '-'}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">
Deskripsi
</Text>
<Text fz="md" c="dimmed">
{data?.deskripsi || '-'}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">
Konten
</Text>
<Text
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{
__html: data?.content || '-',
}} }}
disabled={!pengumumanState.pengumuman.findUnique.data} />
color={"green"} </Box>
<Group gap="sm">
<Tooltip label="Hapus Pengumuman" withArrow position="top">
<Button
color="red"
onClick={() => {
setSelectedId(data.id);
setModalHapus(true);
}}
variant="light"
radius="md"
size="md"
>
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Pengumuman" withArrow position="top">
<Button
color="green"
onClick={() =>
router.push(
`/admin/desa/pengumuman/list-pengumuman/${data.id}/edit`
)
}
variant="light"
radius="md"
size="md"
> >
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Group>
</Stack> </Stack>
</Paper> </Paper>
) : null}
</Stack> </Stack>
</Paper> </Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus pengumuman ini?' text="Apakah anda yakin ingin menghapus pengumuman ini?"
/> />
</Box> </Box>
); );
} }
export default DetailPengumuman;

View File

@@ -1,79 +1,110 @@
'use client' 'use client';
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman'; import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Select,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function CreatePengumuman() { function CreatePengumuman() {
const pengumumanState = useProxy(stateDesaPengumuman) const pengumumanState = useProxy(stateDesaPengumuman);
const router = useRouter(); const router = useRouter();
useShallowEffect(() => { useShallowEffect(() => {
pengumumanState.category.findMany.load() pengumumanState.category.findMany.load();
}, []) }, []);
const handleSubmit = async () => { const handleSubmit = async () => {
await pengumumanState.pengumuman.create.create() await pengumumanState.pengumuman.create.create();
resetForm() resetForm();
router.push("/admin/desa/pengumuman/list-pengumuman") router.push('/admin/desa/pengumuman/list-pengumuman');
} };
const resetForm = () => { const resetForm = () => {
pengumumanState.pengumuman.create.form = { pengumumanState.pengumuman.create.form = {
judul: "", judul: '',
deskripsi: "", deskripsi: '',
content: "", content: '',
categoryPengumumanId: "", categoryPengumumanId: '',
}; };
}; };
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> return (
<Stack gap={"xs"}> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Title order={4}>Create Pengumuman</Title> {/* 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">
Tambah Pengumuman
</Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Judul */}
<TextInput <TextInput
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>} value={pengumumanState.pengumuman.create.form.judul}
placeholder='Masukkan judul' onChange={(val) => (pengumumanState.pengumuman.create.form.judul = val.target.value)}
onChange={(val) => { label={<Text fz="sm" fw="bold">Judul</Text>}
pengumumanState.pengumuman.create.form.judul = val.target.value placeholder="Masukkan judul pengumuman"
}} required
/> />
{/* Kategori */}
<Select <Select
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>} label={<Text fz="sm" fw="bold">Kategori</Text>}
placeholder='Pilih kategori' placeholder="Pilih kategori"
value={pengumumanState.pengumuman.create.form.categoryPengumumanId || ""}
onChange={(val) => {
pengumumanState.pengumuman.create.form.categoryPengumumanId = val ?? "";
}}
data={pengumumanState.category.findMany.data?.map((item) => ({ data={pengumumanState.category.findMany.data?.map((item) => ({
label: item.name, label: item.name,
value: item.id, value: item.id,
}))} }))}
onChange={(val) => {
const selected = pengumumanState.category.findMany.data?.find((item) => item.id === val);
if (selected) {
pengumumanState.pengumuman.create.form.categoryPengumumanId = selected.id;
}
}}
searchable searchable
nothingFoundMessage="Tidak ditemukan" nothingFoundMessage="Tidak ditemukan"
/> />
{/* Deskripsi Singkat */}
<TextInput <TextInput
label={<Text fw={"bold"} fz={"sm"}>Deskripsi Singkat</Text>} value={pengumumanState.pengumuman.create.form.deskripsi}
placeholder='Masukkan deskripsi singkat' onChange={(val) => (pengumumanState.pengumuman.create.form.deskripsi = val.target.value)}
onChange={(val) => { label={<Text fz="sm" fw="bold">Deskripsi Singkat</Text>}
pengumumanState.pengumuman.create.form.deskripsi = val.target.value placeholder="Masukkan deskripsi singkat"
}} required
/> />
{/* Konten Editor */}
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text> <Text fz="sm" fw="bold" mb={6}>
Konten Lengkap
</Text>
<CreateEditor <CreateEditor
value={pengumumanState.pengumuman.create.form.content} value={pengumumanState.pengumuman.create.form.content}
onChange={(htmlContent) => { onChange={(htmlContent) => {
@@ -82,8 +113,20 @@ function CreatePengumuman() {
/> />
</Box> </Box>
<Group> {/* Tombol Submit */}
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button> <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> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -1,6 +1,25 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Grid, GridCol, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import {
Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@@ -9,14 +28,13 @@ import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import stateDesaPengumuman from '../../../_state/desa/pengumuman'; import stateDesaPengumuman from '../../../_state/desa/pengumuman';
function Pengumuman() { function Pengumuman() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title='List Pengumuman' title="Pengumuman Desa"
placeholder='pencarian' placeholder="Cari judul atau kategori..."
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -27,86 +45,107 @@ function Pengumuman() {
} }
function ListPengumuman({ search }: { search: string }) { function ListPengumuman({ search }: { search: string }) {
const pengumumanState = useProxy(stateDesaPengumuman) const pengumumanState = useProxy(stateDesaPengumuman);
const router = useRouter() const router = useRouter();
const {
data, const { data, page, totalPages, loading, load } = pengumumanState.pengumuman.findMany;
page,
totalPages,
loading,
load,
} = pengumumanState.pengumuman.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search) load(page, 10, search);
}, [page, search]) }, [page, search]);
const filteredData = (data || []) const filteredData = data || [];
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
) );
} }
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Stack> <Group justify="space-between" mb="md">
<Grid> <Title order={4}>Daftar Pengumuman</Title>
<GridCol span={{ base: 12, md: 11 }}> <Tooltip label="Tambah Pengumuman" withArrow>
<Text fz={"xl"} fw={"bold"}>List Pengumuman</Text> <Button
</GridCol> leftSection={<IconCircleDashedPlus size={18} />}
<GridCol span={{ base: 12, md: 1 }}> color="blue"
<Button onClick={() => router.push("/admin/desa/pengumuman/list-pengumuman/create")} bg={colors['blue-button']}> variant="light"
<IconCircleDashedPlus size={25} /> onClick={() => router.push('/admin/desa/pengumuman/list-pengumuman/create')}
>
Tambah Baru
</Button> </Button>
</GridCol> </Tooltip>
</Grid> </Group>
<Box style={{ overflowX: "auto" }}> <Box style={{ overflowX: 'auto' }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}> <Table highlightOnHover style={{ minWidth: '700px' }}>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh w={250}>Judul</TableTh> <TableTh style={{ width: '40%' }}>Judul</TableTh>
<TableTh w={250}>Kategori</TableTh> <TableTh style={{ width: '30%' }}>Kategori</TableTh>
<TableTh w={200}>Detail</TableTh> <TableTh style={{ width: '20%' }}>Detail</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item) => ( {filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Box w={100}> <Text fw={500} truncate="end" lineClamp={1}>
<Text truncate="end" fz={"sm"}>{item.judul}</Text> {item.judul}
</Box> </Text>
</TableTd> </TableTd>
<TableTd >{item.CategoryPengumuman?.name}</TableTd>
<TableTd> <TableTd>
<Button bg={"green"} onClick={() => router.push(`/admin/desa/pengumuman/list-pengumuman/${item.id}`)}> <Text fz="sm" c="dimmed">
<IconDeviceImacCog size={25} /> {item.CategoryPengumuman?.name || '-'}
</Text>
</TableTd>
<TableTd>
<Button
variant="light"
color="blue"
onClick={() =>
router.push(`/admin/desa/pengumuman/list-pengumuman/${item.id}`)
}
>
<IconDeviceImacCog size={20} />
<Text ml={5}>Detail</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} ))
) : (
<TableTr>
<TableTd colSpan={3}>
<Center py={20}>
<Text color="dimmed">Tidak ada pengumuman yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
</Stack>
</Paper> </Paper>
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => load(newPage)} onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages} total={totalPages}
mt="md" mt="md"
mb="md" mb="md"
color="blue"
radius="md"
/> />
</Center> </Center>
</Box> </Box>
) );
} }
export default Pengumuman; export default Pengumuman;

View File

@@ -1,7 +1,8 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core'; import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { IconCategory, IconListCheck } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
@@ -12,17 +13,21 @@ function LayoutTabsPotensi({ children }: { children: React.ReactNode }) {
{ {
label: "List Potensi", label: "List Potensi",
value: "list_potensi", value: "list_potensi",
href: "/admin/desa/potensi/list-potensi" href: "/admin/desa/potensi/list-potensi",
icon: <IconListCheck size={18} stroke={1.8} />,
tooltip: "Lihat semua potensi desa"
}, },
{ {
label: "Kategori Potensi", label: "Kategori Potensi",
value: "kategori_potensi", value: "kategori_potensi",
href: "/admin/desa/potensi/kategori-potensi" href: "/admin/desa/potensi/kategori-potensi",
icon: <IconCategory size={18} stroke={1.8} />,
tooltip: "Kelola kategori potensi"
}, },
]; ];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value); const currentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => { const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value) const tab = tabs.find(t => t.value === value)
@@ -40,22 +45,57 @@ function LayoutTabsPotensi({ children }: { children: React.ReactNode }) {
}, [pathname]) }, [pathname])
return ( return (
<Stack> <Stack gap="lg">
<Title order={3}>Potensi</Title> <Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Potensi</Title>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}> <Tabs
<TabsList p={"xs"} bg={"#BBC8E7FF"}> color={colors['blue-button']}
{tabs.map((e, i) => ( variant='pills'
<TabsTab key={i} value={e.value}>{e.label}</TabsTab> value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
}}
>
{tabs.map((tab, i) => (
<Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))} ))}
</TabsList> </TabsList>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value}> {tabs.map((tab, i) => (
{/* Konten dummy, bisa diganti tergantung routing */} <TabsPanel
<></> key={i}
value={tab.value}
style={{
padding: "1.5rem",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
borderRadius: "1rem",
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
}}
>
{/* Konten dummy, bisa diganti sesuai routing */}
<>{children}</>
</TabsPanel> </TabsPanel>
))} ))}
</Tabs> </Tabs>
{children}
</Stack> </Stack>
); );
} }

View File

@@ -1,8 +1,17 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client';
import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi'; import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
TextInput,
Title,
Tooltip
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -10,9 +19,10 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function EditKategoriPotensi() { function EditKategoriPotensi() {
const editState = useProxy(potensiDesaState.kategoriPotensi) const editState = useProxy(potensiDesaState.kategoriPotensi);
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
nama: editState.update.form.nama || '', nama: editState.update.form.nama || '',
}); });
@@ -23,15 +33,15 @@ function EditKategoriPotensi() {
if (!id) return; if (!id) return;
try { try {
const data = await editState.update.load(id); // akses langsung, bukan dari proxy const data = await editState.update.load(id);
if (data) { if (data) {
setFormData({ setFormData({
nama: data.nama || '', nama: data.nama || '',
}); });
} }
} catch (error) { } catch (error) {
console.error("Error loading kategori potensi:", error); console.error('Error loading kategori potensi:', error);
toast.error("Gagal memuat data kategori potensi"); toast.error('Gagal memuat data kategori potensi');
} }
}; };
@@ -44,6 +54,7 @@ function EditKategoriPotensi() {
...editState.update.form, ...editState.update.form,
nama: formData.nama, nama: formData.nama,
}; };
await editState.update.update(); await editState.update.update();
toast.success('Kategori Potensi berhasil diperbarui!'); toast.success('Kategori Potensi berhasil diperbarui!');
router.push('/admin/desa/potensi/kategori-potensi'); router.push('/admin/desa/potensi/kategori-potensi');
@@ -54,23 +65,49 @@ function EditKategoriPotensi() {
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()}> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<IconArrowBack color={colors["blue-button"]} size={30} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
</Box> </Tooltip>
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}> <Title order={4} ml="sm" c="dark">
<Stack gap={"xs"}> Edit Kategori Potensi
<Title order={3}>Edit Kategori Potensi</Title> </Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label="Nama Kategori Potensi"
placeholder="Masukkan nama kategori potensi"
value={formData.nama} value={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })} onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Nama Kategori Potensi</Text>} required
placeholder="masukkan nama kategori potensi"
/> />
<Button onClick={handleSubmit}>Simpan</Button> <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> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

@@ -1,50 +1,87 @@
'use client' 'use client';
import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi'; import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function CreateKategoriPotensi() { function CreateKategoriPotensi() {
const createState = useProxy(potensiDesaState.kategoriPotensi) const createState = useProxy(potensiDesaState.kategoriPotensi);
const router = useRouter(); const router = useRouter();
const resetForm = () => { const resetForm = () => {
createState.create.form = { createState.create.form = {
nama: "", nama: '',
}; };
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
await createState.create.create(); await createState.create.create();
resetForm(); resetForm();
router.push("/admin/desa/potensi/kategori-potensi") router.push('/admin/desa/potensi/kategori-potensi');
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header dengan back button */}
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <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> </Button>
</Box> </Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Kategori Potensi
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> {/* Form utama */}
<Stack gap={"xs"}> <Paper
<Title order={4}>Create Kategori Potensi</Title> w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Potensi</Text>} label={<Text fw="bold" fz="sm">Nama Kategori Potensi</Text>}
placeholder='Masukkan nama kategori Potensi' placeholder="Masukkan nama kategori potensi"
value={createState.create.form.nama} value={createState.create.form.nama || ''}
onChange={(val) => { onChange={(e) => (createState.create.form.nama = e.target.value)}
createState.create.form.nama = val.target.value; required
}}
/> />
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button> <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> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -1,17 +1,14 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import { Box, Button, Center, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip, Pagination } from '@mantine/core';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react'; import { IconEdit, IconSearch, IconTrash, IconPlus } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import potensiDesaState from '../../../_state/desa/potensi'; import potensiDesaState from '../../../_state/desa/potensi';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function KategoriPotensi() { function KategoriPotensi() {
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
@@ -19,7 +16,7 @@ function KategoriPotensi() {
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Kategori Potensi' title='Kategori Potensi'
placeholder='pencarian' placeholder='Cari nama kategori...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -34,69 +31,77 @@ function ListKategoriPotensi({ search }: { search: string }) {
const router = useRouter(); const router = useRouter();
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null)
const { data, page, totalPages, loading, load } = listDataState.findMany;
useEffect(() => { useEffect(() => {
listDataState.findMany.load() load(1, 10, search)
}, []) }, [search])
const handleDelete = () => { const handleDelete = () => {
if (selectedId) { if (selectedId) {
listDataState.delete.delete(selectedId) listDataState.delete.delete(selectedId)
setModalHapus(false) setModalHapus(false)
setSelectedId(null) setSelectedId(null)
load(page, 10, search)
listDataState.findMany.load()
} }
} }
const filteredData = (listDataState.findMany.data || []).filter(item => { const filteredData = data || []
const keyword = search.toLowerCase();
return (
item.nama.toLowerCase().includes(keyword)
);
});
if (!listDataState.findMany.data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) )
} }
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p="md"> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Stack> <Stack>
<JudulList <Box style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 15 }}>
title='List Kategori Potensi' <Title order={4}>List Kategori Potensi</Title>
href='/admin/desa/potensi/kategori-potensi/create' <Tooltip label="Tambah Kategori Potensi" withArrow>
/> <Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/desa/potensi/kategori-potensi/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Box>
<Box style={{ overflowX: 'auto' }}> <Box style={{ overflowX: 'auto' }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}> <Table highlightOnHover striped withRowBorders style={{ minWidth: '700px' }}>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>No</TableTh> <TableTh style={{ width: '10%' }}>No</TableTh>
<TableTh>Nama</TableTh> <TableTh style={{ width: '60%' }}>Nama</TableTh>
<TableTh>Edit</TableTh> <TableTh style={{ width: '15%' }}>Edit</TableTh>
<TableTh>Hapus</TableTh> <TableTh style={{ width: '15%' }}>Hapus</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item, index) => ( {filteredData.length > 0 ? (
filteredData.map((item, index) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Box w={100}> <Text fz="sm">{(page - 1) * 10 + index + 1}</Text>
<Text truncate="end" fz={"sm"}>{index + 1}</Text>
</Box>
</TableTd> </TableTd>
<TableTd>{item.nama}</TableTd>
<TableTd> <TableTd>
<Button color='green' onClick={() => router.push(`/admin/desa/potensi/kategori-potensi/${item.id}`)}> <Text truncate lineClamp={1}>{item.nama}</Text>
</TableTd>
<TableTd>
<Button variant='light' color='green' onClick={() => router.push(`/admin/desa/potensi/kategori-potensi/${item.id}`)}>
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button <Button
variant='light'
color='red' color='red'
disabled={listDataState.delete.loading} disabled={listDataState.delete.loading}
onClick={() => { onClick={() => {
@@ -107,14 +112,32 @@ function ListKategoriPotensi({ search }: { search: string }) {
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} ))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">Tidak ada data kategori potensi yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
</Stack> </Stack>
</Paper> </Paper>
{/* Modal Konfirmasi Hapus */} <Center mt="md">
<Pagination
value={page}
onChange={(newPage) => load(newPage, 10, search)}
total={totalPages}
color="blue"
radius="md"
/>
</Center>
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}

View File

@@ -5,7 +5,19 @@ import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
import potensiDesaState from "@/app/admin/(dashboard)/_state/desa/potensi"; import potensiDesaState from "@/app/admin/(dashboard)/_state/desa/potensi";
import colors from "@/con/colors"; import colors from "@/con/colors";
import ApiFetch from "@/lib/api-fetch"; import ApiFetch from "@/lib/api-fetch";
import { Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput, Title } from "@mantine/core"; import {
Box,
Button,
Group,
Image,
Paper,
Select,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from "@mantine/core";
import { Dropzone } from "@mantine/dropzone"; import { Dropzone } from "@mantine/dropzone";
import { IconArrowBack, IconPhoto, IconUpload, IconX } from "@tabler/icons-react"; import { IconArrowBack, IconPhoto, IconUpload, IconX } from "@tabler/icons-react";
import { useParams, useRouter } from "next/navigation"; import { useParams, useRouter } from "next/navigation";
@@ -13,38 +25,36 @@ import { useEffect, useState } from "react";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { useProxy } from "valtio/utils"; import { useProxy } from "valtio/utils";
function EditPotensi() { function EditPotensi() {
const potensiState = useProxy(potensiDesaState.potensiDesa) const potensiState = useProxy(potensiDesaState.potensiDesa);
const router = useRouter() const router = useRouter();
const params = useParams() const params = useParams();
const [previewImage, setPreviewImage] = useState<string | null>(null); const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: '', name: "",
deskripsi: '', deskripsi: "",
kategoriId: '', kategoriId: "",
content: '', content: "",
imageId: '' imageId: "",
}); });
useEffect(() => { useEffect(() => {
potensiDesaState.kategoriPotensi.findMany.load() potensiDesaState.kategoriPotensi.findMany.load();
const loadPotensi = async () => { const loadPotensi = async () => {
const id = params?.id as string; const id = params?.id as string;
if (!id) return; if (!id) return;
try { try {
const data = await potensiState.edit.load(id); // ambil data dari API const data = await potensiState.edit.load(id);
if (data) { if (data) {
setFormData({ setFormData({
name: data.name || '', name: data.name || "",
deskripsi: data.deskripsi || '', deskripsi: data.deskripsi || "",
kategoriId: data.kategoriId || '', kategoriId: data.kategoriId || "",
content: data.content || '', content: data.content || "",
imageId: data.imageId || '', imageId: data.imageId || "",
}); });
if (data?.image?.link) { if (data?.image?.link) {
@@ -62,13 +72,9 @@ function EditPotensi() {
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
// Sinkronkan semua data dari formData ke state global
potensiState.edit.form = { potensiState.edit.form = {
...potensiState.edit.form, ...potensiState.edit.form,
name: formData.name, ...formData,
deskripsi: formData.deskripsi,
kategoriId: formData.kategoriId,
content: formData.content,
}; };
if (file) { if (file) {
@@ -92,44 +98,52 @@ function EditPotensi() {
}; };
return ( return (
<Box> <Box px={{ base: "sm", md: "lg" }} py="md">
<Box mb={10}> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()}> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<IconArrowBack color={colors['blue-button']} size={25} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors["blue-button"]} size={24} />
</Button> </Button>
</Box> </Tooltip>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}> <Title order={4} ml="sm" c="dark">
<Stack gap={"xs"}> Edit Potensi Desa
<Title order={3}>Edit Potensi</Title> </Title>
</Group>
<Paper
w={{ base: "100%", md: "50%" }}
bg={colors["white-1"]}
p="lg"
radius="md"
shadow="sm"
style={{ border: "1px solid #e0e0e0" }}
>
<Stack gap="md">
<TextInput <TextInput
label="Judul Potensi"
placeholder="Masukkan judul"
value={formData.name} value={formData.name}
onChange={(e) => { onChange={(e) => setFormData({ ...formData, name: e.target.value })}
const val = e.target.value; required
setFormData((prev) => ({ ...prev, name: val }));
potensiState.edit.form.name = val;
}}
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
placeholder="masukkan judul"
/> />
<TextInput <TextInput
label="Deskripsi Singkat"
placeholder="Masukkan deskripsi"
value={formData.deskripsi} value={formData.deskripsi}
onChange={(e) => { onChange={(e) => setFormData({ ...formData, deskripsi: e.target.value })}
const val = e.target.value; required
setFormData((prev) => ({ ...prev, deskripsi: val }));
potensiState.edit.form.deskripsi = val;
}}
label={<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>}
placeholder="masukkan deskripsi"
/> />
<Select <Select
value={formData.kategoriId} value={formData.kategoriId}
onChange={(val) => setFormData({ ...formData, kategoriId: val || "" })} onChange={(val) => setFormData({ ...formData, kategoriId: val || "" })}
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>} label="Kategori"
placeholder='Pilih kategori' placeholder="Pilih kategori"
data={ data={
potensiDesaState.kategoriPotensi.findMany.data?.map((v) => ({ potensiDesaState.kategoriPotensi.findMany.data?.map((v) => ({
value: v.id, value: v.id,
label: v.nama label: v.nama,
})) || [] })) || []
} }
clearable clearable
@@ -137,73 +151,86 @@ function EditPotensi() {
required required
error={!formData.kategoriId ? "Pilih kategori" : undefined} error={!formData.kategoriId ? "Pilih kategori" : undefined}
/> />
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text> <Text fw="bold" fz="sm" mb={6}>
<Box> Gambar Potensi
</Text>
<Dropzone <Dropzone
onDrop={(files) => { onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama const selectedFile = files[0];
if (selectedFile) { if (selectedFile) {
setFile(selectedFile); setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview setPreviewImage(URL.createObjectURL(selectedFile));
} }
}} }}
onReject={() => toast.error('File tidak valid.')} onReject={() => toast.error("File tidak valid, gunakan format gambar")}
maxSize={5 * 1024 ** 2} // Maks 5MB maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }} accept={{ "image/*": [] }}
radius="md"
p="xl"
> >
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}> <Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept> <Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /> <IconUpload size={48} color={colors["blue-button"]} stroke={1.5} />
</Dropzone.Accept> </Dropzone.Accept>
<Dropzone.Reject> <Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} /> <IconX size={48} color="red" stroke={1.5} />
</Dropzone.Reject> </Dropzone.Reject>
<Dropzone.Idle> <Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} /> <IconPhoto size={48} color="#868e96" stroke={1.5} />
</Dropzone.Idle> </Dropzone.Idle>
<Stack gap="xs" align="center">
<div> <Text size="md" fw={500}>
<Text size="xl" inline> Seret gambar atau klik untuk memilih file
Drag gambar ke sini atau klik untuk pilih file
</Text> </Text>
<Text size="sm" c="dimmed" inline mt={7}> <Text size="sm" c="dimmed">
Maksimal 5MB dan harus format gambar Maksimal 5MB, format gambar wajib
</Text> </Text>
</div> </Stack>
</Group> </Group>
</Dropzone> </Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && ( {previewImage && (
<Box mt="sm"> <Box mt="sm" style={{ display: "flex", justifyContent: "center" }}>
<Image <Image
src={previewImage} src={previewImage}
alt="Preview" alt="Preview Gambar"
radius="md"
style={{ style={{
maxWidth: '100%', maxHeight: 220,
maxHeight: '200px', objectFit: "contain",
objectFit: 'contain', border: `1px solid ${colors["blue-button"]}`,
borderRadius: '8px',
border: '1px solid #ddd',
}} }}
/> />
</Box> </Box>
)} )}
</Box>
</Box>
</Box>
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Konten</Text> <Text fz="sm" fw="bold" mb={6}>
Konten Lengkap
</Text>
<EditEditor <EditEditor
value={formData.content} value={formData.content}
onChange={(htmlContent) => { onChange={(htmlContent) => setFormData({ ...formData, content: htmlContent })}
setFormData((prev) => ({ ...prev, content: htmlContent }));
potensiState.edit.form.content = htmlContent;
}}
/> />
</Box> </Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Edit Potensi</Button>
<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> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

@@ -1,122 +1,151 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter, useParams } from 'next/navigation';
import React from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { useParams } from 'next/navigation';
import { useState } from 'react';
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi'; import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi';
export default function DetailPotensi() { export default function DetailPotensi() {
const router = useRouter() const router = useRouter();
const params = useParams() const params = useParams();
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null);
const potensiState = useProxy(potensiDesaState.potensiDesa) const potensiState = useProxy(potensiDesaState.potensiDesa);
useShallowEffect(() => { useShallowEffect(() => {
potensiState.findUnique.load(params?.id as string) potensiState.findUnique.load(params?.id as string);
}, []) }, []);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
potensiState.delete.byId(selectedId) potensiState.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/desa/potensi") router.push("/admin/desa/potensi");
}
} }
};
if (!potensiState.findUnique.data) { if (!potensiState.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
{Array.from({ length: 10 }).map((_, k) => ( <Skeleton height={500} radius="md" />
<Skeleton key={k} h={40} />
))}
</Stack> </Stack>
) );
} }
const data = potensiState.findUnique.data;
return ( return (
<Box> <Box py={10}>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Potensi</Text>
{potensiState.findUnique.data ? (
<Paper key={potensiState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Judul</Text>
<Text fz={"lg"}>{potensiState.findUnique.data.name}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Kategori</Text>
<Text fz={"lg"}>{potensiState.findUnique.data.kategori?.nama}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text>
<Text fz={"lg"}>{potensiState.findUnique.data.deskripsi}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
<Image src={potensiState.findUnique.data.image?.link} alt="gambar" />
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Konten</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: potensiState.findUnique.data.content }} />
</Box>
<Box>
<Flex gap={"xs"}>
<Button <Button
onClick={() => { variant="subtle"
if (potensiState.findUnique.data) { onClick={() => router.back()}
setSelectedId(potensiState.findUnique.data.id) leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
setModalHapus(true) mb={15}
}
}}
disabled={potensiState.delete.loading || !potensiState.findUnique.data}
color="red"
> >
<IconX size={20} /> Kembali
</Button> </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 Potensi
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz="lg" fw="bold">Judul</Text>
<Text fz="md" c="dimmed">{data.name || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Kategori</Text>
<Text fz="md" c="dimmed">{data.kategori?.nama || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Deskripsi</Text>
<Text fz="md" c="dimmed">{data.deskripsi || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Gambar</Text>
{data.image?.link ? (
<Image
src={data.image.link}
alt={data.name || 'Gambar Potensi'}
w={200}
h={200}
radius="md"
fit="cover"
/>
) : (
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
)}
</Box>
<Box>
<Text fz="lg" fw="bold">Konten</Text>
<Text
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{ __html: data.content || '-' }}
/>
</Box>
<Group gap="sm">
<Tooltip label="Hapus Potensi" withArrow position="top">
<Button <Button
color="red"
onClick={() => { onClick={() => {
if (potensiState.findUnique.data) { setSelectedId(data.id);
router.push(`/admin/desa/potensi/list-potensi/${potensiState.findUnique.data.id}/edit`) setModalHapus(true);
}
}} }}
disabled={!potensiState.findUnique.data} variant="light"
radius="md"
size="md"
>
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Potensi" withArrow position="top">
<Button
color="green" color="green"
onClick={() =>
router.push(`/admin/desa/potensi/list-potensi/${data.id}/edit`)
}
variant="light"
radius="md"
size="md"
> >
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Box> </Group>
</Stack> </Stack>
</Paper> </Paper>
) : null}
</Stack> </Stack>
</Paper> </Paper>
{/* Modal Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus potensi ini?" text="Apakah Anda yakin ingin menghapus potensi ini?"
/> />
</Box> </Box>
); );
} }

View File

@@ -4,7 +4,19 @@ import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi'; import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Image,
Paper,
Select,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@@ -12,8 +24,6 @@ import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function CreatePotensi() { function CreatePotensi() {
const potensiState = useProxy(potensiDesaState.potensiDesa); const potensiState = useProxy(potensiDesaState.potensiDesa);
const [previewImage, setPreviewImage] = useState<string | null>(null); const [previewImage, setPreviewImage] = useState<string | null>(null);
@@ -21,8 +31,8 @@ function CreatePotensi() {
const router = useRouter(); const router = useRouter();
useEffect(() => { useEffect(() => {
potensiDesaState.kategoriPotensi.findMany.load() potensiDesaState.kategoriPotensi.findMany.load();
}, []) }, []);
const handleSubmit = async () => { const handleSubmit = async () => {
if (!file) return toast.warn('Pilih file gambar terlebih dahulu'); if (!file) return toast.warn('Pilih file gambar terlebih dahulu');
@@ -59,34 +69,50 @@ function CreatePotensi() {
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header */}
<Button variant="subtle" onClick={() => router.back()}> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <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> </Button>
</Box> </Tooltip>
<Title order={4} ml="sm" c="dark">
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}> Tambah Potensi Desa
<Stack gap="xs"> </Title>
<Title order={3}>Create Potensi</Title> </Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Judul */}
<TextInput <TextInput
value={potensiState.create.form.name} value={potensiState.create.form.name}
onChange={(val) => (potensiState.create.form.name = val.target.value)} onChange={(val) => (potensiState.create.form.name = val.target.value)}
label={<Text fz="sm" fw="bold">Judul</Text>} label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul" placeholder="Masukkan judul potensi"
required
/> />
{/* Deskripsi */}
<TextInput <TextInput
value={potensiState.create.form.deskripsi} value={potensiState.create.form.deskripsi}
onChange={(val) => (potensiState.create.form.deskripsi = val.target.value)} onChange={(val) => (potensiState.create.form.deskripsi = val.target.value)}
label={<Text fz="sm" fw="bold">Deskripsi</Text>} label={<Text fz="sm" fw="bold">Deskripsi</Text>}
placeholder="masukkan deskripsi" placeholder="Masukkan deskripsi singkat"
required
/> />
{/* Kategori */}
<Select <Select
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>} label={<Text fz="sm" fw="bold">Kategori</Text>}
placeholder='Pilih kategori' placeholder="Pilih kategori"
value={potensiState.create.form.kategoriId || ""} value={potensiState.create.form.kategoriId || ""}
onChange={(val) => { onChange={(val) => {
potensiState.create.form.kategoriId = val ?? ""; potensiState.create.form.kategoriId = val ?? "";
@@ -97,65 +123,58 @@ function CreatePotensi() {
}))} }))}
/> />
{/* Upload Gambar */}
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text> <Text fw="bold" fz="sm" mb={6}>
<Box> Gambar Potensi
</Text>
<Dropzone <Dropzone
onDrop={(files) => { onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama const selectedFile = files[0];
if (selectedFile) { if (selectedFile) {
setFile(selectedFile); setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview setPreviewImage(URL.createObjectURL(selectedFile));
} }
}} }}
onReject={() => toast.error('File tidak valid.')} onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2} // Maks 5MB maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }} accept={{ 'image/*': [] }}
radius="md"
p="xl"
> >
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}> <Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept> <Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /> <IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept> </Dropzone.Accept>
<Dropzone.Reject> <Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} /> <IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject> </Dropzone.Reject>
<Dropzone.Idle> <Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} /> <IconPhoto size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle> </Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group> </Group>
<Text ta="center" mt="sm" size="sm" color="dimmed">
Seret gambar atau klik untuk memilih file (maks 5MB)
</Text>
</Dropzone> </Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && ( {previewImage && (
<Box mt="sm"> <Box mt="sm" style={{ textAlign: 'center' }}>
<Image <Image
src={previewImage} src={previewImage}
alt="Preview" alt="Preview Gambar"
style={{ radius="md"
maxWidth: '100%', style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/> />
</Box> </Box>
)} )}
</Box>
</Box> </Box>
{/* Konten Editor */}
<Box> <Box>
<Text fz="sm" fw="bold">Konten</Text> <Text fz="sm" fw="bold" mb={6}>
Konten Lengkap
</Text>
<CreateEditor <CreateEditor
value={potensiState.create.form.content} value={potensiState.create.form.content}
onChange={(htmlContent) => { onChange={(htmlContent) => {
@@ -164,9 +183,21 @@ function CreatePotensi() {
/> />
</Box> </Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}> {/* Tombol Simpan */}
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan Potensi Simpan Potensi
</Button> </Button>
</Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

@@ -1,25 +1,40 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import {
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip
} from '@mantine/core';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import potensiDesaState from '../../../_state/desa/potensi'; import potensiDesaState from '../../../_state/desa/potensi';
function Potensi() { function Potensi() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Posisi Organisasi' title='Potensi Desa'
placeholder='pencarian' placeholder='Cari potensi atau kategori...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -30,8 +45,8 @@ function Potensi() {
} }
function ListPotensi({ search }: { search: string }) { function ListPotensi({ search }: { search: string }) {
const potensiState = useProxy(potensiDesaState) const potensiState = useProxy(potensiDesaState);
const router = useRouter() const router = useRouter();
const { const {
data, data,
@@ -42,117 +57,108 @@ function ListPotensi({ search }: { search: string }) {
} = potensiState.potensiDesa.findMany; } = potensiState.potensiDesa.findMany;
useEffect(() => { useEffect(() => {
potensiState.kategoriPotensi.findMany.load() potensiState.kategoriPotensi.findMany.load();
load(page, 10) load(page, 10, search);
}, []) }, [page, search]);
const filteredData = (potensiState.potensiDesa.findMany.data || []).filter(item => { const filteredData = data || []
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.kategori?.nama.toLowerCase().includes(keyword) ||
item.deskripsi.toLowerCase().includes(keyword)
);
});
// Handle loading state
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton height={300} /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
); );
} }
if (data.length === 0) {
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Stack> <Group justify="space-between" mb="md">
<JudulList <Title order={4}>Daftar Potensi Desa</Title>
title='List Potensi' <Tooltip label="Tambah Potensi" withArrow>
href='/admin/desa/potensi/list-potensi/create' <Button
/> leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/desa/potensi/list-potensi/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: "auto" }}> <Box style={{ overflowX: "auto" }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}> <Table highlightOnHover style={{ minWidth: '700px' }}>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Judul</TableTh> <TableTh style={{ width: '20%' }}>Judul</TableTh>
<TableTh>Kategori</TableTh> <TableTh style={{ width: '20%' }}>Kategori</TableTh>
<TableTh>Deskripsi</TableTh> <TableTh style={{ width: '35%' }}>Deskripsi</TableTh>
<TableTh>Detail</TableTh> <TableTh style={{ width: '15%' }}>Detail</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
<TableTr> {filteredData.length > 0 ? (
<TableTd colSpan={4}>Tidak Ada Data</TableTd> filteredData.map((item) => (
</TableTr>
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
</Box>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<JudulList
title='List Potensi'
href='/admin/desa/potensi/list-potensi/create'
/>
<Box style={{ overflowX: "auto" }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<TableThead>
<TableTr>
<TableTh>Judul</TableTh>
<TableTh>Kategori</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Box w={100}> <Text fw={500} truncate="end" lineClamp={1}>
<Text truncate="end" fz={"sm"}>{item.name}</Text> {item.name}
</Box></TableTd> </Text>
<TableTd>{item.kategori?.nama}</TableTd> </TableTd>
<TableTd>
<Text fz="sm" c="dimmed">{item.kategori?.nama || '-'}</Text>
</TableTd>
<TableTd> <TableTd>
<Box w={300}> <Box w={300}>
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} /> <Text
truncate
fz="sm"
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
/>
</Box> </Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button onClick={() => router.push(`/admin/desa/potensi/list-potensi/${item.id}`)}> <Button
<IconDeviceImacCog size={25} /> variant="light"
color="blue"
onClick={() => router.push(`/admin/desa/potensi/list-potensi/${item.id}`)}
>
<IconDeviceImacCog size={20} />
<Text ml={5}>Detail</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} ))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">Tidak ada data potensi yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
</Stack>
</Paper> </Paper>
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => { onChange={(newPage) => {
load(newPage, 10); load(newPage, 10);
window.scrollTo(0, 0); window.scrollTo({ top: 0, behavior: 'smooth' });
}} }}
total={totalPages} total={totalPages}
mt="md" mt="md"
mb="md" mb="md"
color="blue"
radius="md"
/> />
</Center> </Center>
</Box> </Box>
) );
} }
export default Potensi; export default Potensi;

View File

@@ -1,9 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core'; import { 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 { IconUser, IconUsers, IconCalendar } from '@tabler/icons-react';
function LayoutTabsDetail({ children }: { children: React.ReactNode }) { function LayoutTabsDetail({ children }: { children: React.ReactNode }) {
const router = useRouter() const router = useRouter()
@@ -12,21 +13,28 @@ function LayoutTabsDetail({ children }: { children: React.ReactNode }) {
{ {
label: "Profile Desa", label: "Profile Desa",
value: "profiledesa", value: "profiledesa",
href: "/admin/desa/profile/profile-desa" href: "/admin/desa/profile/profile-desa",
icon: <IconUser size={18} stroke={1.8} />,
tooltip: "Lihat dan kelola profil desa"
}, },
{ {
label: "Profile Perbekel", label: "Profile Perbekel",
value: "profileperbekel", value: "profileperbekel",
href: "/admin/desa/profile/profile-perbekel" href: "/admin/desa/profile/profile-perbekel",
icon: <IconUsers size={18} stroke={1.8} />,
tooltip: "Kelola data Perbekel"
}, },
{ {
label: "Profile Perbekel Dari Masa Ke Masa", label: "Profile Perbekel Dari Masa Ke Masa",
value: "profile-perbekel-dari-masa-ke-masa", value: "profile-perbekel-dari-masa-ke-masa",
href: "/admin/desa/profile/profile-perbekel-dari-masa-ke-masa" href: "/admin/desa/profile/profile-perbekel-dari-masa-ke-masa",
icon: <IconCalendar size={18} stroke={1.8} />,
tooltip: "Riwayat Perbekel dari masa ke masa"
} }
]; ];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value); const currentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => { const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value) const tab = tabs.find(t => t.value === value)
@@ -44,22 +52,57 @@ function LayoutTabsDetail({ children }: { children: React.ReactNode }) {
}, [pathname]) }, [pathname])
return ( return (
<Stack> <Stack gap="lg">
<Title order={3}>Profile Desa</Title> <Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Profile Desa</Title>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}> <Tabs
<TabsList p={"xs"} bg={"#BBC8E7FF"}> color={colors['blue-button']}
{tabs.map((e, i) => ( variant='pills'
<TabsTab key={i} value={e.value}>{e.label}</TabsTab> value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
}}
>
{tabs.map((tab, i) => (
<Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))} ))}
</TabsList> </TabsList>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value}> {tabs.map((tab, i) => (
{/* Konten dummy, bisa diganti tergantung routing */} <TabsPanel
<></> key={i}
value={tab.value}
style={{
padding: "1.5rem",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
borderRadius: "1rem",
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
}}
>
{/* Konten dummy, bisa diganti sesuai routing */}
<>{children}</>
</TabsPanel> </TabsPanel>
))} ))}
</Tabs> </Tabs>
{children}
</Stack> </Stack>
); );
} }

View File

@@ -3,8 +3,8 @@
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
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, Button, Center, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Alert, Box, Button, Center, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconAlertCircle, IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -15,7 +15,7 @@ function Page() {
const router = useRouter() const router = useRouter()
const params = useParams() const params = useParams()
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
// Load data
useEffect(() => { useEffect(() => {
const loadData = async () => { const loadData = async () => {
const id = params?.id as string; const id = params?.id as string;
@@ -25,9 +25,12 @@ function Page() {
return; return;
} }
try {
const data = await lambangState.findUnique.load(id); const data = await lambangState.findUnique.load(id);
if (data) {
lambangState.update.initialize(data); lambangState.update.initialize(data);
} catch (error) {
console.error("Error loading lambang:", error);
toast.error("Gagal memuat data lambang desa");
} }
}; };
@@ -35,19 +38,21 @@ function Page() {
return () => { return () => {
lambangState.update.reset(); lambangState.update.reset();
lambangState.findUnique.reset(); // opsional: reset juga data lama lambangState.findUnique.reset();
}; };
}, [params?.id, router]); }, [params?.id, router]);
const handleSubmit = async () => { const handleSubmit = async () => {
if (isSubmitting || !lambangState.update.form.judul.trim()) { if (isSubmitting || !lambangState.update.form.judul.trim()) {
toast.error("Judul wajib diisi"); toast.error("Judul wajib diisi");
return; return;
} }
setIsSubmitting(true)
setIsSubmitting(true);
try { try {
const success = await lambangState.update.submit() const success = await lambangState.update.submit();
if (success) { if (success) {
toast.success("Data berhasil disimpan"); toast.success("Data berhasil disimpan");
router.push("/admin/desa/profile/profile-desa"); router.push("/admin/desa/profile/profile-desa");
@@ -58,17 +63,12 @@ function Page() {
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
} }
} };
const handleBack = () => { const handleBack = () => router.back();
router.back()
}
if ( // Loading state
lambangState.findUnique.loading || if (lambangState.findUnique.loading || lambangState.update.loading) {
!lambangState.findUnique.data ||
lambangState.update.loading
) {
return ( return (
<Box> <Box>
<Center h={400}> <Center h={400}>
@@ -77,27 +77,50 @@ function Page() {
</Box> </Box>
); );
} }
// Error state
if (lambangState.findUnique.error) {
return ( return (
<Box> <Box>
<Stack gap={'xs'}> <Stack gap="md">
<Group>
<Button variant="subtle" onClick={handleBack}> <Button variant="subtle" onClick={handleBack}>
<IconArrowBack color={colors['blue-button']} size={20} /> <IconArrowBack color={colors['blue-button']} size={20} />
</Button> </Button>
<Alert icon={<IconAlertCircle size={16} />} color="red">
<Text fw="bold">Error</Text>
<Text>{lambangState.findUnique.error}</Text>
</Alert>
</Stack>
</Box>
);
}
return (
<Box>
<Stack gap="xs">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">Edit Lambang Desa</Title>
</Group> </Group>
<Paper bg={colors['white-1']} p={'xs'} w={{ base: '100%', md: '50%' }}>
<Stack gap={'xs'}> <Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p="md" radius="md" shadow="sm" style={{ border: '1px solid #e0e0e0' }}>
<Box> <Stack gap="xs">
<Box>
<Stack>
<Title order={3}>Edit Lambang Desa</Title> <Title order={3}>Edit Lambang Desa</Title>
{/* Judul */}
<TextInput <TextInput
label={<Text fz={"md"} fw={"bold"}>Judul</Text>} label={<Text fw="bold">Judul</Text>}
placeholder="Judul" placeholder="Judul lambang"
value={lambangState.update.form.judul} value={lambangState.update.form.judul}
onChange={(e) => lambangState.update.form.judul = e.currentTarget.value} onChange={(e) => lambangState.update.form.judul = e.currentTarget.value}
error={!lambangState.update.form.judul && "Judul wajib diisi"} error={!lambangState.update.form.judul && "Judul wajib diisi"}
/> />
{/* Deskripsi */}
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Deskripsi</Text> <Text fz={"md"} fw={"bold"}>Deskripsi</Text>
<EditEditor <EditEditor
@@ -105,18 +128,23 @@ function Page() {
onChange={(val) => lambangState.update.form.deskripsi = val} onChange={(val) => lambangState.update.form.deskripsi = val}
/> />
</Box> </Box>
{/* Buttons */}
<Group> <Group>
<Button <Button
onClick={handleSubmit}
bg={colors['blue-button']} bg={colors['blue-button']}
onClick={handleSubmit}
loading={isSubmitting || lambangState.update.loading}
disabled={!lambangState.update.form.judul}
> >
Submit {isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
</Button>
<Button variant="outline" onClick={handleBack} disabled={isSubmitting || lambangState.update.loading}>
Batal
</Button> </Button>
</Group> </Group>
</Stack> </Stack>
</Box>
</Box>
</Stack>
</Paper> </Paper>
</Stack> </Stack>
</Box> </Box>

View File

@@ -5,38 +5,40 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
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 ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, SimpleGrid, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Image, Paper, SimpleGrid, Stack, Text, TextInput, Title, Tooltip, Center, Alert } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconPhoto, IconUpload, IconX, IconAlertCircle } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function Page() { function Page() {
const maskotState = useProxy(stateProfileDesa.maskotDesa) const maskotState = useProxy(stateProfileDesa.maskotDesa);
const router = useRouter() const router = useRouter();
const params = useParams() const params = useParams();
const [images, setImages] = useState<
Array<{ file: File; preview: string; label: string }>
>([]);
const [images, setImages] = useState<Array<{ file: File | null; preview: string; label: string }>>([]);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
judul: maskotState.update.form.judul || '', judul: '',
deskripsi: maskotState.update.form.deskripsi || '', deskripsi: '',
images: [] as Array<{ label: string; imageId: string }> images: [] as Array<{ label: string; imageId: string }>,
}) });
const [isSubmitting, setIsSubmitting] = useState(false);
// Load data
useEffect(() => { useEffect(() => {
const loadData = async () => { const loadData = async () => {
const id = params?.id as string; const id = params?.id as string;
if (!id) return; if (!id) {
toast.error("ID tidak valid");
router.push("/admin/desa/profile/profile-desa");
return;
}
try { try {
const data = await maskotState.findUnique.load(id); const data = await maskotState.findUnique.load(id);
if (data) { if (data) {
// 🔥 INI YANG KURANG!
maskotState.update.initialize(data); maskotState.update.initialize(data);
setFormData({ setFormData({
@@ -57,28 +59,39 @@ function Page() {
} }
} }
} catch (error) { } catch (error) {
console.error("Error loading berita:", error); console.error("Error loading maskot:", error);
toast.error("Gagal memuat data berita"); toast.error("Gagal memuat data maskot");
} }
}; };
loadData(); loadData();
}, [params?.id]);
return () => {
maskotState.update.reset();
maskotState.findUnique.reset();
};
}, [params?.id, router]);
const handleBack = () => { const handleBack = () => router.back();
router.back()
}
const handleSubmit = async () => { const handleSubmit = async () => {
if (isSubmitting || !formData.judul.trim()) {
toast.error("Judul wajib diisi");
return;
}
setIsSubmitting(true);
try { try {
const uploadedImages = []; const uploadedImages = [];
// Upload semua gambar baru // Upload semua gambar baru
for (const img of images) { for (const img of images) {
if (!img.file || !(img.file instanceof File)) { if (!img.file) {
toast.error("File tidak valid untuk di-upload"); // Kalau gambar lama, skip upload
continue; // atau return kalau kamu mau hentikan semua if (!img.preview) continue;
uploadedImages.push({ imageId: '', label: img.label });
continue;
} }
const res = await ApiFetch.api.fileStorage.create.post({ const res = await ApiFetch.api.fileStorage.create.post({
@@ -92,10 +105,7 @@ function Page() {
return; return;
} }
uploadedImages.push({ uploadedImages.push({ imageId: uploaded.id, label: img.label || 'main' });
imageId: uploaded.id,
label: img.label || 'main',
});
} }
// Update ke global state // Update ke global state
@@ -109,44 +119,79 @@ function Page() {
toast.success("Maskot berhasil diperbarui!"); toast.success("Maskot berhasil diperbarui!");
router.push("/admin/desa/profile/profile-desa"); router.push("/admin/desa/profile/profile-desa");
} }
} catch (error) { } catch (error) {
console.error("Error update maskot:", error); console.error("Error update maskot:", error);
toast.error("Gagal update maskot"); toast.error("Gagal update maskot");
} finally {
setIsSubmitting(false);
} }
}; };
// Loading state
if (maskotState.findUnique.loading || maskotState.update.loading) {
return ( return (
<Box> <Box>
<Stack gap={'xs'}> <Center h={400}>
<Group> <Text>Memuat data...</Text>
</Center>
</Box>
);
}
// Error state
if (maskotState.findUnique.error) {
return (
<Box>
<Stack gap="md">
<Button variant="subtle" onClick={handleBack}> <Button variant="subtle" onClick={handleBack}>
<IconArrowBack color={colors['blue-button']} size={20} /> <IconArrowBack color={colors['blue-button']} size={20} />
</Button> </Button>
<Alert icon={<IconAlertCircle size={16} />} color="red">
<Text fw="bold">Error</Text>
<Text>{maskotState.findUnique.error}</Text>
</Alert>
</Stack>
</Box>
);
}
return (
<Box>
<Stack gap="xs">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">Edit Maskot Desa</Title>
</Group> </Group>
<Paper bg={colors['white-1']} p={'xs'} w={{ base: '100%', md: '100%' }}>
<Stack gap={'xs'}> <Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p="md" radius="md" shadow="sm" style={{ border: '1px solid #e0e0e0' }}>
<Box> <Stack gap="xs">
<Box>
<Stack>
<Title order={3}>Edit Maskot Desa</Title> <Title order={3}>Edit Maskot Desa</Title>
{/* Judul */}
<TextInput <TextInput
w={{ base: '100%', md: '50%' }} label={<Text fw="bold">Judul</Text>}
label={<Text fz={"md"} fw={"bold"}>Judul</Text>} placeholder="Masukkan judul maskot"
placeholder="Masukkan judul"
value={formData.judul} value={formData.judul}
onChange={(val) => setFormData({ ...formData, judul: val.currentTarget.value })} onChange={(e) => setFormData({ ...formData, judul: e.currentTarget.value })}
error={!formData.judul && "Judul wajib diisi"}
/> />
<Box w={{ base: '100%', md: '50%' }}>
{/* Deskripsi */}
<Box>
<Text fz={"md"} fw={"bold"}>Deskripsi</Text> <Text fz={"md"} fw={"bold"}>Deskripsi</Text>
<EditEditor <EditEditor
value={formData.deskripsi} value={formData.deskripsi}
onChange={(val) => setFormData({ ...formData, deskripsi: val })} onChange={(val) => setFormData({ ...formData, deskripsi: val })}
/> />
</Box> </Box>
{/* Upload Gambar */}
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text> <Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box w={{ base: '100%', md: '50%' }}>
<Dropzone <Dropzone
onDrop={(files) => { onDrop={(files) => {
const newImages = files.map((file) => ({ const newImages = files.map((file) => ({
@@ -158,34 +203,24 @@ function Page() {
}} }}
> >
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}> <Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept> <Dropzone.Accept><IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /></Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /> <Dropzone.Reject><IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} /></Dropzone.Reject>
</Dropzone.Accept> <Dropzone.Idle><IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} /></Dropzone.Idle>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div> <div>
<Text size="xl" inline> <Text size="xl" inline>Drag images here or click to select files</Text>
Drag images here or click to select files <Text size="sm" c="dimmed" inline mt={7}>Attach as many files as you like, each file max 5mb</Text>
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Attach as many files as you like, each file should not exceed 5mb
</Text>
</div> </div>
</Group> </Group>
</Dropzone> </Dropzone>
</Box> </Box>
</Box>
{/* Preview Gambar */}
<SimpleGrid cols={{ base: 2, md: 4 }}> <SimpleGrid cols={{ base: 2, md: 4 }}>
{images.map((img, index) => ( {images.map((img, index) => (
<Box key={index} mb="md"> <Box key={index} mb="md">
<Paper p="sm" radius="md" withBorder style={{ position: 'relative', maxWidth: 300 }}> <Paper p="sm" radius="md" withBorder style={{ position: 'relative', maxWidth: 300 }}>
<Stack gap={'xs'}> <Stack gap="xs">
<Group> <Group justify="space-between">
<Button <Button
size="xs" size="xs"
color="red" color="red"
@@ -222,18 +257,22 @@ function Page() {
</Box> </Box>
))} ))}
</SimpleGrid> </SimpleGrid>
{/* Buttons */}
<Group> <Group>
<Button <Button
onClick={handleSubmit}
bg={colors['blue-button']} bg={colors['blue-button']}
onClick={handleSubmit}
loading={isSubmitting || maskotState.update.loading}
disabled={!formData.judul}
> >
Submit {isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
</Button>
<Button variant="outline" onClick={handleBack} disabled={isSubmitting || maskotState.update.loading}>
Batal
</Button> </Button>
</Group> </Group>
</Stack> </Stack>
</Box>
</Box>
</Stack>
</Paper> </Paper>
</Stack> </Stack>
</Box> </Box>

View File

@@ -3,8 +3,8 @@
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
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, Button, Center, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Alert, Box, Button, Center, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconAlertCircle, IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -16,6 +16,7 @@ function Page() {
const params = useParams() const params = useParams()
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
// Load data
useEffect(() => { useEffect(() => {
const loadData = async () => { const loadData = async () => {
const id = params?.id as string; const id = params?.id as string;
@@ -25,29 +26,34 @@ function Page() {
return; return;
} }
try {
const data = await sejarahState.findUnique.load(id); const data = await sejarahState.findUnique.load(id);
if (data) { if (data) {
sejarahState.update.initialize(data); sejarahState.update.initialize(data);
} }
} catch (error) {
console.error("Error loading sejarah:", error);
toast.error("Gagal memuat data sejarah desa");
}
}; };
loadData(); loadData();
return () => { return () => {
sejarahState.update.reset(); sejarahState.update.reset();
sejarahState.findUnique.reset(); // opsional: reset juga data lama sejarahState.findUnique.reset();
}; };
}, [params?.id, router]); }, [params?.id, router]);
const handleSubmit = async () => { const handleSubmit = async () => {
if (isSubmitting || !sejarahState.update.form.judul.trim()) { if (isSubmitting || !sejarahState.update.form.judul.trim()) {
toast.error("Judul wajib diisi"); toast.error("Judul wajib diisi");
return; return;
} }
setIsSubmitting(true)
setIsSubmitting(true);
try { try {
const success = await sejarahState.update.submit() const success = await sejarahState.update.submit();
if (success) { if (success) {
toast.success("Data berhasil disimpan"); toast.success("Data berhasil disimpan");
router.push("/admin/desa/profile/profile-desa"); router.push("/admin/desa/profile/profile-desa");
@@ -58,17 +64,12 @@ function Page() {
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
} }
} };
const handleBack = () => { const handleBack = () => router.back();
router.back()
}
if ( // Loading state
sejarahState.findUnique.loading || if (sejarahState.findUnique.loading || sejarahState.update.loading) {
!sejarahState.findUnique.data ||
sejarahState.update.loading
) {
return ( return (
<Box> <Box>
<Center h={400}> <Center h={400}>
@@ -77,27 +78,50 @@ function Page() {
</Box> </Box>
); );
} }
// Error state
if (sejarahState.findUnique.error) {
return ( return (
<Box> <Box>
<Stack gap={'xs'}> <Stack gap="md">
<Group>
<Button variant="subtle" onClick={handleBack}> <Button variant="subtle" onClick={handleBack}>
<IconArrowBack color={colors['blue-button']} size={20} /> <IconArrowBack color={colors['blue-button']} size={20} />
</Button> </Button>
<Alert icon={<IconAlertCircle size={16} />} color="red">
<Text fw="bold">Error</Text>
<Text>{sejarahState.findUnique.error}</Text>
</Alert>
</Stack>
</Box>
);
}
return (
<Box>
<Stack gap="xs">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">Edit Sejarah Desa</Title>
</Group> </Group>
<Paper bg={colors['white-1']} p={'xs'} w={{ base: '100%', md: '50%' }}>
<Stack gap={'xs'}> <Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p="md" radius="md" shadow="sm" style={{ border: '1px solid #e0e0e0' }}>
<Box> <Stack gap="xs">
<Box>
<Stack>
<Title order={3}>Edit Sejarah Desa</Title> <Title order={3}>Edit Sejarah Desa</Title>
{/* Judul */}
<TextInput <TextInput
label={<Text fz={"md"} fw={"bold"}>Judul</Text>} label={<Text fw="bold">Judul</Text>}
placeholder="Judul" placeholder="Judul sejarah"
value={sejarahState.update.form.judul} value={sejarahState.update.form.judul}
onChange={(e) => sejarahState.update.form.judul = e.currentTarget.value} onChange={(e) => sejarahState.update.form.judul = e.currentTarget.value}
error={!sejarahState.update.form.judul && "Judul wajib diisi"} error={!sejarahState.update.form.judul && "Judul wajib diisi"}
/> />
{/* Deskripsi */}
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Deskripsi</Text> <Text fz={"md"} fw={"bold"}>Deskripsi</Text>
<EditEditor <EditEditor
@@ -105,18 +129,23 @@ function Page() {
onChange={(val) => sejarahState.update.form.deskripsi = val} onChange={(val) => sejarahState.update.form.deskripsi = val}
/> />
</Box> </Box>
{/* Buttons */}
<Group> <Group>
<Button <Button
onClick={handleSubmit}
bg={colors['blue-button']} bg={colors['blue-button']}
onClick={handleSubmit}
loading={isSubmitting || sejarahState.update.loading}
disabled={!sejarahState.update.form.judul}
> >
Submit {isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
</Button>
<Button variant="outline" onClick={handleBack} disabled={isSubmitting || sejarahState.update.loading}>
Batal
</Button> </Button>
</Group> </Group>
</Stack> </Stack>
</Box>
</Box>
</Stack>
</Paper> </Paper>
</Stack> </Stack>
</Box> </Box>

View File

@@ -3,8 +3,8 @@
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
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, Button, Center, Group, Paper, Stack, Text, Title } from '@mantine/core'; import { Alert, Box, Button, Center, Group, Paper, Stack, Text, Title, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconAlertCircle, IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -16,6 +16,7 @@ function Page() {
const params = useParams() const params = useParams()
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
// Load data
useEffect(() => { useEffect(() => {
const loadData = async () => { const loadData = async () => {
const id = params?.id as string; const id = params?.id as string;
@@ -25,9 +26,12 @@ function Page() {
return; return;
} }
try {
const data = await visiMisiState.findUnique.load(id); const data = await visiMisiState.findUnique.load(id);
if (data) {
visiMisiState.update.initialize(data); visiMisiState.update.initialize(data);
} catch (error) {
console.error("Error loading visi misi:", error);
toast.error("Gagal memuat data visi misi desa");
} }
}; };
@@ -35,40 +39,37 @@ function Page() {
return () => { return () => {
visiMisiState.update.reset(); visiMisiState.update.reset();
visiMisiState.findUnique.reset(); // opsional: reset juga data lama visiMisiState.findUnique.reset();
}; };
}, [params?.id, router]); }, [params?.id, router]);
const handleSubmit = async () => { const handleSubmit = async () => {
if (isSubmitting || !visiMisiState.update.form.visi.trim()) { if (isSubmitting || !visiMisiState.update.form.visi.trim()) {
toast.error("Visi wajib diisi"); toast.error("Visi wajib diisi");
return; return;
} }
setIsSubmitting(true)
setIsSubmitting(true);
try { try {
const success = await visiMisiState.update.submit() const success = await visiMisiState.update.submit();
if (success) { if (success) {
toast.success("Data berhasil disimpan"); toast.success("Data berhasil disimpan");
router.push("/admin/desa/profile/profile-desa"); router.push("/admin/desa/profile/profile-desa");
} }
} catch (error) { } catch (error) {
console.error("Error update sejarah desa:", error); console.error("Error update visi misi desa:", error);
toast.error("Terjadi kesalahan saat update sejarah desa"); toast.error("Terjadi kesalahan saat update visi misi desa");
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
} }
} };
const handleBack = () => { const handleBack = () => router.back();
router.back()
}
if ( // Loading state
visiMisiState.findUnique.loading || if (visiMisiState.findUnique.loading || visiMisiState.update.loading) {
!visiMisiState.findUnique.data ||
visiMisiState.update.loading
) {
return ( return (
<Box> <Box>
<Center h={400}> <Center h={400}>
@@ -77,25 +78,50 @@ function Page() {
</Box> </Box>
); );
} }
// Error state
if (visiMisiState.findUnique.error) {
return ( return (
<Box> <Box>
<Stack gap={'xs'}> <Stack gap="md">
<Group>
<Button variant="subtle" onClick={handleBack}> <Button variant="subtle" onClick={handleBack}>
<IconArrowBack color={colors['blue-button']} size={20} /> <IconArrowBack color={colors['blue-button']} size={20} />
</Button> </Button>
<Alert icon={<IconAlertCircle size={16} />} color="red">
<Text fw="bold">Error</Text>
<Text>{visiMisiState.findUnique.error}</Text>
</Alert>
</Stack>
</Box>
);
}
return (
<Box>
<Stack gap="xs">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">Edit Visi Misi Desa</Title>
</Group> </Group>
<Paper bg={colors['white-1']} p={'xs'} w={{ base: '100%', md: '50%' }}>
<Stack gap={'xs'}> <Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p="md" radius="md" shadow="sm" style={{ border: '1px solid #e0e0e0' }}>
<Box> <Stack gap="xs">
<Box>
<Stack>
<Title order={3}>Edit Visi Misi Desa</Title> <Title order={3}>Edit Visi Misi Desa</Title>
{/* Visi */}
<Box>
<Text fz={"md"} fw={"bold"}>Visi</Text> <Text fz={"md"} fw={"bold"}>Visi</Text>
<EditEditor <EditEditor
value={visiMisiState.update.form.visi} value={visiMisiState.update.form.visi}
onChange={(val) => visiMisiState.update.form.visi = val} onChange={(val) => visiMisiState.update.form.visi = val}
/> />
</Box>
{/* Misi */}
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Misi</Text> <Text fz={"md"} fw={"bold"}>Misi</Text>
<EditEditor <EditEditor
@@ -103,18 +129,23 @@ function Page() {
onChange={(val) => visiMisiState.update.form.misi = val} onChange={(val) => visiMisiState.update.form.misi = val}
/> />
</Box> </Box>
{/* Buttons */}
<Group> <Group>
<Button <Button
onClick={handleSubmit}
bg={colors['blue-button']} bg={colors['blue-button']}
onClick={handleSubmit}
loading={isSubmitting || visiMisiState.update.loading}
disabled={!visiMisiState.update.form.visi}
> >
Submit {isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
</Button>
<Button variant="outline" onClick={handleBack} disabled={isSubmitting || visiMisiState.update.loading}>
Batal
</Button> </Button>
</Group> </Group>
</Stack> </Stack>
</Box>
</Box>
</Stack>
</Paper> </Paper>
</Stack> </Stack>
</Box> </Box>

View File

@@ -1,7 +1,7 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Card, Center, Grid, GridCol, Group, Image, Paper, Stack, Text, Title } from '@mantine/core'; import { Box, Button, Card, Center, Divider, Grid, GridCol, Image, Paper, SimpleGrid, Stack, Text, Title, Tooltip } from '@mantine/core';
import { useSnapshot } from 'valtio'; import { useSnapshot } from 'valtio';
import stateProfileDesa from '../../../_state/desa/profile'; import stateProfileDesa from '../../../_state/desa/profile';
import { useEffect } from 'react'; import { useEffect } from 'react';
@@ -12,7 +12,6 @@ function Page() {
const router = useRouter(); const router = useRouter();
const snap = useSnapshot(stateProfileDesa); const snap = useSnapshot(stateProfileDesa);
// Panggil load data sekali saat komponen mount
useEffect(() => { useEffect(() => {
stateProfileDesa.sejarahDesa.findUnique.load("edit"); stateProfileDesa.sejarahDesa.findUnique.load("edit");
stateProfileDesa.visiMisiDesa.findUnique.load("edit"); stateProfileDesa.visiMisiDesa.findUnique.load("edit");
@@ -26,142 +25,219 @@ function Page() {
const maskot = snap.maskotDesa.findUnique.data; const maskot = snap.maskotDesa.findUnique.data;
return ( return (
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
<Stack gap={"lg"}> <Stack gap="lg">
<Title order={2}>Preview Profile Desa</Title> <Title order={2} c={colors['blue-button']}>Preview Profile Desa</Title>
{/* Sejarah Desa */} {/* Sejarah Desa */}
{sejarah && ( {sejarah && (
<Box> <Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">
<Stack gap={'lg'}> <Grid align="center">
<Paper p={"md"} bg={colors['BG-trans']}>
<Grid>
<GridCol span={{ base: 12, md: 11 }}> <GridCol span={{ base: 12, md: 11 }}>
<Title order={2}>Preview Sejarah Desa</Title> <Title order={3} c={colors['blue-button']}>Preview Sejarah Desa</Title>
</GridCol> </GridCol>
<GridCol span={{ base: 12, md: 1 }}> <GridCol span={{ base: 12, md: 1 }}>
<Button bg={colors['blue-button']} onClick={() => router.push(`/admin/desa/profile/profile-desa/${sejarah.id}/sejarah_desa`)}> <Tooltip label="Edit Sejarah Desa" withArrow>
<IconEdit size={20} /> <Button
c="green"
variant="light"
leftSection={<IconEdit size={18} stroke={2} />}
radius="md"
onClick={() => router.push(`/admin/desa/profile/profile-desa/${sejarah.id}/sejarah_desa`)}
>
Edit
</Button> </Button>
</Tooltip>
</GridCol> </GridCol>
</Grid> </Grid>
<Box pb={30}>
<Box px={{ base: 0, md: 50 }} py="xl">
<Paper bg={colors['white-1']} withBorder radius="md" shadow="xs" p="lg">
<Stack gap={0}>
<Center> <Center>
<Image src={"/darmasaba-icon.png"} alt="" w={{ base: 200, md: 300 }} /> <Image src="/darmasaba-icon.png" w={{ base: 150, md: 250 }} alt="Logo Desa" />
</Center> </Center>
<Text c={colors['blue-button']} ta={"center"} fw={"bold"} fz={"2.5rem"}>{sejarah.judul}</Text> <Paper
</Box> bg={colors['blue-button']}
<Paper p={"xl"} bg={colors['white-trans-1']}> py="md"
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: sejarah.deskripsi }} /> px="sm"
</Paper> radius="md"
style={{ mt: -30, boxShadow: '0 4px 20px rgba(0,0,0,0.15)' }}
>
<Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "1.2rem", md: "1.6rem" }}>
{sejarah.judul}
</Text>
</Paper> </Paper>
</Stack> </Stack>
<Divider my="md" color={colors['blue-button']} />
<Text fz={{ base: "md", md: "h3" }} ta="justify" dangerouslySetInnerHTML={{ __html: sejarah.deskripsi }} />
</Paper>
</Box> </Box>
</Paper>
)} )}
{/* Visi Misi Desa */} {/* Visi Misi Desa */}
{visiMisi && ( {visiMisi && (
<Box> <Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">
<Stack gap={'lg'}> <Grid align="center">
<Paper p={"md"} bg={colors['BG-trans']}>
<Grid>
<GridCol span={{ base: 12, md: 11 }}> <GridCol span={{ base: 12, md: 11 }}>
<Title order={2}>Preview Visi Misi Desa</Title> <Title order={3} c={colors['blue-button']}>Preview Visi Misi Desa</Title>
</GridCol> </GridCol>
<GridCol span={{ base: 12, md: 1 }}> <GridCol span={{ base: 12, md: 1 }}>
<Button onClick={() => router.push(`/admin/desa/profile/profile-desa/${visiMisi.id}/visi_misi_desa`)} bg={colors['blue-button']}> <Tooltip label="Edit Visi Misi Desa" withArrow>
<IconEdit size={20} /> <Button
c="green"
variant="light"
leftSection={<IconEdit size={18} stroke={2} />}
radius="md"
onClick={() => router.push(`/admin/desa/profile/profile-desa/${visiMisi.id}/visi_misi_desa`)}
>
Edit
</Button> </Button>
</Tooltip>
</GridCol> </GridCol>
</Grid> </Grid>
<Box pb={30}>
<Box px={{ base: 0, md: 50 }} py="xl">
<Paper bg={colors['white-1']} withBorder radius="md" shadow="xs" p="lg">
<Stack gap={0}>
<Center> <Center>
<Image src={"/darmasaba-icon.png"} alt="" w={{ base: 200, md: 300 }} /> <Image src="/darmasaba-icon.png" w={{ base: 150, md: 250 }} alt="Logo Desa" />
</Center> </Center>
<Text c={colors['blue-button']} ta={"center"} fw={"bold"} fz={"2.5rem"}>Visi Misi Desa</Text> <Paper
</Box> bg={colors['blue-button']}
<Paper p={"xl"} bg={colors['white-trans-1']}> py="md"
<Text fw={"bold"} fz={{ base: "lg", md: "h2" }}>Visi Desa</Text> px="sm"
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: visiMisi.visi }} /> radius="md"
<Text fw={"bold"} fz={{ base: "lg", md: "h2" }}>Misi Desa</Text> style={{ mt: -30, boxShadow: '0 4px 20px rgba(0,0,0,0.15)' }}
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: visiMisi.misi }} /> >
</Paper> <Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "1.2rem", md: "1.6rem" }}>
Visi Misi Desa
</Text>
</Paper> </Paper>
</Stack> </Stack>
<Divider my="md" color={colors['blue-button']} />
<Text fw="bold" fz={{ base: "lg", md: "h2" }}>Visi Desa</Text>
<Text fz={{ base: "md", md: "h3" }} ta="justify" dangerouslySetInnerHTML={{ __html: visiMisi.visi }} />
<Text fw="bold" fz={{ base: "lg", md: "h2" }}>Misi Desa</Text>
<Text fz={{ base: "md", md: "h3" }} ta="justify" dangerouslySetInnerHTML={{ __html: visiMisi.misi }} />
</Paper>
</Box> </Box>
</Paper>
)} )}
{/* Lambang Desa */} {/* Lambang Desa */}
{lambang && ( {lambang && (
<Box> <Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">
<Stack gap={'lg'}> <Grid align="center">
<Paper p={"md"} bg={colors['BG-trans']}>
<Grid>
<GridCol span={{ base: 12, md: 11 }}> <GridCol span={{ base: 12, md: 11 }}>
<Title order={2}>Preview Lambang Desa</Title> <Title order={3} c={colors['blue-button']}>Preview Lambang Desa</Title>
</GridCol> </GridCol>
<GridCol span={{ base: 12, md: 1 }}> <GridCol span={{ base: 12, md: 1 }}>
<Button onClick={() => router.push(`/admin/desa/profile/profile-desa/${lambang.id}/lambang_desa`)} bg={colors['blue-button']}> <Tooltip label="Edit Lambang Desa" withArrow>
<IconEdit size={20} /> <Button
c="green"
variant="light"
leftSection={<IconEdit size={18} stroke={2} />}
radius="md"
onClick={() => router.push(`/admin/desa/profile/profile-desa/${lambang.id}/lambang_desa`)}
>
Edit
</Button> </Button>
</Tooltip>
</GridCol> </GridCol>
</Grid> </Grid>
<Box pb={30}>
<Box px={{ base: 0, md: 50 }} py="xl">
<Paper bg={colors['white-1']} withBorder radius="md" shadow="xs" p="lg">
<Stack gap={0}>
<Center> <Center>
<Image src={"/darmasaba-icon.png"} alt="" w={{ base: 200, md: 300 }} /> <Image src="/darmasaba-icon.png" w={{ base: 150, md: 250 }} alt="Logo Desa" />
</Center> </Center>
<Text c={colors['blue-button']} ta={"center"} fw={"bold"} fz={"2.5rem"}>Lambang Desa</Text> <Paper
</Box> bg={colors['blue-button']}
<Paper p={"xl"} bg={colors['white-trans-1']}> py="md"
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: lambang.deskripsi }} /> px="sm"
</Paper> radius="md"
style={{ mt: -30, boxShadow: '0 4px 20px rgba(0,0,0,0.15)' }}
>
<Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "1.2rem", md: "1.6rem" }}>
Lambang Desa
</Text>
</Paper> </Paper>
</Stack> </Stack>
<Divider my="md" color={colors['blue-button']} />
<Text fz={{ base: "md", md: "h3" }} ta="justify" dangerouslySetInnerHTML={{ __html: lambang.deskripsi }} />
</Paper>
</Box> </Box>
</Paper>
)} )}
{/* Maskot Desa */} {/* Maskot Desa */}
{maskot && ( {maskot && (
<Box> <Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">
<Stack gap={'lg'}> <Grid align="center">
<Paper p={"md"} bg={colors['BG-trans']}>
<Grid>
<GridCol span={{ base: 12, md: 11 }}> <GridCol span={{ base: 12, md: 11 }}>
<Title order={2}>Preview Maskot Desa</Title> <Title order={3} c={colors['blue-button']}>Preview Maskot Desa</Title>
</GridCol> </GridCol>
<GridCol span={{ base: 12, md: 1 }}> <GridCol span={{ base: 12, md: 1 }}>
<Button onClick={() => router.push(`/admin/desa/profile/profile-desa/${maskot.id}/maskot_desa`)} bg={colors['blue-button']}> <Tooltip label="Edit Maskot Desa" withArrow>
<IconEdit size={20} /> <Button
c="green"
variant="light"
leftSection={<IconEdit size={18} stroke={2} />}
radius="md"
onClick={() => router.push(`/admin/desa/profile/profile-desa/${maskot.id}/maskot_desa`)}
>
Edit
</Button> </Button>
</Tooltip>
</GridCol> </GridCol>
</Grid> </Grid>
<Box pb={30}>
<Box px={{ base: 0, md: 50 }} py="xl">
<Paper bg={colors['white-1']} withBorder radius="md" shadow="xs" p="lg">
<Stack gap={0}>
<Center> <Center>
<Image src={"/pudak-icon.png"} alt="" w={{ base: 200, md: 300 }} /> <Image src="/pudak-icon.png" w={{ base: 150, md: 250 }} alt="Maskot Desa" />
</Center> </Center>
<Text c={colors['blue-button']} ta={"center"} fw={"bold"} fz={"2.5rem"}>Maskot Desa</Text> <Paper
</Box> bg={colors['blue-button']}
<Paper p={"xl"} bg={colors['white-trans-1']}> py="md"
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: maskot.deskripsi }} /> px="sm"
<Group wrap="wrap" gap="md"> radius="md"
{maskot.images.map((img, index) => ( style={{ mt: -30, boxShadow: '0 4px 20px rgba(0,0,0,0.15)' }}
<Card key={index} p="xs" w={220}> >
<Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "1.2rem", md: "1.6rem" }}>
Maskot Desa
</Text>
</Paper>
</Stack>
<Divider my="md" color={colors['blue-button']} />
<Text fz={{ base: "md", md: "h3" }} ta="justify" dangerouslySetInnerHTML={{ __html: maskot.deskripsi }} />
<Stack mt="md" gap="sm">
<SimpleGrid cols={{ base: 2, md: 4 }} spacing="md">
{maskot.images.map((img, idx) => (
<Card withBorder key={idx} p="xs" w={{ base: '100%', md: 180 }}>
<Center>
<Image <Image
src={img.image.link} src={img.image.link}
alt={img.label} alt={img.label}
w={200} w={150}
h={200} h={150}
fit="cover" fit="cover"
radius="md" radius="md"
style={{ border: '1px solid #ccc', objectFit: 'cover' }} style={{ border: '1px solid #ccc' }}
/> />
</Center>
<Text ta="center" mt="xs" fw="bold">{img.label}</Text> <Text ta="center" mt="xs" fw="bold">{img.label}</Text>
</Card> </Card>
))} ))}
</Group> </SimpleGrid>
</Paper>
</Paper>
</Stack> </Stack>
</Paper>
</Box> </Box>
</Paper>
)} )}
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -3,17 +3,27 @@
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 ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Image,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconImageInPicture, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function EditPerbekelDariMasaKeMasa() { function EditPerbekelDariMasaKeMasa() {
const state = useProxy(stateProfileDesa.mantanPerbekel) const state = useProxy(stateProfileDesa.mantanPerbekel);
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
const [previewImage, setPreviewImage] = useState<string | null>(null); const [previewImage, setPreviewImage] = useState<string | null>(null);
@@ -38,9 +48,7 @@ function EditPerbekelDariMasaKeMasa() {
periode: data.periode || '', periode: data.periode || '',
imageId: data.imageId || '' imageId: data.imageId || ''
}); });
if (data?.imageGalleryFoto?.link) { if (data?.imageGalleryFoto?.link) setPreviewImage(data.imageGalleryFoto.link);
setPreviewImage(data.imageGalleryFoto.link);
}
} }
} catch (error) { } catch (error) {
console.error('Error loading foto:', error); console.error('Error loading foto:', error);
@@ -52,24 +60,18 @@ function EditPerbekelDariMasaKeMasa() {
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
state.update.form = { state.update.form = { ...state.update.form, ...formData };
...state.update.form,
nama: formData.nama,
daerah: formData.daerah,
periode: formData.periode,
imageId: formData.imageId
};
if (file) { if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ const res = await ApiFetch.api.fileStorage.create.post({
file, file,
name: file.name, name: file.name,
}); });
const uploaded = res.data?.data; const uploaded = res.data?.data;
if (!uploaded?.id) { if (!uploaded?.id) return toast.error("Gagal upload gambar");
return toast.error("Gagal upload gambar");
}
state.update.form.imageId = uploaded.id; state.update.form.imageId = uploaded.id;
} }
await state.update.update(); await state.update.update();
toast.success('Perbekel dari masa ke masa berhasil diperbarui!'); toast.success('Perbekel dari masa ke masa berhasil diperbarui!');
router.push('/admin/desa/profile/profile-perbekel-dari-masa-ke-masa'); router.push('/admin/desa/profile/profile-perbekel-dari-masa-ke-masa');
@@ -80,86 +82,119 @@ function EditPerbekelDariMasaKeMasa() {
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> <Group mb="md">
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<IconArrowBack color={colors['blue-button']} size={25} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
</Box> </Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Perbekel Dari Masa Ke Masa
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Paper
<Stack gap={"xs"}> w={{ base: '100%', md: '50%' }}
<Title order={4}>Edit Perbekel Dari Masa Ke Masa</Title> bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama</Text>} label="Nama"
placeholder='Masukkan nama' placeholder="Masukkan nama"
value={formData.nama} value={formData.nama}
onChange={(e) => onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
(formData.nama = e.target.value) required
}
/> />
<Box> <Box>
<Text>Upload Foto</Text> <Text fw="bold" fz="sm" mb={6}>
Foto Perbekel
</Text>
<Dropzone <Dropzone
onDrop={(files) => { onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama const selectedFile = files[0];
if (selectedFile) { if (selectedFile) {
setFile(selectedFile); setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview setPreviewImage(URL.createObjectURL(selectedFile));
} }
}} }}
onReject={() => toast.error('File tidak valid.')} onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2} // Maks 5MB maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }} accept={{ 'image/*': [] }}
radius="md"
p="xl"
> >
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}> <Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept> <Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /> <IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
</Dropzone.Accept> </Dropzone.Accept>
<Dropzone.Reject> <Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} /> <IconX size={48} color="red" stroke={1.5} />
</Dropzone.Reject> </Dropzone.Reject>
<Dropzone.Idle> <Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} /> <IconPhoto size={48} color="#868e96" stroke={1.5} />
</Dropzone.Idle> </Dropzone.Idle>
<Stack gap="xs" align="center">
<div> <Text size="md" fw={500}>
<Text size="xl" inline> Seret gambar atau klik untuk memilih file
Drag gambar ke sini atau klik untuk pilih file
</Text> </Text>
<Text size="sm" c="dimmed" inline mt={7}> <Text size="sm" c="dimmed">
Maksimal 5MB dan harus format gambar Maksimal 5MB, format gambar wajib
</Text> </Text>
</div> </Stack>
</Group> </Group>
</Dropzone> </Dropzone>
{previewImage ? ( {previewImage && (
<Image alt="" src={previewImage} w={200} h={200} /> <Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
) : ( <Image
<Center w={200} h={200} bg={"gray"}> src={previewImage}
<IconImageInPicture /> alt="Preview Gambar"
</Center> radius="md"
style={{
maxHeight: 220,
objectFit: 'contain',
border: `1px solid ${colors['blue-button']}`,
}}
/>
</Box>
)} )}
</Box> </Box>
<TextInput <TextInput
label={<Text fw={"bold"} fz={"sm"}>Daerah</Text>} label="Daerah"
placeholder='Masukkan daerah' placeholder="Masukkan daerah"
value={formData.daerah} value={formData.daerah}
onChange={(e) => onChange={(e) => setFormData({ ...formData, daerah: e.target.value })}
(formData.daerah = e.target.value) required
}
/> />
<TextInput <TextInput
label={<Text fw={"bold"} fz={"sm"}>Periode</Text>} label="Periode"
placeholder='Masukkan periode' placeholder="Masukkan periode"
value={formData.periode} value={formData.periode}
onChange={(e) => onChange={(e) => setFormData({ ...formData, periode: e.target.value })}
(formData.periode = e.target.value) required
}
/> />
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button> <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> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -2,7 +2,7 @@
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
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, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
@@ -10,99 +10,130 @@ import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function DetailPerbekelDariMasa() { function DetailPerbekelDariMasa() {
const state = useProxy(stateProfileDesa.mantanPerbekel) const state = useProxy(stateProfileDesa.mantanPerbekel);
const [modalHapus, setModalHapus] = useState(false); const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null);
const params = useParams() const params = useParams();
const router = useRouter() const router = useRouter();
useShallowEffect(() => { useShallowEffect(() => {
state.findUnique.load(params?.id as string) state.findUnique.load(params?.id as string);
}, []) }, []);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
state.delete.byId(selectedId) state.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/desa/profile/profile-perbekel-dari-masa-ke-masa") router.push("/admin/desa/profile/profile-perbekel-dari-masa-ke-masa");
}
} }
};
if (!state.findUnique.data) { if (!state.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) );
} }
const data = state.findUnique.data;
return ( return (
<Box> <Box py={10}>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Perbekel Dari Masa Ke Masa</Text>
{state.findUnique.data ? (
<Paper key={state.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Nama Perbekel</Text>
<Text fz={"lg"}>{state.findUnique.data?.nama}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Daerah</Text>
<Text fz={"lg"}>{state.findUnique.data?.daerah}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Periode</Text>
<Text fz={"lg"}>{state.findUnique.data?.periode}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
<Image w={{ base: 300, md: 350}} src={state.findUnique.data?.image?.link} alt="gambar" />
</Box>
<Flex gap={"xs"} mt={10}>
<Button <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 Perbekel Dari Masa Ke Masa
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz="lg" fw="bold">Gambar</Text>
{data.image?.link ? (
<Image
src={data.image.link}
alt={data.nama || 'Gambar Perbekel'}
w={150}
h={150}
radius="md"
fit="cover"
/>
) : (
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
)}
</Box>
<Box>
<Text fz="lg" fw="bold">Nama Perbekel</Text>
<Text fz="md" c="dimmed">{data.nama || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Daerah</Text>
<Text fz="md" c="dimmed">{data.daerah || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Periode</Text>
<Text fz="md" c="dimmed">{data.periode || '-'}</Text>
</Box>
<Group gap="sm">
<Tooltip label="Hapus Perbekel" withArrow position="top">
<Button
color="red"
onClick={() => { onClick={() => {
if (state.findUnique.data) { setSelectedId(data.id);
setSelectedId(state.findUnique.data.id);
setModalHapus(true); setModalHapus(true);
}
}} }}
disabled={state.delete.loading || !state.findUnique.data} variant="light"
color={"red"} radius="md"
size="md"
> >
<IconX size={20} /> <IconX size={20} />
</Button> </Button>
</Tooltip>
<Tooltip label="Edit Perbekel" withArrow position="top">
<Button <Button
onClick={() => { color="green"
if (state.findUnique.data) { onClick={() => router.push(`/admin/desa/profile/profile-perbekel-dari-masa-ke-masa/${data.id}/edit`)}
router.push(`/admin/desa/profile/profile-perbekel-dari-masa-ke-masa/${state.findUnique.data.id}/edit`); variant="light"
} radius="md"
}} size="md"
disabled={!state.findUnique.data}
color={"green"}
> >
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Group>
</Stack> </Stack>
</Paper> </Paper>
) : null}
</Stack> </Stack>
</Paper> </Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus perbekel dari masa ke masa ini?' text="Apakah Anda yakin ingin menghapus perbekel dari masa ke masa ini?"
/> />
</Box> </Box>
); );

View File

@@ -1,8 +1,8 @@
'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 ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@@ -10,29 +10,26 @@ import { useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function CreatePerbekelDariMasaKeMasa() {
const state = useProxy(stateProfileDesa.mantanPerbekel);
function CreateVideo() {
const state = useProxy(stateProfileDesa.mantanPerbekel)
const router = useRouter(); const router = useRouter();
const [previewImage, setPreviewImage] = useState<string | null>(null); const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const resetForm = () => { const resetForm = () => {
state.create.form = { state.create.form = {
nama: "", nama: '',
daerah: "", daerah: '',
periode: "", periode: '',
imageId: "", imageId: '',
}; };
setPreviewImage(null) setPreviewImage(null);
setFile(null) setFile(null);
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
if (!file) { if (!file) {
return toast.warn("Pilih file gambar terlebih dahulu"); return toast.warn('Pilih file gambar terlebih dahulu');
} }
const res = await ApiFetch.api.fileStorage.create.post({ const res = await ApiFetch.api.fileStorage.create.post({
@@ -41,110 +38,118 @@ function CreateVideo() {
}); });
const uploaded = res.data?.data; const uploaded = res.data?.data;
if (!uploaded?.id) { if (!uploaded?.id) return toast.error('Gagal upload gambar');
return toast.error("Gagal upload gambar");
}
state.create.form.imageId = uploaded.id; state.create.form.imageId = uploaded.id;
await state.create.create(); await state.create.create();
resetForm(); resetForm();
router.push("/admin/desa/profile/profile-perbekel-dari-masa-ke-masa"); router.push('/admin/desa/profile/profile-perbekel-dari-masa-ke-masa');
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Back button + Title */}
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <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> </Button>
</Box> </Tooltip>
<Title order={4} ml="sm" c="dark">
Create Perbekel Dari Masa Ke Masa
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Paper
<Stack gap={"xs"}> w={{ base: '100%', md: '50%' }}
<Title order={4}>Create Perbekel Dari Masa Ke Masa</Title> bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Perbekel</Text>} label={<Text fw="bold" fz="sm">Nama Perbekel</Text>}
placeholder='Masukkan nama perbekel' placeholder="Masukkan nama perbekel"
value={state.create.form.nama} value={state.create.form.nama}
onChange={(val) => { onChange={(e) => (state.create.form.nama = e.target.value)}
state.create.form.nama = val.target.value;
}}
/>
<TextInput
label="Daerah"
placeholder="Masukkan daerah"
value={state.create.form.daerah}
onChange={(e) => {
state.create.form.daerah = e.currentTarget.value;
}}
required required
/> />
<TextInput <TextInput
label={<Text fw={"bold"} fz={"sm"}>Periode</Text>} label={<Text fw="bold" fz="sm">Daerah</Text>}
placeholder='Masukkan periode' placeholder="Masukkan daerah"
value={state.create.form.periode} value={state.create.form.daerah}
onChange={(e) => onChange={(e) => (state.create.form.daerah = e.target.value)}
(state.create.form.periode = e.target.value) required
}
/> />
<TextInput
label={<Text fw="bold" fz="sm">Periode</Text>}
placeholder="Masukkan periode"
value={state.create.form.periode}
onChange={(e) => (state.create.form.periode = e.target.value)}
required
/>
{/* Dropzone */}
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text> <Text fw="bold" fz="sm" mb={6}>Gambar Perbekel</Text>
<Box>
<Dropzone <Dropzone
onDrop={(files) => { onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama const selectedFile = files[0];
if (selectedFile) { if (selectedFile) {
setFile(selectedFile); setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview setPreviewImage(URL.createObjectURL(selectedFile));
} }
}} }}
onReject={() => toast.error('File tidak valid.')} onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2} // Maks 5MB maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }} accept={{ 'image/*': [] }}
radius="md"
p="xl"
> >
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}> <Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept> <Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /> <IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept> </Dropzone.Accept>
<Dropzone.Reject> <Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} /> <IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject> </Dropzone.Reject>
<Dropzone.Idle> <Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} /> <IconPhoto size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle> </Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group> </Group>
<Text ta="center" mt="sm" size="sm" color="dimmed">
Seret gambar atau klik untuk memilih file (maks 5MB)
</Text>
</Dropzone> </Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && ( {previewImage && (
<Box mt="sm"> <Box mt="sm" style={{ textAlign: 'center' }}>
<Image <Image
src={previewImage} src={previewImage}
alt="Preview" alt="Preview Gambar"
style={{ radius="md"
maxWidth: '100%', style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/> />
</Box> </Box>
)} )}
</Box>
</Box> {/* Submit */}
</Box> <Group justify="right">
<Group> <Button
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</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> </Group>
</Stack> </Stack>
</Paper> </Paper>
@@ -152,4 +157,4 @@ function CreateVideo() {
); );
} }
export default CreateVideo; export default CreatePerbekelDariMasaKeMasa;

View File

@@ -1,13 +1,12 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import stateProfileDesa from '../../../_state/desa/profile'; import stateProfileDesa from '../../../_state/desa/profile';
function PerbekelDariMasaKeMasa() { function PerbekelDariMasaKeMasa() {
@@ -16,7 +15,7 @@ function PerbekelDariMasaKeMasa() {
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Perbekel Dari Masa Ke Masa' title='Perbekel Dari Masa Ke Masa'
placeholder='pencarian' placeholder='Cari nama perbekel...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -29,74 +28,96 @@ function PerbekelDariMasaKeMasa() {
function ListPerbekelDariMasaKeMasa({ search }: { search: string }) { function ListPerbekelDariMasaKeMasa({ search }: { search: string }) {
const state = useProxy(stateProfileDesa.mantanPerbekel) const state = useProxy(stateProfileDesa.mantanPerbekel)
const router = useRouter(); const router = useRouter();
const { const { data, page, totalPages, loading, load } = state.findMany;
data,
page,
totalPages,
loading,
load,
} = state.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search) load(page, 10, search)
}, [page, search]) }, [page, search]);
const filteredData = (data || []) const filteredData = data || [];
if (loading || !data) { if (loading || !data) {
return ( return (
<Box py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={500} radius="md" />
</Box> </Stack>
) );
} }
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<JudulList <Group justify='space-between' mb="md">
title='List Perbekel Dari Masa Ke Masa' <Title order={4}>List Perbekel Dari Masa Ke Masa</Title>
href='/admin/desa/profile/profile-perbekel-dari-masa-ke-masa/create' <Tooltip label="Tambah Perbekel Baru" withArrow>
/> <Button
<Table striped withTableBorder withRowBorders> leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/desa/profile/profile-perbekel-dari-masa-ke-masa/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Nama Perbekel</TableTh> <TableTh style={{ width: '35%' }}>Nama Perbekel</TableTh>
<TableTh>Periode</TableTh> <TableTh style={{ width: '35%' }}>Periode</TableTh>
<TableTh>Detail</TableTh> <TableTh style={{ width: '20%' }}>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item) => ( {filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Box w={200}> <Text fw={500} lineClamp={1}>{item.nama}</Text>
<Text lineClamp={1}>{item.nama}</Text>
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={200}>
<Text lineClamp={1}>{item.periode}</Text> <Text lineClamp={1}>{item.periode}</Text>
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button onClick={() => router.push(`/admin/desa/profile/profile-perbekel-dari-masa-ke-masa/${item.id}`)}> <Button
variant="light"
color="blue"
onClick={() => router.push(`/admin/desa/profile/profile-perbekel-dari-masa-ke-masa/${item.id}`)}
>
<IconDeviceImac size={20} /> <IconDeviceImac size={20} />
<Text ml={5}>Detail</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} ))
) : (
<TableTr>
<TableTd colSpan={3}>
<Center py={20}>
<Text color="dimmed">Tidak ada data perbekel yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody> </TableTbody>
</Table> </Table>
</Box>
</Paper> </Paper>
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => load(newPage)} // ini penting! onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages} total={totalPages}
mt="md" mt="md"
mb="md" mb="md"
color="blue"
radius="md"
/> />
</Center> </Center>
</Box> </Box>

View File

@@ -4,9 +4,9 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
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 ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, Group, Image, Paper, Stack, Text, Title } from '@mantine/core'; import { Box, Button, Center, Group, Image, Paper, Stack, Text, Title, Tooltip } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconPhoto, IconUpload, IconX, IconImageInPicture } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -30,22 +30,29 @@ function ProfilePerbekel() {
} }
const data = await perbekelState.findUnique.load(id); const data = await perbekelState.findUnique.load(id);
if (data) { if (data) perbekelState.edit.initialize(data);
perbekelState.edit.initialize(data); if (data?.image?.link) setPreviewImage(data.image.link);
}
if (data?.image?.link) {
setPreviewImage(data.image.link);
}
}; };
loadData(); loadData();
return () => { return () => {
perbekelState.edit.reset(); perbekelState.edit.reset();
perbekelState.findUnique.reset(); // opsional: reset juga data lama perbekelState.findUnique.reset();
}; };
}, [params?.id, router]); }, [params?.id, router]);
const handleFileChange = (newFile: File | null) => {
if (!newFile) {
setFile(null);
setPreviewImage(null);
return;
}
setFile(newFile);
const reader = new FileReader();
reader.onload = (event) => setPreviewImage(event.target?.result as string);
reader.readAsDataURL(newFile);
}
const handleSubmit = async () => { const handleSubmit = async () => {
if (isSubmitting || !perbekelState.edit.form.biodata.trim()) { if (isSubmitting || !perbekelState.edit.form.biodata.trim()) {
@@ -62,7 +69,6 @@ function ProfilePerbekel() {
toast.error("Gagal upload gambar"); toast.error("Gagal upload gambar");
return; return;
} }
perbekelState.edit.form.imageId = uploaded.id; perbekelState.edit.form.imageId = uploaded.id;
} }
const success = await perbekelState.edit.submit() const success = await perbekelState.edit.submit()
@@ -78,15 +84,9 @@ function ProfilePerbekel() {
} }
} }
const handleBack = () => { const handleBack = () => router.back();
router.back()
}
if ( if (perbekelState.findUnique.loading || perbekelState.edit.loading) {
perbekelState.findUnique.loading ||
!perbekelState.findUnique.data ||
perbekelState.edit.loading
) {
return ( return (
<Box> <Box>
<Center h={400}> <Center h={400}>
@@ -98,117 +98,112 @@ function ProfilePerbekel() {
return ( return (
<Box py={10}> <Box py={10}>
<Stack gap={'xs'}> <Stack gap="xs">
<Group> {/* Header */}
<Button variant="subtle" onClick={handleBack}> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={20} /> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Profil Perbekel
</Title>
</Group> </Group>
<Paper bg={colors['white-1']} p={'xs'} w={{ base: '100%', md: '50%' }}>
<Stack gap={'xs'}> <Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }} radius="md" shadow="sm" style={{ border: '1px solid #e0e0e0' }}>
<Stack gap="xs">
{/* Biodata */}
<Box> <Box>
<Box> <Text fz="md" fw="bold">Biodata</Text>
<Stack>
<Title order={3}>Edit Profil Perbekel</Title>
<Text fz={"md"} fw={"bold"}>Biodata</Text>
<EditEditor <EditEditor
value={perbekelState.edit.form.biodata} value={perbekelState.edit.form.biodata}
onChange={(val) => perbekelState.edit.form.biodata = val} onChange={(val) => perbekelState.edit.form.biodata = val}
/> />
</Box>
{/* Gambar */}
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text> <Text fz="md" fw="bold">Gambar</Text>
<Box>
<Dropzone <Dropzone
onDrop={(files) => { onDrop={(files) => handleFileChange(files[0])}
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')} onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB maxSize={5 * 1024 ** 2} // 5MB
accept={{ 'image/*': [] }} accept={{ 'image/*': [] }}
> >
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}> <Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept> <Dropzone.Accept><IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /></Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /> <Dropzone.Reject><IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} /></Dropzone.Reject>
</Dropzone.Accept> <Dropzone.Idle><IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} /></Dropzone.Idle>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div> <div>
<Text size="xl" inline> <Text size="xl" inline>Drag gambar ke sini atau klik untuk pilih file</Text>
Drag gambar ke sini atau klik untuk pilih file <Text size="sm" c="dimmed" inline mt={7}>Maksimal 5MB dan harus format gambar</Text>
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div> </div>
</Group> </Group>
</Dropzone> </Dropzone>
{/* Tampilkan preview kalau ada */} {/* Preview */}
{previewImage && (
<Box mt="sm"> <Box mt="sm">
<Image {previewImage ? (
src={previewImage} <Image src={previewImage} alt="Preview" w={200} h={200} fit="cover" radius="md" />
alt="Preview" ) : (
style={{ <Center w={200} h={200} bg="gray.2">
maxWidth: '100%', <Stack align="center" gap="xs">
maxHeight: '200px', <IconImageInPicture size={48} color="gray" />
objectFit: 'contain', <Text size="sm" c="gray">Tidak ada gambar</Text>
borderRadius: '8px', </Stack>
border: '1px solid #ddd', </Center>
}}
/>
</Box>
)} )}
</Box> </Box>
</Box> </Box>
{/* Pengalaman */}
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Pengalaman</Text> <Text fz="md" fw="bold">Pengalaman</Text>
<EditEditor <EditEditor
value={perbekelState.edit.form.pengalaman} value={perbekelState.edit.form.pengalaman}
onChange={(val) => perbekelState.edit.form.pengalaman = val} onChange={(val) => perbekelState.edit.form.pengalaman = val}
/> />
</Box> </Box>
{/* Pengalaman Organisasi */}
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Pengalaman Organisasi</Text> <Text fz="md" fw="bold">Pengalaman Organisasi</Text>
<EditEditor <EditEditor
value={perbekelState.edit.form.pengalamanOrganisasi} value={perbekelState.edit.form.pengalamanOrganisasi}
onChange={(val) => perbekelState.edit.form.pengalamanOrganisasi = val} onChange={(val) => perbekelState.edit.form.pengalamanOrganisasi = val}
/> />
</Box> </Box>
{/* Program Unggulan */}
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Program Unggulan</Text> <Text fz="md" fw="bold">Program Unggulan</Text>
<EditEditor <EditEditor
value={perbekelState.edit.form.programUnggulan} value={perbekelState.edit.form.programUnggulan}
onChange={(val) => perbekelState.edit.form.programUnggulan = val} onChange={(val) => perbekelState.edit.form.programUnggulan = val}
/> />
</Box> </Box>
{/* Submit */}
<Group> <Group>
<Button <Button
onClick={handleSubmit}
bg={colors['blue-button']} bg={colors['blue-button']}
onClick={handleSubmit}
loading={isSubmitting || perbekelState.edit.loading}
disabled={!perbekelState.edit.form.biodata.trim()}
> >
Submit {isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
</Button>
<Button variant="outline" onClick={handleBack} disabled={isSubmitting || perbekelState.edit.loading}>
Batal
</Button> </Button>
</Group> </Group>
</Stack> </Stack>
</Box>
</Box>
</Stack>
</Paper> </Paper>
</Stack> </Stack>
</Box> </Box>
); )
} }
export default ProfilePerbekel; export default ProfilePerbekel;

View File

@@ -1,7 +1,7 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Divider, Grid, GridCol, Image, Paper, Stack, Text, Title } from '@mantine/core'; import { Box, Button, Center, Divider, Grid, GridCol, Image, Paper, Skeleton, Stack, Text, Title, Tooltip } from '@mantine/core';
import { IconEdit } from '@tabler/icons-react'; import { IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect } from 'react'; import { useEffect } from 'react';
@@ -12,98 +12,102 @@ function Page() {
const router = useRouter(); const router = useRouter();
const snap = useSnapshot(stateProfileDesa); const snap = useSnapshot(stateProfileDesa);
// Panggil load data sekali saat komponen mount // Load data saat mount
useEffect(() => { useEffect(() => {
stateProfileDesa.profilPerbekel.findUnique.load("edit"); stateProfileDesa.profilPerbekel.findUnique.load("edit");
}, []); }, []);
const perbekel = snap.profilPerbekel.findUnique.data; const perbekel = snap.profilPerbekel.findUnique.data;
if (!perbekel) {
return ( return (
<Paper bg={colors['white-1']} p={'md'}> <Stack align="center" justify="center" py="xl">
<Stack gap={"xs"}> <Skeleton radius="md" height={800} />
<Grid> </Stack>
);
}
return (
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
<Stack gap="md">
{/* Header + tombol edit */}
<Grid align="center">
<GridCol span={{ base: 12, md: 11 }}> <GridCol span={{ base: 12, md: 11 }}>
<Title order={3}>Preview Profile PPID</Title> <Title order={3} c={colors['blue-button']}>Preview Profil PPID</Title>
</GridCol> </GridCol>
<GridCol span={{ base: 12, md: 1 }}> <GridCol span={{ base: 12, md: 1 }}>
<Button bg={colors['blue-button']} onClick={() => router.push(`/admin/desa/profile/profile-perbekel/${snap.profilPerbekel.findUnique.data?.id}`)}> <Tooltip label="Edit Profil Perbekel" withArrow>
<IconEdit size={16} /> <Button
c="green"
variant="light"
leftSection={<IconEdit size={18} stroke={2} />}
radius="md"
onClick={() => router.push(`/admin/desa/profile/profile-perbekel/${perbekel.id}`)}
>
Edit
</Button> </Button>
</Tooltip>
</GridCol> </GridCol>
</Grid> </Grid>
{perbekel && (
<Box> {/* Card Profil */}
<Paper p={"xl"} bg={colors['BG-trans']}> <Paper p="xl" bg={colors['white-1']} withBorder radius="md" shadow="xs">
<Box px={{ base: "md", md: 100 }}> <Box px={{ base: "sm", md: 100 }}>
<Grid> <Grid>
<GridCol span={{ base: 12, md: 12 }}> <GridCol span={12}>
<Center> <Center>
<Image src={"/darmasaba-icon.png"} w={{ base: 100, md: 150 }} alt='' /> <Image src="/darmasaba-icon.png" w={{ base: 100, md: 150 }} alt="Logo Desa" />
</Center> </Center>
</GridCol> </GridCol>
<GridCol span={{ base: 12, md: 12 }}> <GridCol span={12}>
<Text ta={"center"} fz={{ base: "1.2rem", md: "1.8rem" }} fw={'bold'}>PROFIL PIMPINAN BADAN PUBLIK DESA DARMASABA </Text> <Text ta="center" fz={{ base: "1.2rem", md: "1.8rem" }} fw="bold" c={colors['blue-button']}>
Profil Pimpinan Badan Publik Desa Darmasaba
</Text>
</GridCol> </GridCol>
</Grid> </Grid>
</Box> </Box>
<Divider my={"md"} color={colors['blue-button']} /> <Divider my="md" color={colors['blue-button']} />
{/* biodata perbekel */}
<Box px={{ base: 0, md: 50 }} pb={30}> <Stack gap={0} px={{ base: 0, md: 50 }} pb="xl">
<Box pb={20} px={{ base: 0, md: 50 }}>
<Paper bg={colors['BG-trans']} w={{ base: "100%", md: "100%" }}>
<Stack gap={0}>
<Center> <Center>
<Image <Image
pt={{ base: 0, md: 90 }} pt={{ base: 0, md: 60 }}
src={perbekel.image?.link || "/perbekel.png"} src={perbekel.image?.link || "/perbekel.png"}
w={{ base: 250, md: 350 }} w={{ base: 250, md: 350 }}
alt='Foto Profil PPID' alt="Foto Profil Perbekel"
onError={(e) => { radius="md"
e.currentTarget.src = "/perbekel.png"; onError={(e) => { e.currentTarget.src = "/perbekel.png"; }}
}}
/> />
</Center> </Center>
<Paper <Paper
bg={colors['blue-button']} bg={colors['blue-button']}
py={20} py="md"
px="sm"
radius="md"
className="glass3" className="glass3"
px={{ base: 10, md: 10 }} style={{ mt: -30, boxShadow: '0 4px 20px rgba(0,0,0,0.15)' }}
> >
<Text ta={"center"} c={colors['white-1']} fw={"bolder"} fz={{ base: "1.2rem", md: "1.6rem" }}> <Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "1.2rem", md: "1.6rem" }}>
I.B. Surya Prabhawa Manuaba, S.H.,M.H.,NL.P. I.B. Surya Prabhawa Manuaba, S.H., M.H.
</Text> </Text>
</Paper> </Paper>
</Stack> </Stack>
</Paper>
</Box> {/* Biodata & Info */}
<Box pt={10}> <Box mt="lg">
<Box> <Text fz={{ base: "1.125rem", md: "1.5rem" }} fw="bold" mb={4}>Biodata</Text>
<Text fz={{ base: "1.125rem", md: "1.6rem" }} fw={'bold'}>Biodata</Text> <Text fz={{ base: "1rem", md: "1.4rem" }} ta="justify" dangerouslySetInnerHTML={{ __html: perbekel.biodata }} />
<Text fz={{ base: "1rem", md: "1.5rem" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: perbekel.biodata }} />
</Box> <Text fz={{ base: "1.125rem", md: "1.5rem" }} fw="bold" mt="md" mb={4}>Pengalaman</Text>
<Box> <Text fz={{ base: "1rem", md: "1.4rem" }} ta="justify" dangerouslySetInnerHTML={{ __html: perbekel.pengalaman }} />
<Text fz={{ base: "1.125rem", md: "1.6rem" }} fw={'bold'}>Pengalaman</Text>
<Text fz={{ base: "1rem", md: "1.5rem" }} dangerouslySetInnerHTML={{ __html: perbekel.pengalaman }} /> <Text fz={{ base: "1.125rem", md: "1.5rem" }} fw="bold" mt="md" mb={4}>Pengalaman Organisasi</Text>
</Box> <Text fz={{ base: "1rem", md: "1.4rem" }} ta="justify" dangerouslySetInnerHTML={{ __html: perbekel.pengalamanOrganisasi }} />
</Box>
<Box pb={30}> <Text fz={{ base: "1.125rem", md: "1.5rem" }} fw="bold" mt="md" mb={4}>Program Kerja Unggulan</Text>
<Text fz={{ base: "1.125rem", md: "1.6rem" }} fw={'bold'}>Pengalaman Organisasi</Text> <Text fz={{ base: "1rem", md: "1.4rem" }} ta="justify" dangerouslySetInnerHTML={{ __html: perbekel.programUnggulan }} />
<Box px={20}>
<Text fz={{ base: "1rem", md: "1.5rem" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: perbekel.pengalamanOrganisasi }} />
</Box>
</Box>
<Box pb={20}>
<Text fz={{ base: "1.125rem", md: "1.6rem" }} fw={'bold'}>Program Kerja Unggulan</Text>
<Box px={20}>
<Text fz={{ base: "1rem", md: "1.5rem" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: perbekel.programUnggulan }} />
</Box>
</Box>
</Box> </Box>
</Paper> </Paper>
</Box>
)}
</Stack> </Stack>
</Paper> </Paper>
); );

View File

@@ -11,15 +11,21 @@ import {
Stack, Stack,
Text, Text,
TextInput, TextInput,
Title Title,
Tooltip,
} from "@mantine/core"; } from "@mantine/core";
import { IconArrowBack, IconImageInPicture, IconPhoto, IconUpload, IconX } from "@tabler/icons-react"; import {
IconArrowBack,
IconImageInPicture,
IconPhoto,
IconUpload,
IconX,
} from "@tabler/icons-react";
import { useParams, useRouter } from "next/navigation"; import { useParams, useRouter } from "next/navigation";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { useProxy } from "valtio/utils"; import { useProxy } from "valtio/utils";
import EditEditor from "@/app/admin/(dashboard)/_com/editEditor"; import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
import colors from "@/con/colors"; import colors from "@/con/colors";
import ApiFetch from "@/lib/api-fetch"; import ApiFetch from "@/lib/api-fetch";
@@ -34,24 +40,24 @@ function EditKeamananLingkungan() {
const [previewImage, setPreviewImage] = useState<string | null>(null); const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: keamananState.edit.form.name || '', name: keamananState.edit.form.name || "",
deskripsi: keamananState.edit.form.deskripsi || '', deskripsi: keamananState.edit.form.deskripsi || "",
imageId: keamananState.edit.form.imageId || '' imageId: keamananState.edit.form.imageId || "",
}); });
// Load berita by id saat pertama kali // Load data by id
useEffect(() => { useEffect(() => {
const loadBerita = async () => { const loadData = async () => {
const id = params?.id as string; const id = params?.id as string;
if (!id) return; if (!id) return;
try { try {
const data = await keamananState.edit.load(id); // akses langsung, bukan dari proxy const data = await keamananState.edit.load(id);
if (data) { if (data) {
setFormData({ setFormData({
name: data.name || '', name: data.name || "",
deskripsi: data.deskripsi || '', deskripsi: data.deskripsi || "",
imageId: data.imageId || '', imageId: data.imageId || "",
}); });
if (data?.image?.link) { if (data?.image?.link) {
@@ -64,30 +70,29 @@ function EditKeamananLingkungan() {
} }
}; };
loadBerita(); loadData();
}, [params?.id]); // ✅ hapus beritaState dari dependency }, [params?.id]);
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
// Update global state with form data
keamananState.edit.form = { keamananState.edit.form = {
...keamananState.edit.form, ...keamananState.edit.form,
name: formData.name, name: formData.name,
deskripsi: formData.deskripsi, deskripsi: formData.deskripsi,
imageId: formData.imageId // Keep existing imageId if not changed imageId: formData.imageId,
}; };
// Jika ada file baru, upload
if (file) { if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name }); const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data; const uploaded = res.data?.data;
if (!uploaded?.id) { if (!uploaded?.id) {
return toast.error("Gagal upload gambar"); return toast.error("Gagal upload gambar");
} }
// Update imageId in global state
keamananState.edit.form.imageId = uploaded.id; keamananState.edit.form.imageId = uploaded.id;
} }
@@ -101,36 +106,72 @@ function EditKeamananLingkungan() {
}; };
return ( return (
<Box> <Box px={{ base: "sm", md: "lg" }} py="md">
<Box mb={10}> {/* Header */}
<Button variant="subtle" onClick={() => router.back()}> <Group mb="md">
<IconArrowBack color={colors["blue-button"]} size={30} /> <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> </Button>
</Box> </Tooltip>
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}> <Title order={4} ml="sm" c="dark">
<Stack gap={"xs"}> Edit Keamanan Lingkungan
<Title order={3}>Edit Keamanan Lingkungan</Title> </Title>
</Group>
{/* Form */}
<Paper
w={{ base: "100%", md: "50%" }}
bg={colors["white-1"]}
p="lg"
radius="md"
shadow="sm"
style={{ border: "1px solid #e0e0e0" }}
>
<Stack gap="md">
<Dropzone <Dropzone
onDrop={(files) => { onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama const selectedFile = files[0];
if (selectedFile) { if (selectedFile) {
setFile(selectedFile); setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview setPreviewImage(URL.createObjectURL(selectedFile));
} }
}} }}
onReject={() => toast.error('File tidak valid.')} onReject={() => toast.error("File tidak valid.")}
maxSize={5 * 1024 ** 2} // Maks 5MB maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }} accept={{ "image/*": [] }}
>
<Group
justify="center"
gap="xl"
mih={220}
style={{ pointerEvents: "none" }}
> >
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept> <Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /> <IconUpload
size={52}
color="var(--mantine-color-blue-6)"
stroke={1.5}
/>
</Dropzone.Accept> </Dropzone.Accept>
<Dropzone.Reject> <Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} /> <IconX
size={52}
color="var(--mantine-color-red-6)"
stroke={1.5}
/>
</Dropzone.Reject> </Dropzone.Reject>
<Dropzone.Idle> <Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} /> <IconPhoto
size={52}
color="var(--mantine-color-dimmed)"
stroke={1.5}
/>
</Dropzone.Idle> </Dropzone.Idle>
<div> <div>
@@ -151,15 +192,21 @@ function EditKeamananLingkungan() {
<IconImageInPicture /> <IconImageInPicture />
</Center> </Center>
)} )}
<TextInput <TextInput
value={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) =>
label={<Text fz={"sm"} fw={"bold"}>Judul Keamanan Lingkungan</Text>} setFormData({ ...formData, name: e.target.value })
placeholder="masukkan judul" }
label="Judul Keamanan Lingkungan"
placeholder="Masukkan judul"
required
/> />
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text> <Text fz="sm" fw="bold">
Deskripsi
</Text>
<EditEditor <EditEditor
value={formData.deskripsi} value={formData.deskripsi}
onChange={(htmlContent) => { onChange={(htmlContent) => {
@@ -169,7 +216,20 @@ function EditKeamananLingkungan() {
/> />
</Box> </Box>
<Button onClick={handleSubmit}>Simpan</Button> <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> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

@@ -1,9 +1,9 @@
'use client' 'use client'
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -36,64 +36,95 @@ function DetailKeamananLingkungan() {
if (!keamananState.findUnique.data) { if (!keamananState.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={40} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) )
} }
const data = keamananState.findUnique.data
return ( return (
<Box> <Box py={10}>
<Box mb={10}> {/* Tombol Back */}
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Keamanan Lingkungan</Text>
{keamananState.findUnique.data ? (
<Paper key={keamananState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
<Image w={{ base: 150, md: 490}} src={keamananState.findUnique.data?.image?.link} alt="gambar" />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Judul Keamanan Lingkungan</Text>
<Text fz={"lg"}>{keamananState.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: keamananState.findUnique.data?.deskripsi }} />
</Box>
<Flex gap={"xs"} mt={10}>
<Button <Button
onClick={() => { variant="subtle"
if (keamananState.findUnique.data) { onClick={() => router.back()}
setSelectedId(keamananState.findUnique.data.id); leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
setModalHapus(true); mb={15}
}
}}
disabled={keamananState.delete.loading || !keamananState.findUnique.data}
color={"red"}
> >
<IconX size={20} /> Kembali
</Button> </Button>
{/* Wrapper Detail */}
<Paper
withBorder
w={{ base: "100%", md: "50%" }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Keamanan Lingkungan
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz="lg" fw="bold">Gambar</Text>
<Image
w={{ base: 150, md: 490 }}
src={data?.image?.link}
alt="gambar keamanan lingkungan"
/>
</Box>
<Box>
<Text fz="lg" fw="bold">Judul Keamanan Lingkungan</Text>
<Text fz="md" c="dimmed">{data?.name || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Deskripsi</Text>
<Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data?.deskripsi }} />
</Box>
{/* Aksi */}
<Group gap="sm">
<Tooltip label="Hapus Data" withArrow position="top">
<Button <Button
color="red"
onClick={() => { onClick={() => {
if (keamananState.findUnique.data) { setSelectedId(data.id);
router.push(`/admin/keamanan/keamanan-lingkungan-pecalang-patwal/${keamananState.findUnique.data.id}/edit`); setModalHapus(true);
}
}} }}
disabled={!keamananState.findUnique.data} variant="light"
color={"green"} radius="md"
size="md"
>
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Data" withArrow position="top">
<Button
color="green"
onClick={() =>
router.push(
`/admin/keamanan/keamanan-lingkungan-pecalang-patwal/${data.id}/edit`
)
}
variant="light"
radius="md"
size="md"
> >
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Group>
</Stack> </Stack>
</Paper> </Paper>
) : null}
</Stack> </Stack>
</Paper> </Paper>
@@ -102,7 +133,7 @@ function DetailKeamananLingkungan() {
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus keamanan lingkungan ini?' text="Apakah anda yakin ingin menghapus keamanan lingkungan ini?"
/> />
</Box> </Box>
); );

View File

@@ -1,9 +1,25 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Image,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import {
IconArrowBack,
IconPhoto,
IconUpload,
IconX,
} from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -11,37 +27,36 @@ import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor'; import CreateEditor from '../../../_com/createEditor';
import keamananLingkunganState from '../../../_state/keamanan/keamanan-lingkungan'; import keamananLingkunganState from '../../../_state/keamanan/keamanan-lingkungan';
function CreateKeamananLingkungan() { function CreateKeamananLingkungan() {
const keamananState = useProxy(keamananLingkunganState) const keamananState = useProxy(keamananLingkunganState);
const [previewImage, setPreviewImage] = useState<string | null>(null); const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const router = useRouter(); const router = useRouter();
const resetForm = () => { const resetForm = () => {
keamananState.create.form = { keamananState.create.form = {
name: "", name: '',
deskripsi: "", deskripsi: '',
imageId: "", imageId: '',
} };
setPreviewImage(null); setPreviewImage(null);
setFile(null); setFile(null);
} };
const handleSubmit = async () => { const handleSubmit = async () => {
if (!file) { if (!file) {
return toast.warn("Pilih file gambar terlebih dahulu"); return toast.warn('Pilih file gambar terlebih dahulu');
} }
const res = await ApiFetch.api.fileStorage.create.post({ const res = await ApiFetch.api.fileStorage.create.post({
file, file,
name: file.name, name: file.name,
}) });
const uploaded = res.data?.data; const uploaded = res.data?.data;
if (!uploaded?.id) { if (!uploaded?.id) {
return toast.error("Gagal mengupload file"); return toast.error('Gagal mengupload file');
} }
keamananState.create.form.imageId = uploaded.id; keamananState.create.form.imageId = uploaded.id;
@@ -49,44 +64,81 @@ function CreateKeamananLingkungan() {
await keamananState.create.create(); await keamananState.create.create();
resetForm(); resetForm();
router.push("/admin/keamanan/keamanan-lingkungan-pecalang-patwal") router.push('/admin/keamanan/keamanan-lingkungan-pecalang-patwal');
} };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header */}
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <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> </Button>
</Box> </Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Data Keamanan Lingkungan
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> {/* Form */}
<Stack gap={"xs"}> <Paper
<Title order={4}>Create Keamanan Lingkungan</Title> w={{ base: '100%', md: '50%' }}
<Box> bg={colors['white-1']}
<Text fz={"md"} fw={"bold"}>Gambar</Text> p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Upload Gambar */}
<Box> <Box>
<Text fz="sm" fw="bold">
Gambar
</Text>
<Dropzone <Dropzone
onDrop={(files) => { onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama const selectedFile = files[0];
if (selectedFile) { if (selectedFile) {
setFile(selectedFile); setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview setPreviewImage(URL.createObjectURL(selectedFile));
} }
}} }}
onReject={() => toast.error('File tidak valid.')} onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }} accept={{ 'image/*': [] }}
> >
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}> <Group
justify="center"
gap="xl"
mih={220}
style={{ pointerEvents: 'none' }}
>
<Dropzone.Accept> <Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /> <IconUpload
size={52}
color="var(--mantine-color-blue-6)"
stroke={1.5}
/>
</Dropzone.Accept> </Dropzone.Accept>
<Dropzone.Reject> <Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} /> <IconX
size={52}
color="var(--mantine-color-red-6)"
stroke={1.5}
/>
</Dropzone.Reject> </Dropzone.Reject>
<Dropzone.Idle> <Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} /> <IconPhoto
size={52}
color="var(--mantine-color-dimmed)"
stroke={1.5}
/>
</Dropzone.Idle> </Dropzone.Idle>
<div> <div>
@@ -100,7 +152,6 @@ function CreateKeamananLingkungan() {
</Group> </Group>
</Dropzone> </Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && ( {previewImage && (
<Box mt="sm"> <Box mt="sm">
<Image <Image
@@ -116,19 +167,24 @@ function CreateKeamananLingkungan() {
/> />
</Box> </Box>
)} )}
</Box>
</Box> {/* Input Nama */}
</Box>
<TextInput <TextInput
value={keamananState.create.form.name} value={keamananState.create.form.name}
onChange={(val) => { onChange={(val) => {
keamananState.create.form.name = val.target.value; keamananState.create.form.name = val.target.value;
}} }}
label={<Text fw={"bold"} fz={"sm"}>Nama Keamanan Lingkungan</Text>} label={<Text fw="bold" fz="sm">Nama Keamanan Lingkungan</Text>}
placeholder='Masukkan nama Keamanan Lingkungan' placeholder="Masukkan nama Keamanan Lingkungan"
required
/> />
{/* Input Deskripsi */}
<Box> <Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Keamanan Lingkungan</Text> <Text fw="bold" fz="sm">
Deskripsi Keamanan Lingkungan
</Text>
<CreateEditor <CreateEditor
value={keamananState.create.form.deskripsi} value={keamananState.create.form.deskripsi}
onChange={(val) => { onChange={(val) => {
@@ -136,8 +192,21 @@ function CreateKeamananLingkungan() {
}} }}
/> />
</Box> </Box>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button> {/* Tombol Submit */}
<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> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -1,26 +1,46 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import {
import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; Box,
import HeaderSearch from '../../_com/header'; Button,
import JudulList from '../../_com/judulList'; Center,
import { useRouter } from 'next/navigation'; Group,
import { useProxy } from 'valtio/utils'; Pagination,
import keamananLingkunganState from '../../_state/keamanan/keamanan-lingkungan'; Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import keamananLingkunganState from '../../_state/keamanan/keamanan-lingkungan';
function KeamananLingkungan() { function KeamananLingkungan() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
return ( return (
<Box> <Box>
{/* Header Search */}
<HeaderSearch <HeaderSearch
title='Keamanan Lingkungan' title='Keamanan Lingkungan'
placeholder='pencarian' placeholder='Cari nama atau deskripsi...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
/> />
<ListKeamananLingkungan search={search} /> <ListKeamananLingkungan search={search} />
</Box> </Box>
); );
@@ -47,54 +67,94 @@ function ListKeamananLingkungan({ search }: { search: string }) {
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
) )
} }
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<JudulList {/* Judul + Tombol Tambah */}
title='List Keamanan Lingkungan' <Group justify="space-between" mb="md">
href='/admin/keamanan/keamanan-lingkungan-pecalang-patwal/create' <Title order={4}>Daftar Keamanan Lingkungan</Title>
/> <Tooltip label="Tambah Data Keamanan" withArrow>
<Table striped withTableBorder withRowBorders> <Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push('/admin/keamanan/keamanan-lingkungan-pecalang-patwal/create')
}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
{/* Tabel */}
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Nama Keamanan Lingkungan</TableTh> <TableTh>Nama</TableTh>
<TableTh>Deskripsi</TableTh> <TableTh>Deskripsi</TableTh>
<TableTh>Detail</TableTh> <TableTh>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item) => ( {filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Box w={180}> <Text fw={500} truncate="end" lineClamp={1}>
<Text fz={"md"} truncate={"end"} lineClamp={1}>{item.name}</Text> {item.name}
</Box> </Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={250}> <Text fz="sm" c="dimmed" truncate lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
<Text fz={"md"} truncate={"end"} lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button onClick={() => router.push(`/admin/keamanan/keamanan-lingkungan-pecalang-patwal/${item.id}`)}> <Button
<IconDeviceImac size={20} /> variant="light"
color="blue"
onClick={() => router.push(`/admin/keamanan/keamanan-lingkungan-pecalang-patwal/${item.id}`)}
>
<IconDeviceImacCog size={20} />
<Text ml={5}>Detail</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} ))
) : (
<TableTr>
<TableTd colSpan={3}>
<Center py={20}>
<Text color="dimmed">
Tidak ada data keamanan lingkungan yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody> </TableTbody>
</Table> </Table>
</Box>
</Paper> </Paper>
{/* Pagination */}
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => load(newPage)} onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages} total={totalPages}
my={"md"} mt="md"
mb="md"
color="blue"
radius="md"
/> />
</Center> </Center>
</Box> </Box>

View File

@@ -1,26 +1,45 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import {
Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header'; import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import polsekTerdekat from '../../_state/keamanan/polsek-terdekat'; import polsekTerdekat from '../../_state/keamanan/polsek-terdekat';
import { useState } from 'react';
function PolsekTerdekat() { function PolsekTerdekat() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Polsek Terdekat' title='Polsek Terdekat'
placeholder='pencarian' placeholder='Cari nama atau alamat...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
/> />
<ListPolsekTerdekat search={search} /> <ListPolsekTerdekat search={search} />
</Box> </Box>
); );
@@ -47,60 +66,90 @@ function ListPolsekTerdekat({ search }: { search: string }) {
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
) )
} }
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<JudulList <Group justify="space-between" mb="md">
title='List Polsek Terdekat' <Title order={4}>Daftar Polsek Terdekat</Title>
href='/admin/keamanan/polsek-terdekat/create' <Tooltip label="Tambah Polsek" withArrow>
/> <Button
<Table striped withTableBorder withRowBorders> leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/keamanan/polsek-terdekat/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Nama Polsek Terdekat</TableTh> <TableTh>Nama Polsek</TableTh>
<TableTh>Jarak Polsek</TableTh> <TableTh>Jarak</TableTh>
<TableTh>Alamat</TableTh> <TableTh>Alamat</TableTh>
<TableTh>Detail</TableTh> <TableTh>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item) => ( {filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Box w={180}> <Text fw={500} truncate="end" lineClamp={1}>
<Text fz='md' truncate={"end"} lineClamp={1}>{item.nama}</Text> {item.nama}
</Box> </Text>
</TableTd> </TableTd>
<TableTd>{item.jarakKeDesa}</TableTd>
<TableTd>{item.alamat}</TableTd>
<TableTd> <TableTd>
<Box w={180}> <Button
<Text fz='md' truncate={"end"} lineClamp={1}>{item.jarakKeDesa}</Text> variant="light"
</Box> color="blue"
</TableTd> onClick={() => router.push(`/admin/keamanan/polsek-terdekat/${item.id}`)}
<TableTd> >
<Box w={250}> <IconDeviceImac size={18} />
<Text fz='md' truncate={"end"} lineClamp={1}>{item.alamat}</Text> <Text ml={5}>Detail</Text>
</Box>
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/keamanan/polsek-terdekat/${item.id}`)}>
<IconDeviceImac size={20} />
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} ))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">
Tidak ada data Polsek yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody> </TableTbody>
</Table> </Table>
</Box>
</Paper> </Paper>
{/* Pagination */}
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => load(newPage)} onChange={(newPage) => {
load(newPage, 10, search);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages} total={totalPages}
my="md" mt="md"
mb="md"
color="blue"
radius="md"
/> />
</Center> </Center>
</Box> </Box>

View File

@@ -1,77 +1,140 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core'; import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { IconActivity, IconBuildingHospital, IconCalendarEvent, IconGauge, IconNotes } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
function LayoutTabs({ children }: { children: React.ReactNode }) { function LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter() const router = useRouter();
const pathname = usePathname() const pathname = usePathname();
const tabs = [ const tabs = [
{ {
label: "Presentase Kelahiran & Kematian", label: "Presentase Kelahiran & Kematian",
value: "presentasekelahiran&kematian", value: "presentasekelahiran&kematian",
href: "/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian" href: "/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian",
icon: <IconActivity size={18} stroke={1.8} />,
tooltip: "Lihat data kelahiran dan kematian"
}, },
{ {
label: "Grafik Hasil Kepuasan Masyarakat Terhadap Pelayanan Publik", label: "Grafik Hasil Kepuasan Masyarakat",
value: "grafikhasilkepuasan", value: "grafikhasilkepuasan",
href: "/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan" href: "/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan",
icon: <IconGauge size={18} stroke={1.8} />,
tooltip: "Grafik kepuasan masyarakat terhadap pelayanan"
}, },
{ {
label: "Fasilitas Kesehatan", label: "Fasilitas Kesehatan",
value: "fasilitaskesehatan", value: "fasilitaskesehatan",
href: "/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan" href: "/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan",
icon: <IconBuildingHospital size={18} stroke={1.8} />,
tooltip: "Data fasilitas kesehatan desa"
}, },
{ {
label: "Jadwal Kegiatan", label: "Jadwal Kegiatan",
value: "jadwalkegiatan", value: "jadwalkegiatan",
href: "/admin/kesehatan/data-kesehatan-warga/jadwal_kegiatan" href: "/admin/kesehatan/data-kesehatan-warga/jadwal_kegiatan",
icon: <IconCalendarEvent size={18} stroke={1.8} />,
tooltip: "Atur jadwal kegiatan kesehatan"
}, },
{ {
label: "Artikel Kesehatan", label: "Artikel Kesehatan",
value: "artikelkesehatan", value: "artikelkesehatan",
href: "/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan" href: "/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan",
icon: <IconNotes size={18} stroke={1.8} />,
tooltip: "Artikel & informasi seputar kesehatan"
}, },
]; ];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
const currentTab = tabs.find(tab => tab.href === pathname);
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => { const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value) const tab = tabs.find(t => t.value === value);
if (tab) { if (tab) {
router.push(tab.href) router.push(tab.href);
}
setActiveTab(value)
} }
setActiveTab(value);
};
useEffect(() => { useEffect(() => {
const match = tabs.find(tab => tab.href === pathname) const match = tabs.find(tab => tab.href === pathname);
if (match) { if (match) {
setActiveTab(match.value) setActiveTab(match.value);
} }
}, [pathname]) }, [pathname]);
return ( return (
<Stack> <Stack gap="lg">
<Title order={3}>Data Kesehatan Warga</Title> <Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}> Data Kesehatan Warga
<TabsList p={"xs"} bg={"#BBC8E7FF"}> </Title>
{tabs.map((e, i) => ( <Tabs
<TabsTab key={i} value={e.value}>{e.label}</TabsTab> color={colors['blue-button']}
variant="pills"
value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
}}
>
{tabs.map((tab, i) => (
<Tooltip
key={i}
label={tab.tooltip}
position="bottom"
withArrow
transitionProps={{ transition: 'pop', duration: 200 }}
>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))} ))}
</TabsList> </TabsList>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value}>
{/* Konten dummy, bisa diganti tergantung routing */} {tabs.map((tab, i) => (
<></> <TabsPanel
key={i}
value={tab.value}
style={{
padding: "1.5rem",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
borderRadius: "1rem",
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
}}
>
{children}
</TabsPanel> </TabsPanel>
))} ))}
</Tabs> </Tabs>
{children}
</Stack> </Stack>
); );
} }
export default LayoutTabs; export default LayoutTabs;

View File

@@ -4,7 +4,17 @@
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
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 { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -14,29 +24,12 @@ import { useProxy } from 'valtio/utils';
interface ArtikelKesehatanFormBase { interface ArtikelKesehatanFormBase {
title: string; title: string;
content: string; content: string;
introduction: { introduction: { content: string };
content: string; symptom: { title: string; content: string };
}; prevention: { title: string; content: string };
symptom: { firstAid: { title: string; content: string };
title: string; mythVsFact: { title: string; mitos: string; fakta: string };
content: string; doctorSign: { content: string };
};
prevention: {
title: string;
content: string;
};
firstAid: {
title: string;
content: string;
};
mythVsFact: {
title: string;
mitos: string;
fakta: string;
};
doctorSign: {
content: string;
};
} }
function EditArtikelKesehatan() { function EditArtikelKesehatan() {
@@ -47,29 +40,27 @@ function EditArtikelKesehatan() {
const [formData, setFormData] = useState<ArtikelKesehatanFormBase>({ const [formData, setFormData] = useState<ArtikelKesehatanFormBase>({
title: stateArtikelKesehatan.edit.form.title || '', title: stateArtikelKesehatan.edit.form.title || '',
content: stateArtikelKesehatan.edit.form.content || '', content: stateArtikelKesehatan.edit.form.content || '',
introduction: { introduction: { content: stateArtikelKesehatan.edit.form.introduction?.content || '' },
content: stateArtikelKesehatan.edit.form.introduction?.content || '',
},
symptom: { symptom: {
title: stateArtikelKesehatan.edit.form.symptom?.title || '', title: stateArtikelKesehatan.edit.form.symptom?.title || '',
content: stateArtikelKesehatan.edit.form.symptom?.content || '', content: stateArtikelKesehatan.edit.form.symptom?.content || ''
}, },
prevention: { prevention: {
title: stateArtikelKesehatan.edit.form.prevention?.title || '', title: stateArtikelKesehatan.edit.form.prevention?.title || '',
content: stateArtikelKesehatan.edit.form.prevention?.content || '', content: stateArtikelKesehatan.edit.form.prevention?.content || ''
}, },
firstAid: { firstAid: {
title: stateArtikelKesehatan.edit.form.firstAid?.title || '', title: stateArtikelKesehatan.edit.form.firstAid?.title || '',
content: stateArtikelKesehatan.edit.form.firstAid?.content || '', content: stateArtikelKesehatan.edit.form.firstAid?.content || ''
}, },
mythVsFact: { mythVsFact: {
title: stateArtikelKesehatan.edit.form.mythVsFact?.title || '', title: stateArtikelKesehatan.edit.form.mythVsFact?.title || '',
mitos: stateArtikelKesehatan.edit.form.mythVsFact?.mitos || '', mitos: stateArtikelKesehatan.edit.form.mythVsFact?.mitos || '',
fakta: stateArtikelKesehatan.edit.form.mythVsFact?.fakta || '', fakta: stateArtikelKesehatan.edit.form.mythVsFact?.fakta || ''
}, },
doctorSign: { doctorSign: {
content: stateArtikelKesehatan.edit.form.doctorSign?.content || '', content: stateArtikelKesehatan.edit.form.doctorSign?.content || ''
}, }
}); });
useEffect(() => { useEffect(() => {
@@ -84,29 +75,27 @@ function EditArtikelKesehatan() {
setFormData({ setFormData({
title: form.title, title: form.title,
content: form.content, content: form.content,
introduction: { introduction: { content: form.introduction?.content || '' },
content: form.introduction?.content || '',
},
symptom: { symptom: {
title: form.symptom?.title || '', title: form.symptom?.title || '',
content: form.symptom?.content || '', content: form.symptom?.content || ''
}, },
prevention: { prevention: {
title: form.prevention?.title || '', title: form.prevention?.title || '',
content: form.prevention?.content || '', content: form.prevention?.content || ''
}, },
firstAid: { firstAid: {
title: form.firstAid?.title || '', title: form.firstAid?.title || '',
content: form.firstAid?.content || '', content: form.firstAid?.content || ''
}, },
mythVsFact: { mythVsFact: {
title: form.mythVsFact?.title || '', title: form.mythVsFact?.title || '',
mitos: form.mythVsFact?.mitos || '', mitos: form.mythVsFact?.mitos || '',
fakta: form.mythVsFact?.fakta || '', fakta: form.mythVsFact?.fakta || ''
}, },
doctorSign: { doctorSign: {
content: form.doctorSign?.content || '', content: form.doctorSign?.content || ''
}, }
}); });
} }
} catch (error) { } catch (error) {
@@ -119,34 +108,7 @@ function EditArtikelKesehatan() {
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
stateArtikelKesehatan.edit.form = { stateArtikelKesehatan.edit.form = { ...formData };
...stateArtikelKesehatan.edit.form,
title: formData.title,
content: formData.content,
introduction: {
content: formData.introduction.content,
},
symptom: {
title: formData.symptom.title,
content: formData.symptom.content,
},
prevention: {
title: formData.prevention.title,
content: formData.prevention.content,
},
firstAid: {
title: formData.firstAid.title,
content: formData.firstAid.content,
},
mythVsFact: {
title: formData.mythVsFact.title,
mitos: formData.mythVsFact.mitos,
fakta: formData.mythVsFact.fakta,
},
doctorSign: {
content: formData.doctorSign.content,
},
};
const success = await stateArtikelKesehatan.edit.submit(); const success = await stateArtikelKesehatan.edit.submit();
if (success) { if (success) {
toast.success("Artikel kesehatan berhasil diperbarui!"); toast.success("Artikel kesehatan berhasil diperbarui!");
@@ -157,214 +119,196 @@ function EditArtikelKesehatan() {
toast.error(error instanceof Error ? error.message : "Gagal memperbarui data artikel kesehatan"); toast.error(error instanceof Error ? error.message : "Gagal memperbarui data artikel kesehatan");
} }
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header */}
<Button onClick={() => router.back()} variant="subtle" color="blue"> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <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> </Button>
</Box> </Tooltip>
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}> <Title order={4} ml="sm" c="dark">
<Stack gap="xs"> Edit Artikel Kesehatan
<Title order={3}>Edit Artikel Kesehatan</Title> </Title>
</Group>
{/* Form */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Judul</Text>} label="Judul"
placeholder="masukkan judul" placeholder="Masukkan judul artikel"
value={formData.title} value={formData.title}
onChange={(e) => { onChange={(e) => setFormData(prev => ({ ...prev, title: e.target.value }))}
setFormData(prev => ({ required
...prev,
title: e.target.value
}));
}}
/> />
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Deskripsi</Text>} label="Deskripsi"
placeholder="masukkan deskripsi" placeholder="Masukkan deskripsi artikel"
value={formData.content} value={formData.content}
onChange={(e) => { onChange={(e) => setFormData(prev => ({ ...prev, content: e.target.value }))}
setFormData(prev => ({ required
...prev,
content: e.target.value
}));
}}
/> />
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Pendahuluan</Text>} label="Pendahuluan"
placeholder="masukkan pendahuluan" placeholder="Masukkan pendahuluan"
value={formData.introduction.content} value={formData.introduction.content}
onChange={(e) => { onChange={(e) =>
setFormData(prev => ({ setFormData(prev => ({
...prev, ...prev,
introduction: { introduction: { ...prev.introduction, content: e.target.value }
...prev.introduction, }))
content: e.target.value
} }
}));
}}
/> />
{/* Gejala */}
<Box> <Box>
<Text fz="md" fw="bold">Gejala</Text> <Text fw="bold">Gejala</Text>
<Stack gap="xs"> <Stack gap="xs">
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Judul</Text>} label="Judul Gejala"
placeholder="masukkan judul gejala penyakit" placeholder="Masukkan judul gejala"
value={formData.symptom.title} value={formData.symptom.title}
onChange={(e) => { onChange={(e) =>
setFormData(prev => ({ setFormData(prev => ({
...prev, ...prev,
symptom: { symptom: { ...prev.symptom, title: e.target.value }
...prev.symptom, }))
title: e.target.value
} }
}));
}}
/> />
<Box>
<Text fz="sm" fw="bold">Deskripsi Gejala</Text>
<EditEditor <EditEditor
value={formData.symptom.content} value={formData.symptom.content}
onChange={(e) => { onChange={(e) =>
setFormData(prev => ({ setFormData(prev => ({
...prev, ...prev,
symptom: { symptom: { ...prev.symptom, content: e }
...prev.symptom, }))
content: e
} }
}));
}}
/> />
</Box>
</Stack> </Stack>
</Box> </Box>
{/* Pencegahan */}
<Box> <Box>
<Text fz="md" fw="bold">Pencegahan</Text> <Text fw="bold">Pencegahan</Text>
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Judul</Text>} label="Judul"
placeholder="masukkan judul"
value={formData.prevention.title} value={formData.prevention.title}
onChange={(e) => { onChange={(e) =>
setFormData(prev => ({ setFormData(prev => ({
...prev, ...prev,
prevention: { prevention: { ...prev.prevention, title: e.target.value }
...prev.prevention, }))
title: e.target.value
} }
}));
}}
/> />
<EditEditor <EditEditor
value={formData.prevention.content} value={formData.prevention.content}
onChange={(e) => { onChange={(e) =>
setFormData(prev => ({ setFormData(prev => ({
...prev, ...prev,
prevention: { prevention: { ...prev.prevention, content: e }
...prev.prevention, }))
content: e
} }
}));
}}
/> />
</Box> </Box>
{/* Pertolongan Pertama */}
<Box> <Box>
<Text fz="md" fw="bold">Pertolongan Pertama</Text> <Text fw="bold">Pertolongan Pertama</Text>
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Judul</Text>} label="Judul"
placeholder="masukkan judul"
value={formData.firstAid.title} value={formData.firstAid.title}
onChange={(e) => { onChange={(e) =>
setFormData(prev => ({ setFormData(prev => ({
...prev, ...prev,
firstAid: { firstAid: { ...prev.firstAid, title: e.target.value }
...prev.firstAid, }))
title: e.target.value
} }
}));
}}
/> />
<EditEditor <EditEditor
value={formData.firstAid.content} value={formData.firstAid.content}
onChange={(e) => { onChange={(e) =>
setFormData(prev => ({ setFormData(prev => ({
...prev, ...prev,
firstAid: { firstAid: { ...prev.firstAid, content: e }
...prev.firstAid, }))
content: e
} }
}));
}}
/> />
</Box> </Box>
{/* Mitos vs Fakta */}
<Box> <Box>
<Text fz="md" fw="bold">Mitos dan Fakta</Text> <Text fw="bold">Mitos vs Fakta</Text>
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Judul</Text>} label="Judul"
placeholder="masukkan judul"
value={formData.mythVsFact.title} value={formData.mythVsFact.title}
onChange={(e) => { onChange={(e) =>
setFormData(prev => ({ setFormData(prev => ({
...prev, ...prev,
mythVsFact: { mythVsFact: { ...prev.mythVsFact, title: e.target.value }
...prev.mythVsFact, }))
title: e.target.value
} }
}));
}}
/> />
<Box> <Text fw="500">Mitos</Text>
<Text>
Mitos
</Text>
<EditEditor <EditEditor
value={formData.mythVsFact.mitos} value={formData.mythVsFact.mitos}
onChange={(e) => { onChange={(e) =>
setFormData(prev => ({ setFormData(prev => ({
...prev, ...prev,
mythVsFact: { mythVsFact: { ...prev.mythVsFact, mitos: e }
...prev.mythVsFact, }))
mitos: e
} }
}));
}}
/> />
</Box> <Text fw="500">Fakta</Text>
<Box>
<Text>
Fakta
</Text>
<EditEditor <EditEditor
value={formData.mythVsFact.fakta} value={formData.mythVsFact.fakta}
onChange={(e) => { onChange={(e) =>
setFormData(prev => ({ setFormData(prev => ({
...prev, ...prev,
mythVsFact: { mythVsFact: { ...prev.mythVsFact, fakta: e }
...prev.mythVsFact, }))
fakta: e
} }
}));
}}
/> />
</Box> </Box>
</Box>
{/* Kapan harus ke dokter */}
<Box> <Box>
<Text fz="md" fw="bold">Kapan Harus Ke Dokter</Text> <Text fw="bold">Kapan Harus Ke Dokter</Text>
<EditEditor <EditEditor
value={formData.doctorSign.content} value={formData.doctorSign.content}
onChange={(e) => { onChange={(e) =>
setFormData(prev => ({ setFormData(prev => ({
...prev, ...prev,
doctorSign: { doctorSign: { ...prev.doctorSign, content: e }
...prev.doctorSign, }))
content: e
} }
}));
}}
/> />
</Box> </Box>
<Button onClick={handleSubmit} bg={colors['blue-button']}>
{/* Save button */}
<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 Simpan
</Button> </Button>
</Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

@@ -2,134 +2,181 @@
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
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 { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Skeleton,
Stack,
Text,
Tooltip,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function DetailArtikelKesehatan() { function DetailArtikelKesehatan() {
const params = useParams() const params = useParams();
const router = useRouter(); const router = useRouter();
const stateArtikelKesehatan = useProxy(artikelKesehatanState) const state = useProxy(artikelKesehatanState);
const [modalHapus, setModalHapus] = useState(false); const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null);
useShallowEffect(() => { useShallowEffect(() => {
stateArtikelKesehatan.findUnique.load(params?.id as string) state.findUnique.load(params?.id as string);
}, []) }, []);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
stateArtikelKesehatan.delete.byId(selectedId) state.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan") router.push('/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan');
}
} }
};
if (!stateArtikelKesehatan.findUnique.data) { if (!state.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) );
} }
const data = state.findUnique.data;
return ( return (
<Box> <Box py={10}>
<Box mb={10}> {/* Tombol Back */}
<Button variant="subtle" onClick={() => router.back()}> <Button
<IconArrowBack color={colors['blue-button']} size={25} /> variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15}
>
Kembali
</Button> </Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}> {/* Wrapper Detail */}
<Stack> <Paper
<Text fz={"xl"} fw={"bold"}>Detail Artikel Kesehatan</Text> withBorder
{stateArtikelKesehatan.findUnique.data ? ( w={{ base: '100%', md: '50%' }}
<Paper bg={colors['BG-trans']} p={'md'}> bg={colors['white-1']}
<Stack gap={"xs"}> p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Artikel Kesehatan
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
{/* Judul */}
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Judul</Text> <Text fz="lg" fw="bold">Judul</Text>
<Text fz={"md"}>{stateArtikelKesehatan.findUnique.data.title}</Text> <Text fz="md" c="dimmed">{data.title}</Text>
</Box> </Box>
{/* Deskripsi */}
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text> <Text fz="lg" fw="bold">Deskripsi</Text>
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: stateArtikelKesehatan.findUnique.data.content }} /> <Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.content }} />
</Box> </Box>
{/* Pendahuluan */}
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Pendahuluan</Text> <Text fz="lg" fw="bold">Pendahuluan</Text>
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: stateArtikelKesehatan.findUnique.data.introduction.content }} /> <Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.introduction?.content }} />
</Box> </Box>
{/* Gejala */}
<Box> <Box>
<Stack gap={"xs"}> <Text fz="lg" fw="bold">Gejala</Text>
<Text fz={"lg"} fw={"bold"}>Gejala</Text> <Text fz="md" fw="bold">Judul</Text>
<Text fz={"md"} fw={"bold"}>Judul Gejala</Text> <Text fz="md" c="dimmed">{data.symptom?.title}</Text>
<Text fz={"md"}>{stateArtikelKesehatan.findUnique.data.symptom.title}</Text> <Text fz="md" fw="bold">Deskripsi</Text>
<Text fz={"md"} fw={"bold"}>Deskripsi Gejala</Text> <Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.symptom?.content }} />
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: stateArtikelKesehatan.findUnique.data.symptom.content }} />
</Stack>
</Box> </Box>
{/* Pencegahan */}
<Box> <Box>
<Stack gap={"xs"}> <Text fz="lg" fw="bold">Pencegahan</Text>
<Text fz={"lg"} fw={"bold"}>Pencegahan</Text> <Text fz="md" fw="bold">Judul</Text>
<Text fz={"md"} fw={"bold"}>Judul Pencegahan</Text> <Text fz="md" c="dimmed">{data.prevention?.title}</Text>
<Text fz={"md"}>{stateArtikelKesehatan.findUnique.data.prevention.title}</Text> <Text fz="md" fw="bold">Deskripsi</Text>
<Text fz={"md"} fw={"bold"}>Deskripsi Pencegahan</Text> <Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.prevention?.content }} />
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: stateArtikelKesehatan.findUnique.data.prevention.content }} />
</Stack>
</Box> </Box>
{/* Pertolongan Pertama */}
<Box> <Box>
<Stack gap={"xs"}> <Text fz="lg" fw="bold">Pertolongan Pertama</Text>
<Text fz={"lg"} fw={"bold"}>Pertolongan Pertama</Text> <Text fz="md" fw="bold">Judul</Text>
<Text fz={"md"} fw={"bold"}>Judul Pertolongan Pertama</Text> <Text fz="md" c="dimmed">{data.firstaid?.title}</Text>
<Text fz={"md"}>{stateArtikelKesehatan.findUnique.data.firstaid.title}</Text> <Text fz="md" fw="bold">Deskripsi</Text>
<Text fz={"md"} fw={"bold"}>Deskripsi Pertolongan Pertama</Text> <Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.firstaid?.content }} />
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: stateArtikelKesehatan.findUnique.data.firstaid.content }} />
</Stack>
</Box> </Box>
{/* Mitos vs Fakta */}
<Box> <Box>
<Stack gap={"xs"}> <Text fz="lg" fw="bold">Mitos dan Fakta</Text>
<Text fz={"lg"} fw={"bold"}>Mitos dan Fakta</Text> <Text fz="md" fw="bold">Judul</Text>
<Text fz={"md"} fw={"bold"}>Judul Mitos dan Fakta</Text> <Text fz="md" c="dimmed">{data.mythvsfact?.title}</Text>
<Text fz={"md"}>{stateArtikelKesehatan.findUnique.data.mythvsfact.title}</Text> <Text fz="md" fw="bold">Mitos</Text>
<Text fz={"md"} fw={"bold"}>Deskripsi Mitos</Text> <Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.mythvsfact?.mitos }} />
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: stateArtikelKesehatan.findUnique.data.mythvsfact.mitos }} /> <Text fz="md" fw="bold">Fakta</Text>
<Text fz={"md"} fw={"bold"}>Deskripsi Fakta</Text> <Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.mythvsfact?.fakta }} />
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: stateArtikelKesehatan.findUnique.data.mythvsfact.fakta }} />
</Stack>
</Box> </Box>
{/* Kapan ke Dokter */}
<Box> <Box>
<Stack gap={"xs"}> <Text fz="lg" fw="bold">Kapan Harus ke Dokter</Text>
<Text fz={"lg"} fw={"bold"}>Kapan Harus ke Dokter</Text> <Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.doctorsign?.content }} />
<Text fz={"md"} fw={"bold"}>Deskripsi Kapan Harus ke Dokter</Text>
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: stateArtikelKesehatan.findUnique.data.doctorsign.content }} />
</Stack>
</Box> </Box>
<Box>
<Flex gap={"xs"}> {/* Aksi */}
<Button color="red" onClick={() => { <Group gap="sm">
if (stateArtikelKesehatan.findUnique.data) { <Tooltip label="Hapus Artikel" withArrow position="top">
setSelectedId(stateArtikelKesehatan.findUnique.data.id) <Button
setModalHapus(true) color="red"
onClick={() => {
setSelectedId(data.id);
setModalHapus(true);
}}
variant="light"
radius="md"
size="md"
>
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Artikel" withArrow position="top">
<Button
color="green"
onClick={() =>
router.push(
`/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan/${data.id}/edit`
)
} }
}}> variant="light"
<IconX size={20} /> radius="md"
</Button> size="md"
<Button onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan/${stateArtikelKesehatan.findUnique.data?.id}/edit`)} color="green"> >
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Box> </Group>
</Stack> </Stack>
</Paper> </Paper>
) : null}
</Stack> </Stack>
</Paper> </Paper>
{/* Modal Hapus */} {/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}

View File

@@ -2,101 +2,129 @@
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
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 { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function CreateArtikelKesehatan() { function CreateArtikelKesehatan() {
const stateArtikelKesehatan = useProxy(artikelKesehatanState) const stateArtikelKesehatan = useProxy(artikelKesehatanState);
const router = useRouter(); const router = useRouter();
const resetForm = () => { const resetForm = () => {
stateArtikelKesehatan.create.form = { stateArtikelKesehatan.create.form = {
title: "", title: '',
content: "", content: '',
introduction: { introduction: {
content: "", content: '',
}, },
symptom: { symptom: {
title: "", title: '',
content: "", content: '',
}, },
prevention: { prevention: {
title: "", title: '',
content: "", content: '',
}, },
firstAid: { firstAid: {
title: "", title: '',
content: "", content: '',
}, },
mythVsFact: { mythVsFact: {
title: "", title: '',
mitos: "", mitos: '',
fakta: "", fakta: '',
}, },
doctorSign: { doctorSign: {
content: "" content: '',
} },
}; };
}; };
const handleSubmit = async (e?: React.FormEvent) => {
e?.preventDefault();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
await stateArtikelKesehatan.create.submit(); await stateArtikelKesehatan.create.submit();
toast.success('Data berhasil disimpan');
toast.success("Data berhasil disimpan");
resetForm(); resetForm();
// After successful submission, redirect to the list page
router.push('/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan'); router.push('/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan');
} };
return ( return (
<Box component="form" onSubmit={handleSubmit}> <Box px={{ base: 'sm', md: 'lg' }} py="md" component="form" onSubmit={handleSubmit}>
<Box mb={10}> {/* Header */}
<Button variant="subtle" onClick={() => router.back()}> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <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> </Button>
</Box> </Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Artikel Kesehatan
</Title>
</Group>
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}> {/* Form */}
<Stack gap="xs"> <Paper
<Title order={3}>Create Artikel Kesehatan</Title> w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Judul</Text>} label={"Judul"}
placeholder="masukkan judul" placeholder="Masukkan judul"
value={stateArtikelKesehatan.create.form.title} value={stateArtikelKesehatan.create.form.title}
onChange={(e) => { onChange={(e) => {
stateArtikelKesehatan.create.form.title = e.target.value; stateArtikelKesehatan.create.form.title = e.target.value;
}} }}
required
/> />
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Deskripsi</Text>} label={"Deskripsi"}
placeholder="masukkan deskripsi" placeholder="Masukkan deskripsi"
value={stateArtikelKesehatan.create.form.content} value={stateArtikelKesehatan.create.form.content}
onChange={(e) => { onChange={(e) => {
stateArtikelKesehatan.create.form.content = e.target.value; stateArtikelKesehatan.create.form.content = e.target.value;
}} }}
required
/> />
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Pendahuluan</Text>} label={"Pendahuluan"}
placeholder="masukkan pendahuluan" placeholder="Masukkan pendahuluan"
required
value={stateArtikelKesehatan.create.form.introduction.content} value={stateArtikelKesehatan.create.form.introduction.content}
onChange={(e) => { onChange={(e) => {
stateArtikelKesehatan.create.form.introduction.content = e.target.value; stateArtikelKesehatan.create.form.introduction.content = e.target.value;
}} }}
/> />
{/* Gejala */}
<Box> <Box>
<Text fz="md" fw="bold">Gejala</Text> <Text fz="md" fw="bold">Gejala</Text>
<Stack gap="xs"> <Stack gap="sm">
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Judul</Text>} label={"Judul Gejala"}
placeholder="masukkan judul gejala penyakit" required
placeholder="Masukkan judul gejala penyakit"
value={stateArtikelKesehatan.create.form.symptom.title} value={stateArtikelKesehatan.create.form.symptom.title}
onChange={(e) => { onChange={(e) => {
stateArtikelKesehatan.create.form.symptom.title = e.target.value; stateArtikelKesehatan.create.form.symptom.title = e.target.value;
@@ -114,54 +142,62 @@ function CreateArtikelKesehatan() {
</Stack> </Stack>
</Box> </Box>
<Box> {/* Pencegahan */}
<Stack gap="xs">
<Text fz="md" fw="bold">Pencegahan</Text> <Text fz="md" fw="bold">Pencegahan</Text>
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Judul</Text>} label={"Judul Pencegahan"}
placeholder="masukkan judul" required
placeholder="Masukkan judul"
value={stateArtikelKesehatan.create.form.prevention.title} value={stateArtikelKesehatan.create.form.prevention.title}
onChange={(e) => { onChange={(e) => {
stateArtikelKesehatan.create.form.prevention.title = e.target.value; stateArtikelKesehatan.create.form.prevention.title = e.target.value;
}} }}
/> />
<Text fz="sm">Deskripsi Pencegahan</Text>
<CreateEditor <CreateEditor
value={stateArtikelKesehatan.create.form.prevention.content} value={stateArtikelKesehatan.create.form.prevention.content}
onChange={(e) => { onChange={(e) => {
stateArtikelKesehatan.create.form.prevention.content = e; stateArtikelKesehatan.create.form.prevention.content = e;
}} }}
/> />
</Box> </Stack>
<Box>
{/* Pertolongan Pertama */}
<Stack gap={"xs"}>
<Text fz="md" fw="bold">Pertolongan Pertama</Text> <Text fz="md" fw="bold">Pertolongan Pertama</Text>
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Judul</Text>} label={"Judul Pertolongan Pertama"}
placeholder="masukkan judul" required
placeholder="Masukkan judul"
value={stateArtikelKesehatan.create.form.firstAid.title} value={stateArtikelKesehatan.create.form.firstAid.title}
onChange={(e) => { onChange={(e) => {
stateArtikelKesehatan.create.form.firstAid.title = e.target.value; stateArtikelKesehatan.create.form.firstAid.title = e.target.value;
}} }}
/> />
<Text fz="sm">Deskripsi Pertolongan Pertama</Text>
<CreateEditor <CreateEditor
value={stateArtikelKesehatan.create.form.firstAid.content} value={stateArtikelKesehatan.create.form.firstAid.content}
onChange={(e) => { onChange={(e) => {
stateArtikelKesehatan.create.form.firstAid.content = e; stateArtikelKesehatan.create.form.firstAid.content = e;
}} }}
/> />
</Box> </Stack>
{/* Mitos vs Fakta */}
<Box> <Box>
<Text fz="md" fw="bold">Mitos dan Fakta</Text> <Text fz="md" fw="bold">Mitos dan Fakta</Text>
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Judul</Text>} label={"Judul Mitos dan Fakta"}
placeholder="masukkan judul" required
placeholder="Masukkan judul"
value={stateArtikelKesehatan.create.form.mythVsFact.title} value={stateArtikelKesehatan.create.form.mythVsFact.title}
onChange={(e) => { onChange={(e) => {
stateArtikelKesehatan.create.form.mythVsFact.title = e.target.value; stateArtikelKesehatan.create.form.mythVsFact.title = e.target.value;
}} }}
/> />
<Box> <Box mt="sm">
<Text> <Text fz="sm" fw="bold">Mitos</Text>
Mitos
</Text>
<CreateEditor <CreateEditor
value={stateArtikelKesehatan.create.form.mythVsFact.mitos} value={stateArtikelKesehatan.create.form.mythVsFact.mitos}
onChange={(e) => { onChange={(e) => {
@@ -169,10 +205,8 @@ function CreateArtikelKesehatan() {
}} }}
/> />
</Box> </Box>
<Box> <Box mt="sm">
<Text> <Text fz="sm" fw="bold">Fakta</Text>
Fakta
</Text>
<CreateEditor <CreateEditor
value={stateArtikelKesehatan.create.form.mythVsFact.fakta} value={stateArtikelKesehatan.create.form.mythVsFact.fakta}
onChange={(e) => { onChange={(e) => {
@@ -181,8 +215,10 @@ function CreateArtikelKesehatan() {
/> />
</Box> </Box>
</Box> </Box>
{/* Kapan Harus ke Dokter */}
<Box> <Box>
<Text fz="md" fw="bold">Kapan Harus Ke Dokter</Text> <Text fz="md" fw="bold">Kapan Harus ke Dokter</Text>
<CreateEditor <CreateEditor
value={stateArtikelKesehatan.create.form.doctorSign.content} value={stateArtikelKesehatan.create.form.doctorSign.content}
onChange={(e) => { onChange={(e) => {
@@ -191,9 +227,21 @@ function CreateArtikelKesehatan() {
/> />
</Box> </Box>
<Button onClick={handleSubmit} bg={colors['blue-button']}> {/* Submit Button */}
<Group justify="right">
<Button
type="submit"
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 Simpan
</Button> </Button>
</Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

@@ -1,99 +1,164 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import {
Box,
Button,
Center,
Group,
Paper,
Pagination,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; import { IconArrowBack, IconDeviceImacCog, IconPlus, IconSearch } 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 HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import artikelKesehatanState from '../../../_state/kesehatan/data_kesehatan_warga/artikelKesehatan'; import artikelKesehatanState from '../../../_state/kesehatan/data_kesehatan_warga/artikelKesehatan';
import { useState } from 'react'; import { useState } from 'react';
function ArtikelKesehatan() { function ArtikelKesehatan() {
const router = useRouter();
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
return ( return (
<Box> <Box>
{/* Tombol Back */}
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors["blue-button"]} size={25} />
</Button>
</Box>
{/* Header Search */}
<HeaderSearch <HeaderSearch
title='Artikel Kesehatan' title='Artikel Kesehatan'
placeholder='pencarian' placeholder='Cari judul atau konten...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
/> />
<ListArtikelKesehatan search={search} /> <ListArtikelKesehatan search={search} />
</Box> </Box>
); );
} }
function ListArtikelKesehatan({ search }: { search: string }) { function ListArtikelKesehatan({ search }: { search: string }) {
const stateArtikelKesehatan = useProxy(artikelKesehatanState) const stateArtikel = useProxy(artikelKesehatanState);
const router = useRouter(); const router = useRouter();
const { data, page, totalPages, loading, load } = stateArtikel.findMany;
useShallowEffect(() => { useShallowEffect(() => {
stateArtikelKesehatan.findMany.load() load(page, 10, search);
}, []) }, [page, search]);
const filteredData = (stateArtikelKesehatan.findMany.data || []).filter(item => { const filteredData = data || [];
const keyword = search.toLowerCase();
if (loading || !data) {
return ( return (
item.title.toLowerCase().includes(keyword) || <Stack py={10}>
item.content.toLowerCase().includes(keyword) <Skeleton height={600} radius="md" />
</Stack>
); );
});
if (!stateArtikelKesehatan.findMany.data) {
return (
<Box py={10}>
<Skeleton h={500} />
</Box>
)
} }
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<Stack> {/* Judul + Tombol Tambah */}
<JudulList <Group justify="space-between" mb="md">
title='List Artikel Kesehatan' <Title order={4}>Daftar Artikel Kesehatan</Title>
href='/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan/create' <Tooltip label="Tambah Artikel Kesehatan" withArrow>
/> <Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
{/* Tabel */}
<Box style={{ overflowX: "auto" }}> <Box style={{ overflowX: "auto" }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}> <Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Judul</TableTh> <TableTh>Judul</TableTh>
<TableTh>Content</TableTh> <TableTh>Konten</TableTh>
<TableTh>Detail</TableTh> <TableTh>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item) => ( {filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Box w={100}> <Text fw={500} truncate="end" lineClamp={1}>
<Text truncate={'end'} lineClamp={1} fz={'h5'}>{item.title}</Text> {item.title}
</Box> </Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={200}> <Text truncate fz="sm" c="dimmed" lineClamp={1}>
<Text truncate={'end'} lineClamp={1} fz={'h5'}>{item.content}</Text> {item.content}
</Box> </Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan/${item.id}`)}> <Button
<IconDeviceImacCog size={25} /> variant="light"
color="blue"
onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan/${item.id}`)}
>
<IconDeviceImacCog size={20} />
<Text ml={5}>Detail</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} ))
) : (
<TableTr>
<TableTd colSpan={3}>
<Center py={20}>
<Text color="dimmed">Tidak ada artikel yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
</Stack>
</Paper> </Paper>
{/* Pagination */}
<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> </Box>
) );
} }
export default ArtikelKesehatan; export default ArtikelKesehatan;

View File

@@ -4,7 +4,17 @@
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
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 { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -18,20 +28,14 @@ interface FasilitasKesehatanFormBase {
alamat: string; alamat: string;
jamOperasional: string; jamOperasional: string;
}; };
layananUnggulan: { layananUnggulan: { content: string };
content: string;
};
dokterdanTenagaMedis: { dokterdanTenagaMedis: {
name: string; name: string;
specialist: string; specialist: string;
jadwal: string; jadwal: string;
}; };
fasilitasPendukung: { fasilitasPendukung: { content: string };
content: string; prosedurPendaftaran: { content: string };
};
prosedurPendaftaran: {
content: string;
};
tarifDanLayanan: { tarifDanLayanan: {
layanan: string; layanan: string;
tarif: string; tarif: string;
@@ -86,20 +90,14 @@ function EditFasilitasKesehatan() {
alamat: form.informasiUmum?.alamat || '', alamat: form.informasiUmum?.alamat || '',
jamOperasional: form.informasiUmum?.jamOperasional || '', jamOperasional: form.informasiUmum?.jamOperasional || '',
}, },
layananUnggulan: { layananUnggulan: { content: form.layananUnggulan?.content || '' },
content: form.layananUnggulan?.content || '',
},
dokterdanTenagaMedis: { dokterdanTenagaMedis: {
name: form.dokterdanTenagaMedis?.name || '', name: form.dokterdanTenagaMedis?.name || '',
specialist: form.dokterdanTenagaMedis?.specialist || '', specialist: form.dokterdanTenagaMedis?.specialist || '',
jadwal: form.dokterdanTenagaMedis?.jadwal || '', jadwal: form.dokterdanTenagaMedis?.jadwal || '',
}, },
fasilitasPendukung: { fasilitasPendukung: { content: form.fasilitasPendukung?.content || '' },
content: form.fasilitasPendukung?.content || '', prosedurPendaftaran: { content: form.prosedurPendaftaran?.content || '' },
},
prosedurPendaftaran: {
content: form.prosedurPendaftaran?.content || '',
},
tarifDanLayanan: { tarifDanLayanan: {
layanan: form.tarifDanLayanan?.layanan || '', layanan: form.tarifDanLayanan?.layanan || '',
tarif: form.tarifDanLayanan?.tarif || '', tarif: form.tarifDanLayanan?.tarif || '',
@@ -118,30 +116,7 @@ function EditFasilitasKesehatan() {
try { try {
stateFasilitasKesehatan.edit.form = { stateFasilitasKesehatan.edit.form = {
...stateFasilitasKesehatan.edit.form, ...stateFasilitasKesehatan.edit.form,
name: formData.name, ...formData,
informasiUmum: {
fasilitas: formData.informasiUmum.fasilitas,
alamat: formData.informasiUmum.alamat,
jamOperasional: formData.informasiUmum.jamOperasional,
},
layananUnggulan: {
content: formData.layananUnggulan.content,
},
dokterdanTenagaMedis: {
name: formData.dokterdanTenagaMedis.name,
specialist: formData.dokterdanTenagaMedis.specialist,
jadwal: formData.dokterdanTenagaMedis.jadwal,
},
fasilitasPendukung: {
content: formData.fasilitasPendukung.content,
},
prosedurPendaftaran: {
content: formData.prosedurPendaftaran.content,
},
tarifDanLayanan: {
layanan: formData.tarifDanLayanan.layanan,
tarif: formData.tarifDanLayanan.tarif,
},
}; };
const success = await stateFasilitasKesehatan.edit.submit(); const success = await stateFasilitasKesehatan.edit.submit();
if (success) { if (success) {
@@ -150,208 +125,197 @@ function EditFasilitasKesehatan() {
} }
} catch (error) { } catch (error) {
console.error("Error updating fasilitas kesehatan:", error); console.error("Error updating fasilitas kesehatan:", error);
toast.error(error instanceof Error ? error.message : "Gagal memperbarui data fasilitas kesehatan"); toast.error("Terjadi kesalahan saat memperbarui data fasilitas kesehatan");
} }
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header */}
<Button onClick={() => router.back()} variant="subtle" color="blue"> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <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> </Button>
</Box> </Tooltip>
<Stack gap="xs"> <Title order={4} ml="sm" c="dark">
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}> Edit Fasilitas Kesehatan
<Stack gap="xs"> </Title>
<Title order={3}>Edit Fasilitas Kesehatan</Title> </Group>
{/* Form */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Nama Fasilitas Kesehatan</Text>} label="Nama Fasilitas Kesehatan"
placeholder="masukkan nama fasilitas kesehatan" placeholder="Masukkan nama fasilitas kesehatan"
value={formData.name} value={formData.name}
onChange={(e) => { onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
setFormData(prev => ({ required
...prev,
name: e.target.value
}));
}}
/> />
{/* Informasi Umum */}
<Box> <Box>
<Text fz="md" fw="bold">Informasi Umum</Text> <Text fw="bold" mb={5}>Informasi Umum</Text>
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Fasilitas</Text>} label="Fasilitas"
placeholder="masukkan fasilitas"
value={formData.informasiUmum.fasilitas} value={formData.informasiUmum.fasilitas}
onChange={(e) => { onChange={(e) =>
setFormData(prev => ({ setFormData(prev => ({
...prev, ...prev,
informasiUmum: { informasiUmum: { ...prev.informasiUmum, fasilitas: e.target.value },
...prev.informasiUmum, }))
fasilitas: e.target.value
} }
}));
}}
/> />
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Alamat</Text>} label="Alamat"
placeholder="masukkan alamat"
value={formData.informasiUmum.alamat} value={formData.informasiUmum.alamat}
onChange={(e) => { onChange={(e) =>
setFormData(prev => ({ setFormData(prev => ({
...prev, ...prev,
informasiUmum: { informasiUmum: { ...prev.informasiUmum, alamat: e.target.value },
...prev.informasiUmum, }))
alamat: e.target.value
} }
}));
}}
/> />
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Jam Operasional</Text>} label="Jam Operasional"
placeholder="masukkan jam operasional"
value={formData.informasiUmum.jamOperasional} value={formData.informasiUmum.jamOperasional}
onChange={(e) => { onChange={(e) =>
setFormData(prev => ({ setFormData(prev => ({
...prev, ...prev,
informasiUmum: { informasiUmum: { ...prev.informasiUmum, jamOperasional: e.target.value },
...prev.informasiUmum, }))
jamOperasional: e.target.value
} }
}));
}}
/>
</Box>
<Box>
<Text fz="md" fw="bold">Layanan Unggulan</Text>
<EditEditor
value={formData.layananUnggulan.content}
onChange={(e) => {
setFormData(prev => ({
...prev,
layananUnggulan: {
...prev.layananUnggulan,
content: e
}
}));
}}
/>
</Box>
<Box>
<Text fz="md" fw="bold">Dokter dan Tenaga Medis</Text>
<TextInput
label={<Text fz="sm" fw="bold">Nama Dokter</Text>}
placeholder="masukkan nama dokter"
value={formData.dokterdanTenagaMedis.name}
onChange={(e) => {
setFormData(prev => ({
...prev,
dokterdanTenagaMedis: {
...prev.dokterdanTenagaMedis,
name: e.target.value
}
}));
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Specialist</Text>}
placeholder="masukkan specialist"
value={formData.dokterdanTenagaMedis.specialist}
onChange={(e) => {
setFormData(prev => ({
...prev,
dokterdanTenagaMedis: {
...prev.dokterdanTenagaMedis,
specialist: e.target.value
}
}));
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Jadwal</Text>}
placeholder="masukkan jadwal"
value={formData.dokterdanTenagaMedis.jadwal}
onChange={(e) => {
setFormData(prev => ({
...prev,
dokterdanTenagaMedis: {
...prev.dokterdanTenagaMedis,
jadwal: e.target.value
}
}));
}}
/>
</Box>
<Box>
<Text fz="md" fw="bold">Fasilitas Pendukung</Text>
<EditEditor
value={formData.fasilitasPendukung.content}
onChange={(e) => {
setFormData(prev => ({
...prev,
fasilitasPendukung: {
...prev.fasilitasPendukung,
content: e
}
}));
}}
/>
</Box>
<Box>
<Text fz="md" fw="bold">Prosedur Pendaftaran</Text>
<EditEditor
value={formData.prosedurPendaftaran.content}
onChange={(e) => {
setFormData(prev => ({
...prev,
prosedurPendaftaran: {
...prev.prosedurPendaftaran,
content: e
}
}));
}}
/>
</Box>
<Box>
<Text fz="md" fw="bold">Tarif dan Layanan</Text>
<TextInput
label={<Text fz="sm" fw="bold">Tarif</Text>}
placeholder="masukkan tarif"
value={formData.tarifDanLayanan.tarif}
onChange={(e) => {
setFormData(prev => ({
...prev,
tarifDanLayanan: {
...prev.tarifDanLayanan,
tarif: e.target.value
}
}));
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Layanan</Text>}
placeholder="masukkan layanan"
value={formData.tarifDanLayanan.layanan}
onChange={(e) => {
setFormData(prev => ({
...prev,
tarifDanLayanan: {
...prev.tarifDanLayanan,
layanan: e.target.value
}
}));
}}
/> />
</Box> </Box>
{/* Layanan Unggulan */}
<Box>
<Text fw="bold" mb={5}>Layanan Unggulan</Text>
<EditEditor
value={formData.layananUnggulan.content}
onChange={(e) =>
setFormData(prev => ({
...prev,
layananUnggulan: { content: e },
}))
}
/>
</Box>
{/* Dokter dan Tenaga Medis */}
<Box>
<Text fw="bold" mb={5}>Dokter dan Tenaga Medis</Text>
<TextInput
label="Nama Dokter"
value={formData.dokterdanTenagaMedis.name}
onChange={(e) =>
setFormData(prev => ({
...prev,
dokterdanTenagaMedis: { ...prev.dokterdanTenagaMedis, name: e.target.value },
}))
}
/>
<TextInput
label="Specialist"
value={formData.dokterdanTenagaMedis.specialist}
onChange={(e) =>
setFormData(prev => ({
...prev,
dokterdanTenagaMedis: { ...prev.dokterdanTenagaMedis, specialist: e.target.value },
}))
}
/>
<TextInput
label="Jadwal"
value={formData.dokterdanTenagaMedis.jadwal}
onChange={(e) =>
setFormData(prev => ({
...prev,
dokterdanTenagaMedis: { ...prev.dokterdanTenagaMedis, jadwal: e.target.value },
}))
}
/>
</Box>
{/* Fasilitas Pendukung */}
<Box>
<Text fw="bold" mb={5}>Fasilitas Pendukung</Text>
<EditEditor
value={formData.fasilitasPendukung.content}
onChange={(e) =>
setFormData(prev => ({
...prev,
fasilitasPendukung: { content: e },
}))
}
/>
</Box>
{/* Prosedur Pendaftaran */}
<Box>
<Text fw="bold" mb={5}>Prosedur Pendaftaran</Text>
<EditEditor
value={formData.prosedurPendaftaran.content}
onChange={(e) =>
setFormData(prev => ({
...prev,
prosedurPendaftaran: { content: e },
}))
}
/>
</Box>
{/* Tarif dan Layanan */}
<Box>
<Text fw="bold" mb={5}>Tarif dan Layanan</Text>
<TextInput
label="Tarif"
value={formData.tarifDanLayanan.tarif}
onChange={(e) =>
setFormData(prev => ({
...prev,
tarifDanLayanan: { ...prev.tarifDanLayanan, tarif: e.target.value },
}))
}
/>
<TextInput
label="Layanan"
value={formData.tarifDanLayanan.layanan}
onChange={(e) =>
setFormData(prev => ({
...prev,
tarifDanLayanan: { ...prev.tarifDanLayanan, layanan: e.target.value },
}))
}
/>
</Box>
{/* Tombol Simpan */}
<Group justify="flex-end">
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
bg={colors['blue-button']} radius="md"
size="md"
loading={stateFasilitasKesehatan.edit.loading} loading={stateFasilitasKesehatan.edit.loading}
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
> >
Simpan Simpan
</Button> </Button>
</Group>
</Stack> </Stack>
</Paper> </Paper>
</Stack>
</Box> </Box>
); );
} }

View File

@@ -2,133 +2,169 @@
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
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 { Box, Button, Flex, Grid, GridCol, Paper, Skeleton, Stack, Text } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Skeleton,
Stack,
Text,
Tooltip,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function DetailFasilitasKesehatan() { function DetailFasilitasKesehatan() {
const params = useParams() const params = useParams();
const router = useRouter(); const router = useRouter();
const stateFasilitasKesehatan = useProxy(fasilitasKesehatanState.fasilitasKesehatan) const state = useProxy(fasilitasKesehatanState.fasilitasKesehatan);
const [modalHapus, setModalHapus] = useState(false); const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null);
useShallowEffect(() => { useShallowEffect(() => {
stateFasilitasKesehatan.findUnique.load(params?.id as string) state.findUnique.load(params?.id as string);
}, []) }, []);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
stateFasilitasKesehatan.delete.byId(selectedId) state.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan") router.push(
} '/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan'
);
} }
};
if (!stateFasilitasKesehatan.findUnique.data) { if (!state.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) );
} }
const data = state.findUnique.data;
return ( return (
<Box> <Box py={10}>
<Box mb={10}> {/* Tombol Back */}
<Button variant="subtle" onClick={() => router.back()}> <Button
<IconArrowBack color={colors['blue-button']} size={25} /> variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15}
>
Kembali
</Button> </Button>
{/* Wrapper Detail */}
<Paper
withBorder
w={{ base: '100%', md: '70%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Fasilitas Kesehatan
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz="lg" fw="bold">Nama Fasilitas</Text>
<Text fz="md" c="dimmed">{data.name || '-'}</Text>
</Box> </Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack> <Box>
<Grid> <Text fz="lg" fw="bold">Informasi Umum</Text>
<GridCol span={12}> <Text fz="md" fw="bold">Fasilitas</Text>
<Text fz={"xl"} fw={"bold"}>Detail Fasilitas Kesehatan</Text> <Text fz="md" c="dimmed">{data.informasiumum?.fasilitas || '-'}</Text>
</GridCol> <Text fz="md" fw="bold">Alamat</Text>
{/* <GridCol span={12}> <Text fz="md" c="dimmed">{data.informasiumum?.alamat || '-'}</Text>
<Flex gap={"xs"}> <Text fz="md" fw="bold">Jam Operasional</Text>
<Button color={colors['blue-button']} onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/${params?.id}/dokter-tenaga-medis`)}> <Text fz="md" c="dimmed">{data.informasiumum?.jamOperasional || '-'}</Text>
Tambah Dokter </Box>
<Box>
<Text fz="lg" fw="bold">Layanan Unggulan</Text>
<Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.layananunggulan?.content || '-' }} />
</Box>
<Box>
<Text fz="lg" fw="bold">Fasilitas Pendukung</Text>
<Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.fasilitaspendukung?.content || '-' }} />
</Box>
<Box>
<Text fz="lg" fw="bold">Prosedur Pendaftaran</Text>
<Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.prosedurpendaftaran?.content || '-' }} />
</Box>
<Box>
<Text fz="lg" fw="bold">Dokter & Tenaga Medis</Text>
<Text fz="md" fw="bold">Nama</Text>
<Text fz="md" c="dimmed">{data.dokterdantenagamedis?.name || '-'}</Text>
<Text fz="md" fw="bold">Spesialis</Text>
<Text fz="md" c="dimmed">{data.dokterdantenagamedis?.specialist || '-'}</Text>
<Text fz="md" fw="bold">Jadwal</Text>
<Text fz="md" c="dimmed">{data.dokterdantenagamedis?.jadwal || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Tarif & Layanan</Text>
<Text fz="md" fw="bold">Layanan</Text>
<Text fz="md" c="dimmed">{data.tarifdanlayanan?.layanan || '-'}</Text>
<Text fz="md" fw="bold">Tarif</Text>
<Text fz="md" c="dimmed">{data.tarifdanlayanan?.tarif || '-'}</Text>
</Box>
{/* Aksi */}
<Group gap="sm">
<Tooltip label="Hapus Data" withArrow position="top">
<Button
color="red"
onClick={() => {
setSelectedId(data.id);
setModalHapus(true);
}}
variant="light"
radius="md"
size="md"
>
<IconTrash size={20} />
</Button> </Button>
<Button color={colors['blue-button']} onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/${params?.id}/layanan-unggulan/create`)}> </Tooltip>
Tambah Layanan
</Button> <Tooltip label="Edit Data" withArrow position="top">
</Flex> <Button
</GridCol> */} color="green"
</Grid> onClick={() =>
{stateFasilitasKesehatan.findUnique.data ? ( router.push(
<Paper bg={colors['BG-trans']} p={'md'}> `/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/${data.id}/edit`
<Stack gap={"xs"}> )
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Fasilitas Kesehatan</Text>
<Text fz={"md"}>{stateFasilitasKesehatan.findUnique.data.name}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Informasi Umum</Text>
<Text fz={"md"} fw={"bold"}>Fasilitas</Text>
<Text fz={"md"}>{stateFasilitasKesehatan.findUnique.data.informasiumum.fasilitas}</Text>
<Text fz={"md"} fw={"bold"}>Alamat</Text>
<Text fz={"md"}>{stateFasilitasKesehatan.findUnique.data.informasiumum.alamat}</Text>
<Text fz={"md"} fw={"bold"}>Jam Operasional</Text>
<Text fz={"md"}>{stateFasilitasKesehatan.findUnique.data.informasiumum.jamOperasional}</Text>
</Box>
<Box>
<Text fz={"md"} fw={"bold"}>Layanan Unggulan</Text>
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: stateFasilitasKesehatan.findUnique.data.layananunggulan.content }} />
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Fasilitas Pendukung</Text>
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: stateFasilitasKesehatan.findUnique.data.fasilitaspendukung.content }} />
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Prosedur Pendaftaran</Text>
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: stateFasilitasKesehatan.findUnique.data.prosedurpendaftaran.content }} />
</Box>
<Box>
<Text fz={"md"} fw={"bold"}>Dokter dan Tenaga Medis</Text>
<Text fz={"md"} fw={"bold"}>Nama Dokter</Text>
<Text fz={"md"}>{stateFasilitasKesehatan.findUnique.data.dokterdantenagamedis.name}</Text>
<Text fz={"md"} fw={"bold"}>Specialist</Text>
<Text fz={"md"}>{stateFasilitasKesehatan.findUnique.data.dokterdantenagamedis.specialist}</Text>
<Text fz={"md"} fw={"bold"}>Jadwal</Text>
<Text fz={"md"}>{stateFasilitasKesehatan.findUnique.data.dokterdantenagamedis.jadwal}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Tarif dan Layanan</Text>
<Text fz={"md"} fw={"bold"}>Layanan</Text>
<Text fz={"md"}>{stateFasilitasKesehatan.findUnique.data.tarifdanlayanan.layanan}</Text>
<Text fz={"md"} fw={"bold"}>Tarif</Text>
<Text fz={"md"}>{stateFasilitasKesehatan.findUnique.data.tarifdanlayanan.tarif}</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red" onClick={() => {
if (stateFasilitasKesehatan.findUnique.data) {
setSelectedId(stateFasilitasKesehatan.findUnique.data.id)
setModalHapus(true)
} }
}}> variant="light"
<IconX size={20} /> radius="md"
</Button> size="md"
<Button onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/${stateFasilitasKesehatan.findUnique.data?.id}/edit`)} color="green"> >
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Box> </Group>
</Stack> </Stack>
</Paper> </Paper>
) : null}
</Stack> </Stack>
</Paper> </Paper>
{/* Modal Hapus */} {/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}

View File

@@ -2,7 +2,17 @@
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
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 { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -10,174 +20,196 @@ import { useProxy } from 'valtio/utils';
function CreateFasilitasKesehatan() { function CreateFasilitasKesehatan() {
const stateFasilitasKesehatan = useProxy(fasilitasKesehatanState.fasilitasKesehatan) const stateFasilitasKesehatan = useProxy(fasilitasKesehatanState.fasilitasKesehatan);
const router = useRouter(); const router = useRouter();
const resetForm = () => { const resetForm = () => {
stateFasilitasKesehatan.create.form = { stateFasilitasKesehatan.create.form = {
name: "", name: '',
informasiUmum: { informasiUmum: {
fasilitas: "", fasilitas: '',
alamat: "", alamat: '',
jamOperasional: "", jamOperasional: '',
}, },
layananUnggulan: { layananUnggulan: {
content: "", content: '',
}, },
dokterdanTenagaMedis: { dokterdanTenagaMedis: {
name: "", name: '',
specialist: "", specialist: '',
jadwal: "", jadwal: '',
}, },
fasilitasPendukung: { fasilitasPendukung: {
content: "", content: '',
}, },
prosedurPendaftaran: { prosedurPendaftaran: {
content: "", content: '',
}, },
tarifDanLayanan: { tarifDanLayanan: {
layanan: "", layanan: '',
tarif: "", tarif: '',
}, },
}; };
}; };
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
await stateFasilitasKesehatan.create.submit(); await stateFasilitasKesehatan.create.submit();
toast.success('Data berhasil disimpan');
toast.success("Data berhasil disimpan");
resetForm(); resetForm();
// After successful submission, redirect to the list page
router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan'); router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan');
} };
return ( return (
<Box component="form" onSubmit={handleSubmit}> <Box px={{ base: 'sm', md: 'lg' }} py="md" component="form" onSubmit={handleSubmit}>
<Box mb={10}> {/* Header */}
<Button variant="subtle" onClick={() => router.back()}> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <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> </Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Data Fasilitas Kesehatan
</Title>
</Group>
{/* Form */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label={"Nama Fasilitas Kesehatan"}
placeholder="Masukkan nama fasilitas kesehatan"
value={stateFasilitasKesehatan.create.form.name}
onChange={(e) => (stateFasilitasKesehatan.create.form.name = e.target.value)}
required
/>
{/* Informasi Umum */}
<Box>
<Text fz="md" fw="bold" mb={5}>Informasi Umum</Text>
<TextInput
label="Fasilitas"
placeholder="Masukkan fasilitas"
value={stateFasilitasKesehatan.create.form.informasiUmum.fasilitas}
onChange={(e) => (stateFasilitasKesehatan.create.form.informasiUmum.fasilitas = e.target.value)}
required
/>
<TextInput
label="Alamat"
placeholder="Masukkan alamat"
value={stateFasilitasKesehatan.create.form.informasiUmum.alamat}
onChange={(e) => (stateFasilitasKesehatan.create.form.informasiUmum.alamat = e.target.value)}
required
/>
<TextInput
label="Jam Operasional"
placeholder="Masukkan jam operasional"
value={stateFasilitasKesehatan.create.form.informasiUmum.jamOperasional}
onChange={(e) => (stateFasilitasKesehatan.create.form.informasiUmum.jamOperasional = e.target.value)}
required
/>
</Box> </Box>
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}> {/* Layanan Unggulan */}
<Stack gap="xs">
<Title order={3}>Create Fasilitas Kesehatan</Title>
<TextInput
label={<Text fz="sm" fw="bold">Nama Fasilitas Kesehatan</Text>}
placeholder="masukkan nama fasilitas kesehatan"
value={stateFasilitasKesehatan.create.form.name}
onChange={(e) => {
stateFasilitasKesehatan.create.form.name = e.target.value;
}}
/>
<Box> <Box>
<Text fz="md" fw="bold">Informasi Umum</Text> <Text fz="md" fw="bold" mb={5}>Layanan Unggulan</Text>
<TextInput
label={<Text fz="sm" fw="bold">Fasilitas</Text>}
placeholder="masukkan fasilitas"
value={stateFasilitasKesehatan.create.form.informasiUmum.fasilitas}
onChange={(e) => {
stateFasilitasKesehatan.create.form.informasiUmum.fasilitas = e.target.value;
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Alamat</Text>}
placeholder="masukkan alamat"
value={stateFasilitasKesehatan.create.form.informasiUmum.alamat}
onChange={(e) => {
stateFasilitasKesehatan.create.form.informasiUmum.alamat = e.target.value;
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Jam Operasional</Text>}
placeholder="masukkan jam operasional"
value={stateFasilitasKesehatan.create.form.informasiUmum.jamOperasional}
onChange={(e) => {
stateFasilitasKesehatan.create.form.informasiUmum.jamOperasional = e.target.value;
}}
/>
</Box>
<Box>
<Text fz="md" fw="bold">Layanan Unggulan</Text>
<CreateEditor <CreateEditor
value={stateFasilitasKesehatan.create.form.layananUnggulan.content} value={stateFasilitasKesehatan.create.form.layananUnggulan.content}
onChange={(e) => { onChange={(val) => (stateFasilitasKesehatan.create.form.layananUnggulan.content = val)}
stateFasilitasKesehatan.create.form.layananUnggulan.content = e;
}}
/>
</Box>
<Box>
<Text fz="md" fw="bold">Dokter dan Tenaga Medis</Text>
<TextInput
label={<Text fz="sm" fw="bold">Nama Dokter</Text>}
placeholder="masukkan nama dokter"
value={stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.name}
onChange={(e) => {
stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.name = e.target.value;
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Specialist</Text>}
placeholder="masukkan specialist"
value={stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.specialist}
onChange={(e) => {
stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.specialist = e.target.value;
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Jadwal</Text>}
placeholder="masukkan jadwal"
value={stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.jadwal}
onChange={(e) => {
stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.jadwal = e.target.value;
}}
/>
</Box>
<Box>
<Text fz="md" fw="bold">Fasilitas Pendukung</Text>
<CreateEditor
value={stateFasilitasKesehatan.create.form.fasilitasPendukung.content}
onChange={(e) => {
stateFasilitasKesehatan.create.form.fasilitasPendukung.content = e;
}}
/>
</Box>
<Box>
<Text fz="md" fw="bold">Prosedur Pendaftaran</Text>
<CreateEditor
value={stateFasilitasKesehatan.create.form.prosedurPendaftaran.content}
onChange={(e) => {
stateFasilitasKesehatan.create.form.prosedurPendaftaran.content = e;
}}
/>
</Box>
<Box>
<Text fz="md" fw="bold">Tarif dan Layanan</Text>
<TextInput
label={<Text fz="sm" fw="bold">Tarif</Text>}
placeholder="masukkan tarif"
value={stateFasilitasKesehatan.create.form.tarifDanLayanan.tarif}
onChange={(e) => {
stateFasilitasKesehatan.create.form.tarifDanLayanan.tarif = e.target.value;
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Layanan</Text>}
placeholder="masukkan layanan"
value={stateFasilitasKesehatan.create.form.tarifDanLayanan.layanan}
onChange={(e) => {
stateFasilitasKesehatan.create.form.tarifDanLayanan.layanan = e.target.value;
}}
/> />
</Box> </Box>
<Button onClick={handleSubmit} bg={colors['blue-button']}> {/* Dokter dan Tenaga Medis */}
<Box>
<Text fz="md" fw="bold" mb={5}>Dokter dan Tenaga Medis</Text>
<TextInput
label="Nama Dokter"
placeholder="Masukkan nama dokter"
value={stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.name}
onChange={(e) => (stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.name = e.target.value)}
required
/>
<TextInput
label="Spesialis"
placeholder="Masukkan spesialis"
value={stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.specialist}
onChange={(e) => (stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.specialist = e.target.value)}
required
/>
<TextInput
label="Jadwal"
placeholder="Masukkan jadwal"
value={stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.jadwal}
onChange={(e) => (stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.jadwal = e.target.value)}
required
/>
</Box>
{/* Fasilitas Pendukung */}
<Box>
<Text fz="md" fw="bold" mb={5}>Fasilitas Pendukung</Text>
<CreateEditor
value={stateFasilitasKesehatan.create.form.fasilitasPendukung.content}
onChange={(val) => (stateFasilitasKesehatan.create.form.fasilitasPendukung.content = val)}
/>
</Box>
{/* Prosedur Pendaftaran */}
<Box>
<Text fz="md" fw="bold" mb={5}>Prosedur Pendaftaran</Text>
<CreateEditor
value={stateFasilitasKesehatan.create.form.prosedurPendaftaran.content}
onChange={(val) => (stateFasilitasKesehatan.create.form.prosedurPendaftaran.content = val)}
/>
</Box>
{/* Tarif dan Layanan */}
<Box>
<Text fz="md" fw="bold" mb={5}>Tarif dan Layanan</Text>
<TextInput
label="Tarif"
placeholder="Masukkan tarif"
value={stateFasilitasKesehatan.create.form.tarifDanLayanan.tarif}
onChange={(e) => (stateFasilitasKesehatan.create.form.tarifDanLayanan.tarif = e.target.value)}
required
/>
<TextInput
label="Layanan"
placeholder="Masukkan layanan"
value={stateFasilitasKesehatan.create.form.tarifDanLayanan.layanan}
onChange={(e) => (stateFasilitasKesehatan.create.form.tarifDanLayanan.layanan = e.target.value)}
required
/>
</Box>
{/* Submit */}
<Group justify="right" mt="md">
<Button
type="submit"
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 Simpan
</Button> </Button>
</Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

@@ -1,114 +1,172 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core'; import {
Box,
Button,
Center,
Group,
Paper,
Pagination,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; import { IconArrowBack, IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import fasilitasKesehatanState from '../../../_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan'; import fasilitasKesehatanState from '../../../_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
function FasilitasKesehatan() { function FasilitasKesehatan() {
const router = useRouter();
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
// const router = useRouter();
return ( return (
<Box> <Box>
{/* <Grid> {/* Tombol Back */}
<GridCol span={12}> <Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors["blue-button"]} size={25} />
</Button>
</Box>
{/* Header Search */}
<HeaderSearch <HeaderSearch
title='Fasilitas Kesehatan' title='Fasilitas Kesehatan'
placeholder='pencarian' placeholder='Cari nama, alamat, atau jam operasional...'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
</GridCol>
<GridCol span={12}>
<Flex gap={"xs"}>
<Button color={colors['blue-button']} onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis`)}>
<IconList size={20} /> List Dokter
</Button>
<Button color={colors['blue-button']} onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan`)}>
<IconList size={20} /> List Layanan
</Button>
</Flex>
</GridCol>
</Grid> */}
<HeaderSearch
title='Fasilitas Kesehatan'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
/> />
<ListFasilitasKesehatan search={search} /> <ListFasilitasKesehatan search={search} />
</Box> </Box>
); );
} }
function ListFasilitasKesehatan({ search }: { search: string }) { function ListFasilitasKesehatan({ search }: { search: string }) {
const stateFasilitasKesehatan = useProxy(fasilitasKesehatanState.fasilitasKesehatan) const stateFasilitasKesehatan = useProxy(fasilitasKesehatanState.fasilitasKesehatan)
const router = useRouter(); const router = useRouter();
const { data, page, totalPages, loading, load } = stateFasilitasKesehatan.findMany;
useShallowEffect(() => { useShallowEffect(() => {
stateFasilitasKesehatan.findMany.load() load(page, 10, search);
}, []) }, [page, search]);
const filteredData = (stateFasilitasKesehatan.findMany.data || []).filter(item => { const filteredData = data || [];
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.informasiumum.alamat.toLowerCase().includes(keyword) ||
item.informasiumum.jamOperasional.toLowerCase().includes(keyword)
);
});
if (!stateFasilitasKesehatan.findMany.data) { if (loading || !data) {
return ( return (
<Box py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={600} radius="md" />
</Box> </Stack>
) )
} }
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<Stack> {/* Judul + Tombol Tambah */}
<JudulList <Group justify="space-between" mb="md">
title='List Fasilitas Kesehatan' <Title order={4}>Daftar Fasilitas Kesehatan</Title>
href='/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/create' <Tooltip label="Tambah Fasilitas Kesehatan" withArrow>
/> <Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push(
'/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/create'
)
}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
{/* Tabel */}
<Box style={{ overflowX: "auto" }}> <Box style={{ overflowX: "auto" }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}> <Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Fasilitas Kesehatan</TableTh> <TableTh>Fasilitas Kesehatan</TableTh>
<TableTh>Dokter</TableTh> <TableTh>Dokter</TableTh>
<TableTh>Layanan</TableTh> <TableTh>Layanan</TableTh>
<TableTh>Detail</TableTh> <TableTh>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item) => ( {filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>{item.dokterdantenagamedis.name}</TableTd>
<TableTd>{item.tarifdanlayanan.layanan}</TableTd>
<TableTd> <TableTd>
<Button onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/${item.id}`)}> <Text fw={500} truncate="end" lineClamp={1}>
<IconDeviceImacCog size={25} /> {item.name}
</Text>
</TableTd>
<TableTd>{item.dokterdantenagamedis?.name || '-'}</TableTd>
<TableTd>{item.tarifdanlayanan?.layanan || '-'}</TableTd>
<TableTd>
<Button
variant="light"
color="blue"
onClick={() =>
router.push(
`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/${item.id}`
)
}
>
<IconDeviceImacCog size={20} />
<Text ml={5}>Detail</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} ))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">
Tidak ada fasilitas kesehatan yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
</Stack>
</Paper> </Paper>
{/* Pagination */}
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10, search);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
</Box> </Box>
) )
} }

View File

@@ -2,7 +2,16 @@
'use client' 'use client'
import grafikkepuasan from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/grafikKepuasan'; import grafikkepuasan from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/grafikKepuasan';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
TextInput,
Title,
Tooltip
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -10,9 +19,10 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function EditGrafikHasilKepuasan() { function EditGrafikHasilKepuasan() {
const editState = useProxy(grafikkepuasan) const editState = useProxy(grafikkepuasan);
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
nama: editState.update.form.nama || '', nama: editState.update.form.nama || '',
tanggal: editState.update.form.tanggal || '', tanggal: editState.update.form.tanggal || '',
@@ -22,12 +32,12 @@ function EditGrafikHasilKepuasan() {
}); });
useEffect(() => { useEffect(() => {
const loadKelahiran = async () => { const loadData = async () => {
const id = params?.id as string; const id = params?.id as string;
if (!id) return; if (!id) return;
try { try {
const data = await editState.update.load(id); // akses langsung, bukan dari proxy const data = await editState.update.load(id);
if (data) { if (data) {
setFormData({ setFormData({
nama: data.nama || '', nama: data.nama || '',
@@ -43,21 +53,17 @@ function EditGrafikHasilKepuasan() {
} }
}; };
loadKelahiran(); loadData();
}, [params?.id]); }, [params?.id]);
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
editState.update.form = { editState.update.form = {
...editState.update.form, ...editState.update.form,
nama: formData.nama, ...formData,
tanggal: formData.tanggal,
jenisKelamin: formData.jenisKelamin,
alamat: formData.alamat,
penyakit: formData.penyakit,
}; };
await editState.update.submit(); await editState.update.submit();
toast.success('grafik hasil kepuasan berhasil diperbarui!'); toast.success('Grafik hasil kepuasan berhasil diperbarui!');
router.push('/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan'); router.push('/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan');
} catch (error) { } catch (error) {
console.error('Error updating grafik hasil kepuasan:', error); console.error('Error updating grafik hasil kepuasan:', error);
@@ -66,47 +72,87 @@ function EditGrafikHasilKepuasan() {
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header */}
<Button variant="subtle" onClick={() => router.back()}> <Group mb="md">
<IconArrowBack color={colors["blue-button"]} size={30} /> <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> </Button>
</Box> </Tooltip>
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}> <Title order={4} ml="sm" c="dark">
<Stack gap={"xs"}> Edit Grafik Hasil Kepuasan
<Title order={3}>Edit grafik hasil kepuasan</Title> </Title>
</Group>
{/* Form */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
value={formData.nama} value={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })} onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Nama</Text>} label="Nama"
placeholder="masukkan nama" placeholder="Masukkan nama"
required
/> />
<TextInput <TextInput
type='date' type="date"
value={formData.tanggal} value={formData.tanggal}
onChange={(e) => setFormData({ ...formData, tanggal: e.target.value })} onChange={(e) => setFormData({ ...formData, tanggal: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Tanggal</Text>} label="Tanggal"
placeholder="masukkan tanggal" placeholder="Masukkan tanggal"
required
/> />
<TextInput <TextInput
value={formData.jenisKelamin} value={formData.jenisKelamin}
onChange={(e) => setFormData({ ...formData, jenisKelamin: e.target.value })} onChange={(e) =>
label={<Text fz={"sm"} fw={"bold"}>Jenis Kelamin</Text>} setFormData({ ...formData, jenisKelamin: e.target.value })
placeholder="masukkan jenis kelamin" }
label="Jenis Kelamin"
placeholder="Masukkan jenis kelamin"
required
/> />
<TextInput <TextInput
value={formData.alamat} value={formData.alamat}
onChange={(e) => setFormData({ ...formData, alamat: e.target.value })} onChange={(e) => setFormData({ ...formData, alamat: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Alamat</Text>} label="Alamat"
placeholder="masukkan alamat" placeholder="Masukkan alamat"
required
/> />
<TextInput <TextInput
value={formData.penyakit} value={formData.penyakit}
onChange={(e) => setFormData({ ...formData, penyakit: e.target.value })} onChange={(e) => setFormData({ ...formData, penyakit: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Penyakit</Text>} label="Penyakit"
placeholder="masukkan penyakit" placeholder="Masukkan penyakit"
required
/> />
<Button onClick={handleSubmit}>Simpan</Button>
<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> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

@@ -1,9 +1,8 @@
'use client' 'use client'
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -11,103 +10,130 @@ import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirma
import grafikkepuasan from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/grafikKepuasan'; import grafikkepuasan from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/grafikKepuasan';
import colors from '@/con/colors'; import colors from '@/con/colors';
function DetailGrafikHasilKepuasan() { function DetailGrafikHasilKepuasan() {
const state = useProxy(grafikkepuasan) const state = useProxy(grafikkepuasan);
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null);
const params = useParams() const params = useParams();
const router = useRouter() const router = useRouter();
useShallowEffect(() => { useShallowEffect(() => {
state.findUnique.load(params?.id as string) state.findUnique.load(params?.id as string);
}, []) }, []);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
state.delete.byId(selectedId) state.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan") router.push("/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan");
}
} }
};
if (!state.findUnique.data) { if (!state.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={40} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) );
} }
const data = state.findUnique.data;
return ( return (
<Box> <Box py={10}>
<Box mb={10}> {/* Tombol Back */}
<Button variant="subtle" onClick={() => router.back()}> <Button
<IconArrowBack color={colors['blue-button']} size={25} /> variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15}
>
Kembali
</Button> </Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}> {/* Wrapper Detail */}
<Stack> <Paper
<Text fz={"xl"} fw={"bold"}>Detail Data Grafik Hasil Kepuasan</Text> withBorder
{state.findUnique.data ? ( w={{ base: "100%", md: "50%" }}
<Paper key={state.findUnique.data.id} bg={colors['BG-trans']} p={'md'}> bg={colors['white-1']}
<Stack gap={"xs"}> p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Data Grafik Hasil Kepuasan
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box> <Box>
<Text fw={"bold"} fz={"lg"}>Nama</Text> <Text fz="lg" fw="bold">Nama</Text>
<Text fz={"lg"}>{state.findUnique.data?.nama}</Text> <Text fz="md" c="dimmed">{data.nama || '-'}</Text>
</Box> </Box>
<Box> <Box>
<Text fw={"bold"} fz={"lg"}>Tanggal</Text> <Text fz="lg" fw="bold">Tanggal</Text>
<Text fz={"lg"}> <Text fz="md" c="dimmed">
{new Date(state.findUnique.data?.tanggal).toLocaleDateString('id-ID', { {new Date(data.tanggal).toLocaleDateString("id-ID", {
day: '2-digit', day: "2-digit",
month: 'long', month: "long",
year: 'numeric' year: "numeric",
})} })}
</Text> </Text>
</Box> </Box>
<Box> <Box>
<Text fw={"bold"} fz={"lg"}>Jenis Kelamin</Text> <Text fz="lg" fw="bold">Jenis Kelamin</Text>
<Text fz={"lg"} >{state.findUnique.data?.jenisKelamin}</Text> <Text fz="md" c="dimmed">{data.jenisKelamin || '-'}</Text>
</Box> </Box>
<Box> <Box>
<Text fw={"bold"} fz={"lg"}>Alamat</Text> <Text fz="lg" fw="bold">Alamat</Text>
<Text fz={"lg"} >{state.findUnique.data?.alamat}</Text> <Text fz="md" c="dimmed">{data.alamat || '-'}</Text>
</Box> </Box>
<Box> <Box>
<Text fw={"bold"} fz={"lg"}>Penyakit</Text> <Text fz="lg" fw="bold">Penyakit</Text>
<Text fz={"lg"} >{state.findUnique.data?.penyakit}</Text> <Text fz="md" c="dimmed">{data.penyakit || '-'}</Text>
</Box> </Box>
<Flex gap={"xs"} mt={10}>
{/* Aksi */}
<Group gap="sm">
<Tooltip label="Hapus Data" withArrow position="top">
<Button <Button
color="red"
onClick={() => { onClick={() => {
if (state.findUnique.data) { setSelectedId(data.id);
setSelectedId(state.findUnique.data.id);
setModalHapus(true); setModalHapus(true);
}
}} }}
disabled={state.delete.loading || !state.findUnique.data} variant="light"
color={"red"} radius="md"
size="md"
> >
<IconX size={20} /> <IconTrash size={20} />
</Button> </Button>
</Tooltip>
<Tooltip label="Edit Data" withArrow position="top">
<Button <Button
onClick={() => { color="green"
if (state.findUnique.data) { onClick={() =>
router.push(`/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/${state.findUnique.data.id}/edit`); router.push(
`/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/${data.id}/edit`
)
} }
}} variant="light"
disabled={!state.findUnique.data} radius="md"
color={"green"} size="md"
> >
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Group>
</Stack> </Stack>
</Paper> </Paper>
) : null}
</Stack> </Stack>
</Paper> </Paper>
@@ -116,7 +142,7 @@ function DetailGrafikHasilKepuasan() {
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus data ini?' text="Apakah anda yakin ingin menghapus data ini?"
/> />
</Box> </Box>
); );

View File

@@ -3,7 +3,17 @@
'use client' 'use client'
import grafikkepuasan from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/grafikKepuasan'; import grafikkepuasan from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/grafikKepuasan';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -12,7 +22,7 @@ import { useProxy } from 'valtio/utils';
function CreateGrafikHasilKepuasanMasyarakat() { function CreateGrafikHasilKepuasanMasyarakat() {
const stateGrafikKepuasan = useProxy(grafikkepuasan); const stateGrafikKepuasan = useProxy(grafikkepuasan);
const [chartData, setChartData] = useState<any[]>([]); const [chartData, setChartData] = useState<any[]>([]);
const router = useRouter() const router = useRouter();
const resetForm = () => { const resetForm = () => {
stateGrafikKepuasan.create.form = { stateGrafikKepuasan.create.form = {
@@ -21,83 +31,98 @@ function CreateGrafikHasilKepuasanMasyarakat() {
jenisKelamin: "", jenisKelamin: "",
alamat: "", alamat: "",
penyakit: "", penyakit: "",
} };
} };
const handleSubmit = async () => { const handleSubmit = async () => {
await stateGrafikKepuasan.create.create(); await stateGrafikKepuasan.create.create();
resetForm(); resetForm();
router.push("/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan"); router.push("/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan");
} };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header */}
<Button variant="subtle" onClick={() => router.back()}> <Group mb="md">
<IconArrowBack size={20} /> <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> </Button>
</Box> </Tooltip>
<Box> <Title order={4} ml="sm" c="dark">
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> Tambah Grafik Hasil Kepuasan Masyarakat
<Title order={4}>Tambah Grafik Hasil Kepuasan Masyarakat</Title> </Title>
<Stack gap={"xs"}> </Group>
{/* Form */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label="Nama" label="Nama"
type="text"
value={stateGrafikKepuasan.create.form.nama}
placeholder="Masukkan nama" placeholder="Masukkan nama"
onChange={(val) => { value={stateGrafikKepuasan.create.form.nama}
stateGrafikKepuasan.create.form.nama = val.currentTarget.value; onChange={(e) => (stateGrafikKepuasan.create.form.nama = e.target.value)}
}} required
/> />
<TextInput <TextInput
label="Tanggal"
type="date" type="date"
value={stateGrafikKepuasan.create.form.tanggal} label="Tanggal"
placeholder="Masukkan tanggal" placeholder="Masukkan tanggal"
onChange={(val) => { value={stateGrafikKepuasan.create.form.tanggal}
stateGrafikKepuasan.create.form.tanggal = val.currentTarget.value; onChange={(e) => (stateGrafikKepuasan.create.form.tanggal = e.target.value)}
}} required
/> />
<TextInput <TextInput
label="Jenis Kelamin" label="Jenis Kelamin"
type="text"
value={stateGrafikKepuasan.create.form.jenisKelamin}
placeholder="Masukkan jenis kelamin" placeholder="Masukkan jenis kelamin"
onChange={(val) => { value={stateGrafikKepuasan.create.form.jenisKelamin}
stateGrafikKepuasan.create.form.jenisKelamin = val.currentTarget.value; onChange={(e) => (stateGrafikKepuasan.create.form.jenisKelamin = e.target.value)}
}} required
/> />
<TextInput <TextInput
label="Alamat" label="Alamat"
type="text"
value={stateGrafikKepuasan.create.form.alamat}
placeholder="Masukkan alamat" placeholder="Masukkan alamat"
onChange={(val) => { value={stateGrafikKepuasan.create.form.alamat}
stateGrafikKepuasan.create.form.alamat = val.currentTarget.value; onChange={(e) => (stateGrafikKepuasan.create.form.alamat = e.target.value)}
}} required
/> />
<TextInput <TextInput
label="Penyakit" label="Penyakit"
type="text"
value={stateGrafikKepuasan.create.form.penyakit}
placeholder="Masukkan penyakit" placeholder="Masukkan penyakit"
onChange={(val) => { value={stateGrafikKepuasan.create.form.penyakit}
stateGrafikKepuasan.create.form.penyakit = val.currentTarget.value; onChange={(e) => (stateGrafikKepuasan.create.form.penyakit = e.target.value)}
}} required
/> />
<Group>
<Group justify="right">
<Button <Button
bg={colors['blue-button']}
mt={10}
onClick={handleSubmit} 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)',
}}
> >
Submit Simpan
</Button> </Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>
</Box>
); );
} }

View File

@@ -1,99 +1,108 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import {
Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip
} from '@mantine/core';
import { useMediaQuery, useShallowEffect } from '@mantine/hooks'; import { useMediaQuery, useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; import { IconArrowBack, IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Bar, BarChart, Legend, Tooltip, XAxis, YAxis } from 'recharts'; import { Bar, BarChart, Tooltip as ChartTooltip, Legend, XAxis, YAxis } from 'recharts';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import grafikkepuasan from '../../../_state/kesehatan/data_kesehatan_warga/grafikKepuasan'; import grafikkepuasan from '../../../_state/kesehatan/data_kesehatan_warga/grafikKepuasan';
function GrafikHasilKepuasanMasyarakat() { function GrafikHasilKepuasanMasyarakat() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const router = useRouter();
return ( return (
<Box> <Box>
{/* Tombol Back */}
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors["blue-button"]} size={25} />
</Button>
</Box>
{/* Header Search */}
<HeaderSearch <HeaderSearch
title='Grafik Hasil Kepuasan Masyarakat' title='Grafik Hasil Kepuasan Masyarakat'
placeholder='pencarian' placeholder='Cari nama atau alamat...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
/> />
<ListGrafikHasilKepuasanMasyarakat search={search} /> <ListGrafikHasilKepuasanMasyarakat search={search} />
</Box> </Box>
); );
} }
function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) { function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
type PDKMGrafik = { type PDKMGrafik = {
id: string; id: string;
nama: string; nama: string;
tanggal: string | Date; // Allow both string and Date types tanggal: string | Date;
jenisKelamin: string; jenisKelamin: string;
alamat: string; alamat: string;
penyakit: string; penyakit: string;
createdAt?: Date; // Add optional fields that might come from the API };
updatedAt?: Date;
deletedAt?: Date | null;
}
const stateGrafikKepuasan = useProxy(grafikkepuasan); const stateGrafikKepuasan = useProxy(grafikkepuasan);
const [chartData, setChartData] = useState<PDKMGrafik[]>([]); const [chartData, setChartData] = useState<PDKMGrafik[]>([]);
const [mounted, setMounted] = useState(false); // untuk memastikan DOM sudah ready const [mounted, setMounted] = useState(false);
const isTablet = useMediaQuery('(max-width: 1024px)') const isTablet = useMediaQuery('(max-width: 1024px)');
const isMobile = useMediaQuery('(max-width: 768px)') const isMobile = useMediaQuery('(max-width: 768px)');
const router = useRouter(); const router = useRouter();
const { const { data, page, totalPages, loading, load } = stateGrafikKepuasan.findMany;
data,
page,
totalPages,
loading,
load
} = stateGrafikKepuasan.findMany;
useShallowEffect(() => { useShallowEffect(() => {
setMounted(true) setMounted(true);
load(page, 10, search) load(page, 10, search);
}, [page, search]) }, [page, search]);
useEffect(() => { useEffect(() => {
setMounted(true);
if (data) { if (data) {
setChartData(data.map((item) => ({ setChartData(data.map((item) => ({
id: item.id, ...item,
nama: item.nama, tanggal: item.tanggal instanceof Date ? item.tanggal.toISOString() : item.tanggal
tanggal: item.tanggal instanceof Date ? item.tanggal.toISOString() : item.tanggal,
jenisKelamin: item.jenisKelamin,
alamat: item.alamat,
penyakit: item.penyakit,
}))); })));
} }
}, [data]); }, [data]);
// Add this function to process the data
const processDiseaseData = (data: PDKMGrafik[]) => { const processDiseaseData = (data: PDKMGrafik[]) => {
const diseaseCount: Record<string, number> = {}; const diseaseCount: Record<string, number> = {};
data.forEach(item => { data.forEach(item => {
const penyakit = item.penyakit.trim(); const penyakit = item.penyakit.trim();
if (penyakit) { if (penyakit) {
diseaseCount[penyakit] = (diseaseCount[penyakit] || 0) + 1; diseaseCount[penyakit] = (diseaseCount[penyakit] || 0) + 1;
} }
}); });
return Object.entries(diseaseCount).map(([name, count]) => ({ name, count }));
return Object.entries(diseaseCount).map(([name, count]) => ({
name,
count
}));
}; };
// Add this state to store the processed chart data
const [diseaseChartData, setDiseaseChartData] = useState<{ name: string, count: number }[]>([]); const [diseaseChartData, setDiseaseChartData] = useState<{ name: string, count: number }[]>([]);
// Update the chart data when data changes
useEffect(() => { useEffect(() => {
if (data && data.length > 0) { if (data && data.length > 0) {
setDiseaseChartData(processDiseaseData(data)); setDiseaseChartData(processDiseaseData(data));
@@ -104,79 +113,118 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
) );
} }
return ( return (
<Box> <Box py={10}>
<Stack gap={"xs"}> <Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
{/* Form Input */} {/* Judul + Tombol Tambah */}
<Paper bg={colors['white-1']} p={'md'}> <Group justify="space-between" mb="md">
<JudulList <Title order={4}>Daftar Grafik Hasil Kepuasan Masyarakat</Title>
title='List Grafik Hasil Kepuasan Masyarakat' <Tooltip label="Tambah Data" withArrow>
href='/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/create' <Button
/> leftSection={<IconPlus size={18} />}
<Table striped withTableBorder withRowBorders> color="blue"
variant="light"
onClick={() =>
router.push(
'/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/create'
)
}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
{/* Tabel */}
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Nama</TableTh> <TableTh>Nama</TableTh>
<TableTh>Tanggal</TableTh> <TableTh>Tanggal</TableTh>
<TableTh>Jenis Kelamin</TableTh> <TableTh>Jenis Kelamin</TableTh>
<TableTh>Penyakit</TableTh> <TableTh>Penyakit</TableTh>
<TableTh>Detail</TableTh> <TableTh>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item) => ( {filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd>{item.nama}</TableTd> <TableTd>{item.nama}</TableTd>
<TableTd> <TableTd>
{new Date(item.tanggal).toLocaleDateString('id-ID', { {new Date(item.tanggal).toLocaleDateString('id-ID', {
day: '2-digit', day: '2-digit',
month: 'long', month: 'long',
year: 'numeric' year: 'numeric',
})} })}
</TableTd> </TableTd>
<TableTd>{item.jenisKelamin}</TableTd> <TableTd>{item.jenisKelamin}</TableTd>
<TableTd>{item.penyakit}</TableTd> <TableTd>{item.penyakit}</TableTd>
<TableTd> <TableTd>
<Button color='green' onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/${item.id}`)}> <Button
variant="light"
color="blue"
onClick={() =>
router.push(
`/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/${item.id}`
)
}
>
<IconDeviceImacCog size={20} /> <IconDeviceImacCog size={20} />
<Text ml={5}>Detail</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} ))
) : (
<TableTr>
<TableTd colSpan={5}>
<Center py={20}>
<Text color="dimmed">
Tidak ada data kepuasan masyarakat yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody> </TableTbody>
</Table> </Table>
</Box>
</Paper> </Paper>
{/* Pagination */}
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => load(newPage)} // ini penting! onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages} total={totalPages}
mt="md" mt="md"
mb="md" mb="md"
color="blue"
radius="md"
/> />
</Center> </Center>
{/* Chart */} {/* Chart */}
{!mounted && !chartData ? ( <Box mt="lg" style={{ width: '100%', minWidth: 300, height: 420, minHeight: 300 }}>
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}>
<Paper bg={colors['white-1']} p={'md'}>
<Title pb={10} order={3}>Grafik Hasil Kepuasan Masyarakat</Title>
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text>
</Paper>
</Box>
) : (
<Box style={{ width: '100%', minWidth: 300, height: 420, minHeight: 300 }}>
<Paper bg={colors['white-1']} p={'md'}> <Paper 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 ? (
<BarChart width={isMobile ? 450 : isTablet ? 500 : 550} height={350} data={diseaseChartData} > <BarChart
width={isMobile ? 450 : isTablet ? 500 : 550}
height={350}
data={diseaseChartData}
>
<XAxis <XAxis
dataKey="name" dataKey="name"
tick={{ fontSize: 12 }} tick={{ fontSize: 12 }}
@@ -186,15 +234,15 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
height={70} height={70}
/> />
<YAxis /> <YAxis />
<Tooltip /> <ChartTooltip />
<Legend /> <Legend />
<Bar dataKey="count" fill={colors['blue-button']} name="Jumlah Kasus" /> <Bar dataKey="count" fill={colors['blue-button']} name="Jumlah Kasus" />
</BarChart> </BarChart>
) : (
<Text c="dimmed">Belum ada data untuk ditampilkan dalam grafik</Text>
)} )}
</Paper> </Paper>
</Box> </Box>
)}
</Stack>
</Box> </Box>
); );
} }

View File

@@ -4,7 +4,17 @@
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import jadwalKegiatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/jadwalKegiatan'; import jadwalKegiatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/jadwalKegiatan';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -128,33 +138,14 @@ function EditJadwalKegiatan() {
stateJadwalKegiatan.edit.form = { stateJadwalKegiatan.edit.form = {
...stateJadwalKegiatan.edit.form, ...stateJadwalKegiatan.edit.form,
content: formData.content, content: formData.content,
informasiJadwalKegiatan: { informasiJadwalKegiatan: { ...formData.informasiJadwalKegiatan },
name: formData.informasiJadwalKegiatan.name, deskripsiJadwalKegiatan: { ...formData.deskripsiJadwalKegiatan },
tanggal: formData.informasiJadwalKegiatan.tanggal, layananJadwalKegiatan: { ...formData.layananJadwalKegiatan },
waktu: formData.informasiJadwalKegiatan.waktu, syaratKetentuanJadwalKegiatan: { ...formData.syaratKetentuanJadwalKegiatan },
lokasi: formData.informasiJadwalKegiatan.lokasi, dokumenJadwalKegiatan: { ...formData.dokumenJadwalKegiatan },
}, pendaftaranJadwalKegiatan: { ...formData.pendaftaranJadwalKegiatan },
deskripsiJadwalKegiatan: {
deskripsi: formData.deskripsiJadwalKegiatan.deskripsi,
},
layananJadwalKegiatan: {
content: formData.layananJadwalKegiatan.content,
},
syaratKetentuanJadwalKegiatan: {
content: formData.syaratKetentuanJadwalKegiatan.content,
},
dokumenJadwalKegiatan: {
content: formData.dokumenJadwalKegiatan.content,
},
pendaftaranJadwalKegiatan: {
name: formData.pendaftaranJadwalKegiatan.name,
tanggal: formData.pendaftaranJadwalKegiatan.tanggal,
namaOrangtua: formData.pendaftaranJadwalKegiatan.namaOrangtua,
nomor: formData.pendaftaranJadwalKegiatan.nomor,
alamat: formData.pendaftaranJadwalKegiatan.alamat,
catatan: formData.pendaftaranJadwalKegiatan.catatan,
},
}; };
const success = await stateJadwalKegiatan.edit.submit(); const success = await stateJadwalKegiatan.edit.submit();
if (success) { if (success) {
toast.success("Jadwal kegiatan berhasil diperbarui!"); toast.success("Jadwal kegiatan berhasil diperbarui!");
@@ -165,241 +156,164 @@ function EditJadwalKegiatan() {
toast.error(error instanceof Error ? error.message : "Gagal memperbarui data jadwal kegiatan"); toast.error(error instanceof Error ? error.message : "Gagal memperbarui data jadwal kegiatan");
} }
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header */}
<Button onClick={() => router.back()} variant="subtle" color="blue"> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <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> </Button>
</Box> </Tooltip>
<Stack gap="xs"> <Title order={4} ml="sm" c="dark">
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}> Edit Jadwal Kegiatan
<Stack gap="xs"> </Title>
<Title order={3}>Edit Jadwal Kegiatan</Title> </Group>
{/* Form */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Nama Jadwal */}
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Nama Jadwal Kegiatan</Text>} label="Nama Jadwal Kegiatan"
placeholder="masukkan nama jadwal kegiatan" placeholder="Masukkan nama jadwal kegiatan"
value={formData.content} value={formData.content}
onChange={(e) => { onChange={(e) => setFormData((prev) => ({ ...prev, content: e.target.value }))}
setFormData(prev => ({
...prev,
content: e.target.value
}));
}}
/> />
{/* Deskripsi */}
<Box> <Box>
<Text fz="sm" fw="bold">Deskripsi Jadwal Kegiatan</Text> <Text fz="sm" fw="bold">Deskripsi Jadwal Kegiatan</Text>
<EditEditor <EditEditor
value={formData.deskripsiJadwalKegiatan.deskripsi} value={formData.deskripsiJadwalKegiatan.deskripsi}
onChange={(e) => { onChange={(val) => setFormData((prev) => ({
setFormData(prev => ({
...prev, ...prev,
deskripsiJadwalKegiatan: { deskripsiJadwalKegiatan: { deskripsi: val }
...prev.deskripsiJadwalKegiatan, }))}
deskripsi: e
}
}));
}}
/> />
</Box> </Box>
{/* Informasi Jadwal */}
<Box> <Box>
<Text fz="md" fw="bold">Informasi Jadwal Kegiatan</Text> <Text fz="md" fw="bold">Informasi Jadwal Kegiatan</Text>
<TextInput <TextInput label="Nama" value={formData.informasiJadwalKegiatan.name}
label={<Text fz="sm" fw="bold">Nama</Text>} onChange={(e) => setFormData((prev) => ({
placeholder="masukkan nama" ...prev, informasiJadwalKegiatan: { ...prev.informasiJadwalKegiatan, name: e.target.value }
value={formData.informasiJadwalKegiatan.name} }))}
onChange={(e) => {
setFormData(prev => ({
...prev,
informasiJadwalKegiatan: {
...prev.informasiJadwalKegiatan,
name: e.target.value
}
}));
}}
/> />
<TextInput <TextInput type="date" label="Tanggal" value={formData.informasiJadwalKegiatan.tanggal}
label={<Text fz="sm" fw="bold">Tanggal</Text>} onChange={(e) => setFormData((prev) => ({
placeholder="masukkan tanggal" ...prev, informasiJadwalKegiatan: { ...prev.informasiJadwalKegiatan, tanggal: e.target.value }
value={formData.informasiJadwalKegiatan.tanggal} }))}
onChange={(e) => {
setFormData(prev => ({
...prev,
informasiJadwalKegiatan: {
...prev.informasiJadwalKegiatan,
tanggal: e.target.value
}
}));
}}
/> />
<TextInput <TextInput label="Waktu" value={formData.informasiJadwalKegiatan.waktu}
label={<Text fz="sm" fw="bold">Waktu</Text>} onChange={(e) => setFormData((prev) => ({
placeholder="masukkan waktu" ...prev, informasiJadwalKegiatan: { ...prev.informasiJadwalKegiatan, waktu: e.target.value }
value={formData.informasiJadwalKegiatan.waktu} }))}
onChange={(e) => {
setFormData(prev => ({
...prev,
informasiJadwalKegiatan: {
...prev.informasiJadwalKegiatan,
waktu: e.target.value
}
}));
}}
/> />
<TextInput <TextInput label="Lokasi" value={formData.informasiJadwalKegiatan.lokasi}
label={<Text fz="sm" fw="bold">Lokasi</Text>} onChange={(e) => setFormData((prev) => ({
placeholder="masukkan lokasi" ...prev, informasiJadwalKegiatan: { ...prev.informasiJadwalKegiatan, lokasi: e.target.value }
value={formData.informasiJadwalKegiatan.lokasi} }))}
onChange={(e) => {
setFormData(prev => ({
...prev,
informasiJadwalKegiatan: {
...prev.informasiJadwalKegiatan,
lokasi: e.target.value
}
}));
}}
/> />
</Box> </Box>
{/* Layanan */}
<Box> <Box>
<Text fz="md" fw="bold">Layanan Jadwal Kegiatan</Text> <Text fz="md" fw="bold">Layanan Jadwal Kegiatan</Text>
<EditEditor <EditEditor
value={formData.layananJadwalKegiatan.content} value={formData.layananJadwalKegiatan.content}
onChange={(e) => { onChange={(val) => setFormData((prev) => ({
setFormData(prev => ({
...prev, ...prev,
layananJadwalKegiatan: { layananJadwalKegiatan: { content: val }
...prev.layananJadwalKegiatan, }))}
content: e
}
}));
}}
/> />
</Box> </Box>
{/* Syarat */}
<Box> <Box>
<Text fz="md" fw="bold">Syarat dan Ketentuan Jadwal Kegiatan</Text> <Text fz="md" fw="bold">Syarat dan Ketentuan</Text>
<EditEditor <EditEditor
value={formData.syaratKetentuanJadwalKegiatan.content} value={formData.syaratKetentuanJadwalKegiatan.content}
onChange={(e) => { onChange={(val) => setFormData((prev) => ({
setFormData(prev => ({
...prev, ...prev,
syaratKetentuanJadwalKegiatan: { syaratKetentuanJadwalKegiatan: { content: val }
...prev.syaratKetentuanJadwalKegiatan, }))}
content: e
}
}));
}}
/> />
</Box> </Box>
{/* Dokumen */}
<Box> <Box>
<Text fz="md" fw="bold">Dokumen Jadwal Kegiatan</Text> <Text fz="md" fw="bold">Dokumen Jadwal Kegiatan</Text>
<EditEditor <EditEditor
value={formData.dokumenJadwalKegiatan.content} value={formData.dokumenJadwalKegiatan.content}
onChange={(e) => { onChange={(val) => setFormData((prev) => ({
setFormData(prev => ({
...prev, ...prev,
dokumenJadwalKegiatan: { dokumenJadwalKegiatan: { content: val }
...prev.dokumenJadwalKegiatan, }))}
content: e
}
}));
}}
/>
</Box>
<Box>
<Text fz="md" fw="bold">Pendaftaran Jadwal Kegiatan</Text>
<TextInput
label={<Text fz="sm" fw="bold">Nama</Text>}
placeholder="masukkan nama"
value={formData.pendaftaranJadwalKegiatan.name}
onChange={(e) => {
setFormData(prev => ({
...prev,
pendaftaranJadwalKegiatan: {
...prev.pendaftaranJadwalKegiatan,
name: e.target.value
}
}));
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Tanggal</Text>}
placeholder="masukkan tanggal"
value={formData.pendaftaranJadwalKegiatan.tanggal}
onChange={(e) => {
setFormData(prev => ({
...prev,
pendaftaranJadwalKegiatan: {
...prev.pendaftaranJadwalKegiatan,
tanggal: e.target.value
}
}));
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Nama Orangtua</Text>}
placeholder="masukkan nama orangtua"
value={formData.pendaftaranJadwalKegiatan.namaOrangtua}
onChange={(e) => {
setFormData(prev => ({
...prev,
pendaftaranJadwalKegiatan: {
...prev.pendaftaranJadwalKegiatan,
namaOrangtua: e.target.value
}
}));
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Nomor</Text>}
placeholder="masukkan nomor"
value={formData.pendaftaranJadwalKegiatan.nomor}
onChange={(e) => {
setFormData(prev => ({
...prev,
pendaftaranJadwalKegiatan: {
...prev.pendaftaranJadwalKegiatan,
nomor: e.target.value
}
}));
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Alamat</Text>}
placeholder="masukkan alamat"
value={formData.pendaftaranJadwalKegiatan.alamat}
onChange={(e) => {
setFormData(prev => ({
...prev,
pendaftaranJadwalKegiatan: {
...prev.pendaftaranJadwalKegiatan,
alamat: e.target.value
}
}));
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Catatan</Text>}
placeholder="masukkan catatan"
value={formData.pendaftaranJadwalKegiatan.catatan}
onChange={(e) => {
setFormData(prev => ({
...prev,
pendaftaranJadwalKegiatan: {
...prev.pendaftaranJadwalKegiatan,
catatan: e.target.value
}
}));
}}
/> />
</Box> </Box>
<Button onClick={handleSubmit} bg={colors['blue-button']}> {/* Pendaftaran */}
<Box>
<Text fz="md" fw="bold">Pendaftaran Jadwal Kegiatan</Text>
<TextInput label="Nama" value={formData.pendaftaranJadwalKegiatan.name}
onChange={(e) => setFormData((prev) => ({
...prev, pendaftaranJadwalKegiatan: { ...prev.pendaftaranJadwalKegiatan, name: e.target.value }
}))}
/>
<TextInput type="date" label="Tanggal" value={formData.pendaftaranJadwalKegiatan.tanggal}
onChange={(e) => setFormData((prev) => ({
...prev, pendaftaranJadwalKegiatan: { ...prev.pendaftaranJadwalKegiatan, tanggal: e.target.value }
}))}
/>
<TextInput label="Nama Orangtua" value={formData.pendaftaranJadwalKegiatan.namaOrangtua}
onChange={(e) => setFormData((prev) => ({
...prev, pendaftaranJadwalKegiatan: { ...prev.pendaftaranJadwalKegiatan, namaOrangtua: e.target.value }
}))}
/>
<TextInput label="Nomor" value={formData.pendaftaranJadwalKegiatan.nomor}
onChange={(e) => setFormData((prev) => ({
...prev, pendaftaranJadwalKegiatan: { ...prev.pendaftaranJadwalKegiatan, nomor: e.target.value }
}))}
/>
<TextInput label="Alamat" value={formData.pendaftaranJadwalKegiatan.alamat}
onChange={(e) => setFormData((prev) => ({
...prev, pendaftaranJadwalKegiatan: { ...prev.pendaftaranJadwalKegiatan, alamat: e.target.value }
}))}
/>
<TextInput label="Catatan" value={formData.pendaftaranJadwalKegiatan.catatan}
onChange={(e) => setFormData((prev) => ({
...prev, pendaftaranJadwalKegiatan: { ...prev.pendaftaranJadwalKegiatan, catatan: e.target.value }
}))}
/>
</Box>
{/* Submit */}
<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 Simpan
</Button> </Button>
</Group>
</Stack> </Stack>
</Paper> </Paper>
</Stack>
</Box> </Box>
); );
} }

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