Compare commits
5 Commits
nico/22-ju
...
nico/24-ju
| Author | SHA1 | Date | |
|---|---|---|---|
| a25cfe8b8a | |||
| b745bd4623 | |||
| bdf751ec3d | |||
| 1bc6dd8dbf | |||
| 88a10538a7 |
@@ -60,6 +60,7 @@ model FileStorage {
|
||||
deletedAt DateTime?
|
||||
isActive Boolean @default(true)
|
||||
link String
|
||||
category String // "image" / "document" / "other"
|
||||
Berita Berita[]
|
||||
PotensiDesa PotensiDesa[]
|
||||
Posyandu Posyandu[]
|
||||
@@ -86,28 +87,30 @@ model FileStorage {
|
||||
KolaborasiInovasi KolaborasiInovasi[]
|
||||
InfoTekno InfoTekno[]
|
||||
PengaduanMasyarakat PengaduanMasyarakat[]
|
||||
KegiatanDesa KegiatanDesa[]
|
||||
ProgramInovasi ProgramInovasi[]
|
||||
PejabatDesa PejabatDesa[]
|
||||
MediaSosial MediaSosial[]
|
||||
DesaAntiKorupsi DesaAntiKorupsi[]
|
||||
SDGSDesa SDGSDesa[]
|
||||
APBDesImage APBDes[] @relation("APBDesImage")
|
||||
APBDesFile APBDes[] @relation("APBDesFile")
|
||||
|
||||
KegiatanDesa KegiatanDesa[]
|
||||
|
||||
ProgramInovasi ProgramInovasi[]
|
||||
|
||||
PejabatDesa PejabatDesa[]
|
||||
|
||||
MediaSosial MediaSosial[]
|
||||
PrestasiDesa PrestasiDesa[]
|
||||
}
|
||||
|
||||
//========================================= MENU LANDING PAGE ========================================= //
|
||||
//========================================= PROFILE ========================================= //
|
||||
model PejabatDesa {
|
||||
id String @id @default(cuid())
|
||||
name String @unique @db.VarChar(255)
|
||||
id String @id @default(cuid())
|
||||
name String @unique @db.VarChar(255)
|
||||
position String
|
||||
image FileStorage? @relation(fields: [imageId], references: [id])
|
||||
imageId String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime @default(now())
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime @default(now())
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
model ProgramInovasi {
|
||||
@@ -125,6 +128,7 @@ model ProgramInovasi {
|
||||
|
||||
model MediaSosial {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
image FileStorage @relation(fields: [imageId], references: [id])
|
||||
imageId String
|
||||
iconUrl String? @db.VarChar(255)
|
||||
@@ -134,6 +138,84 @@ model MediaSosial {
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
//========================================= PROFILE ========================================= //
|
||||
model DesaAntiKorupsi {
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
deskripsi String @db.Text
|
||||
kategori KategoriDesaAntiKorupsi @relation(fields: [kategoriId], references: [id])
|
||||
kategoriId String
|
||||
file FileStorage @relation(fields: [fileId], references: [id])
|
||||
fileId String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime @default(now())
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
model KategoriDesaAntiKorupsi {
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime @default(now())
|
||||
isActive Boolean @default(true)
|
||||
DesaAntiKorupsi DesaAntiKorupsi[]
|
||||
}
|
||||
|
||||
//========================================= SDGS Desa ========================================= //
|
||||
model SDGSDesa {
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
jumlah String
|
||||
image FileStorage @relation(fields: [imageId], references: [id])
|
||||
imageId String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime @default(now())
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
//========================================= APBDes ========================================= //
|
||||
model APBDes {
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
jumlah String
|
||||
image FileStorage @relation("APBDesImage", fields: [imageId], references: [id])
|
||||
imageId String
|
||||
file FileStorage @relation("APBDesFile", fields: [fileId], references: [id])
|
||||
fileId String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime @default(now())
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
//========================================= PRESTASI DESA ========================================= //
|
||||
model PrestasiDesa {
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
deskripsi String @db.Text
|
||||
kategori KategoriPrestasiDesa @relation(fields: [kategoriId], references: [id])
|
||||
kategoriId String
|
||||
image FileStorage @relation(fields: [imageId], references: [id])
|
||||
imageId String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime @default(now())
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
model KategoriPrestasiDesa {
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime @default(now())
|
||||
isActive Boolean @default(true)
|
||||
PrestasiDesa PrestasiDesa[]
|
||||
}
|
||||
|
||||
//========================================= MENU PPID ========================================= //
|
||||
|
||||
//========================================= STRUKTUR PPID ========================================= //
|
||||
|
||||
BIN
public/assets/images/sosmed/telephone-call.png
Normal file
BIN
public/assets/images/sosmed/telephone-call.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
242
src/app/admin/(dashboard)/_state/landing-page/apbdes.ts
Normal file
242
src/app/admin/(dashboard)/_state/landing-page/apbdes.ts
Normal file
@@ -0,0 +1,242 @@
|
||||
import ApiFetch from "@/lib/api-fetch";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { toast } from "react-toastify";
|
||||
import { proxy } from "valtio";
|
||||
import { z } from "zod";
|
||||
|
||||
const templateapbDesaForm = z.object({
|
||||
name: z.string().min(1, "Judul minimal 1 karakter"),
|
||||
jumlah: z.string().min(1, "Deskripsi minimal 1 karakter"),
|
||||
imageId: z.string().min(1, "File minimal 1"),
|
||||
fileId: z.string().min(1, "File minimal 1"),
|
||||
});
|
||||
|
||||
const defaultapbdesForm = {
|
||||
name: "",
|
||||
jumlah: "",
|
||||
imageId: "",
|
||||
fileId: "",
|
||||
};
|
||||
|
||||
const apbdes = proxy({
|
||||
create: {
|
||||
form: { ...defaultapbdesForm },
|
||||
loading: false,
|
||||
async create() {
|
||||
const cek = templateapbDesaForm.safeParse(
|
||||
apbdes.create.form
|
||||
);
|
||||
if (!cek.success) {
|
||||
const err = `[${cek.error.issues
|
||||
.map((v) => `${v.path.join(".")}`)
|
||||
.join("\n")}] required`;
|
||||
return toast.error(err);
|
||||
}
|
||||
try {
|
||||
apbdes.create.loading = true;
|
||||
const res = await ApiFetch.api.landingpage.apbdes[
|
||||
"create"
|
||||
].post({
|
||||
...apbdes.create.form,
|
||||
});
|
||||
|
||||
if (res.status === 200) {
|
||||
apbdes.findMany.load();
|
||||
return toast.success("Data berhasil ditambahkan");
|
||||
}
|
||||
return toast.error("Gagal menambahkan data");
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
toast.error("Gagal menambahkan data");
|
||||
} finally {
|
||||
apbdes.create.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
findMany: {
|
||||
data: null as Array<
|
||||
Prisma.APBDesGetPayload<{
|
||||
include: {
|
||||
image: true;
|
||||
file: true;
|
||||
};
|
||||
}>
|
||||
> | null,
|
||||
async load() {
|
||||
const res = await ApiFetch.api.landingpage.apbdes[
|
||||
"find-many"
|
||||
].get();
|
||||
if (res.status === 200) {
|
||||
apbdes.findMany.data = res.data?.data ?? [];
|
||||
}
|
||||
},
|
||||
},
|
||||
findUnique: {
|
||||
data: null as Prisma.APBDesGetPayload<{
|
||||
include: {
|
||||
image: true;
|
||||
file: true;
|
||||
};
|
||||
}> | null,
|
||||
async load(id: string) {
|
||||
try {
|
||||
const res = await fetch(`/api/landingpage/apbdes/${id}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
apbdes.findUnique.data = data.data ?? null;
|
||||
} else {
|
||||
console.error("Failed to fetch data", res.status, res.statusText);
|
||||
apbdes.findUnique.data = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
apbdes.findUnique.data = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
delete: {
|
||||
loading: false,
|
||||
async byId(id: string) {
|
||||
if (!id) return toast.warn("ID tidak valid");
|
||||
|
||||
try {
|
||||
apbdes.delete.loading = true;
|
||||
|
||||
const response = await fetch(
|
||||
`/api/landingpage/apbdes/del/${id}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result?.success) {
|
||||
toast.success(result.message || "apbdes berhasil dihapus");
|
||||
await apbdes.findMany.load(); // refresh list
|
||||
} else {
|
||||
toast.error(result?.message || "Gagal menghapus apbdes");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Gagal delete:", error);
|
||||
toast.error("Terjadi kesalahan saat menghapus apbdes");
|
||||
} finally {
|
||||
apbdes.delete.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
edit: {
|
||||
id: "",
|
||||
form: { ...defaultapbdesForm },
|
||||
loading: false,
|
||||
|
||||
async load(id: string) {
|
||||
if (!id) {
|
||||
toast.warn("ID tidak valid");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
apbdes.edit.loading = true;
|
||||
|
||||
const response = await fetch(`/api/landingpage/apbdes/${id}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const result = await response.json();
|
||||
if (result?.success) {
|
||||
const data = result.data;
|
||||
this.id = data.id;
|
||||
this.form = {
|
||||
name: data.name,
|
||||
jumlah: data.jumlah,
|
||||
imageId: data.imageId,
|
||||
fileId: data.fileId,
|
||||
};
|
||||
return data;
|
||||
} else {
|
||||
throw new Error(result?.message || "Gagal memuat data");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading apbdes:", error);
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "Gagal memuat data"
|
||||
);
|
||||
return null;
|
||||
} finally {
|
||||
apbdes.edit.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async update() {
|
||||
const cek = templateapbDesaForm.safeParse(
|
||||
apbdes.edit.form
|
||||
);
|
||||
if (!cek.success) {
|
||||
const err = `[${cek.error.issues
|
||||
.map((v) => `${v.path.join(".")}`)
|
||||
.join("\n")}] required`;
|
||||
return toast.error(err);
|
||||
}
|
||||
|
||||
try {
|
||||
apbdes.edit.loading = true;
|
||||
const response = await fetch(
|
||||
`/api/landingpage/apbdes/${this.id}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: this.form.name,
|
||||
jumlah: this.form.jumlah,
|
||||
imageId: this.form.imageId,
|
||||
fileId: this.form.fileId,
|
||||
}),
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(
|
||||
errorData.message || `HTTP error! status: ${response.status}`
|
||||
);
|
||||
}
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
toast.success("Berhasil update apbdes");
|
||||
await apbdes.findMany.load(); // refresh list
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(
|
||||
result.message || "Gagal mengupdate apbdes"
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating apbdes:", error);
|
||||
toast.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Gagal mengupdate apbdes"
|
||||
);
|
||||
return false;
|
||||
} finally {
|
||||
apbdes.edit.loading = false;
|
||||
}
|
||||
},
|
||||
reset() {
|
||||
apbdes.edit.id = "";
|
||||
apbdes.edit.form = { ...defaultapbdesForm };
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default apbdes;
|
||||
@@ -0,0 +1,485 @@
|
||||
import ApiFetch from "@/lib/api-fetch";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { toast } from "react-toastify";
|
||||
import { proxy } from "valtio";
|
||||
import { z } from "zod";
|
||||
|
||||
const templateDesaAntiKorupsiForm = z.object({
|
||||
name: z.string().min(1, "Judul minimal 1 karakter"),
|
||||
deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
|
||||
kategoriId: z.string().min(1, "Kategori minimal 1"),
|
||||
fileId: z.string().min(1, "File minimal 1"),
|
||||
});
|
||||
|
||||
const defaultDesaAntiKorupsiForm = {
|
||||
name: "",
|
||||
deskripsi: "",
|
||||
kategoriId: "",
|
||||
fileId: "",
|
||||
};
|
||||
|
||||
const desaAntikorupsi = proxy({
|
||||
create: {
|
||||
form: { ...defaultDesaAntiKorupsiForm },
|
||||
loading: false,
|
||||
async create() {
|
||||
const cek = templateDesaAntiKorupsiForm.safeParse(
|
||||
desaAntikorupsi.create.form
|
||||
);
|
||||
if (!cek.success) {
|
||||
const err = `[${cek.error.issues
|
||||
.map((v) => `${v.path.join(".")}`)
|
||||
.join("\n")}] required`;
|
||||
return toast.error(err);
|
||||
}
|
||||
try {
|
||||
desaAntikorupsi.create.loading = true;
|
||||
const res = await ApiFetch.api.landingpage.desaantikorupsi[
|
||||
"create"
|
||||
].post({
|
||||
...desaAntikorupsi.create.form,
|
||||
});
|
||||
|
||||
if (res.status === 200) {
|
||||
desaAntikorupsi.findMany.load();
|
||||
return toast.success("Data berhasil ditambahkan");
|
||||
}
|
||||
return toast.error("Gagal menambahkan data");
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
toast.error("Gagal menambahkan data");
|
||||
} finally {
|
||||
desaAntikorupsi.create.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
findMany: {
|
||||
data: null as Array<
|
||||
Prisma.DesaAntiKorupsiGetPayload<{
|
||||
include: {
|
||||
file: true;
|
||||
kategori: true;
|
||||
};
|
||||
}>
|
||||
> | null,
|
||||
async load() {
|
||||
const res = await ApiFetch.api.landingpage.desaantikorupsi[
|
||||
"find-many"
|
||||
].get();
|
||||
if (res.status === 200) {
|
||||
desaAntikorupsi.findMany.data = res.data?.data ?? [];
|
||||
}
|
||||
},
|
||||
},
|
||||
findUnique: {
|
||||
data: null as Prisma.DesaAntiKorupsiGetPayload<{
|
||||
include: {
|
||||
file: true;
|
||||
kategori: true;
|
||||
};
|
||||
}> | null,
|
||||
async load(id: string) {
|
||||
try {
|
||||
const res = await fetch(`/api/landingpage/desaantikorupsi/${id}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
desaAntikorupsi.findUnique.data = data.data ?? null;
|
||||
} else {
|
||||
console.error("Failed to fetch data", res.status, res.statusText);
|
||||
desaAntikorupsi.findUnique.data = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
desaAntikorupsi.findUnique.data = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
delete: {
|
||||
loading: false,
|
||||
async byId(id: string) {
|
||||
if (!id) return toast.warn("ID tidak valid");
|
||||
|
||||
try {
|
||||
desaAntikorupsi.delete.loading = true;
|
||||
|
||||
const response = await fetch(
|
||||
`/api/landingpage/desaantikorupsi/del/${id}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result?.success) {
|
||||
toast.success(result.message || "desa anti korupsi berhasil dihapus");
|
||||
await desaAntikorupsi.findMany.load(); // refresh list
|
||||
} else {
|
||||
toast.error(result?.message || "Gagal menghapus desa anti korupsi");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Gagal delete:", error);
|
||||
toast.error("Terjadi kesalahan saat menghapus desa anti korupsi");
|
||||
} finally {
|
||||
desaAntikorupsi.delete.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
edit: {
|
||||
id: "",
|
||||
form: { ...defaultDesaAntiKorupsiForm },
|
||||
loading: false,
|
||||
|
||||
async load(id: string) {
|
||||
if (!id) {
|
||||
toast.warn("ID tidak valid");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
desaAntikorupsi.edit.loading = true;
|
||||
|
||||
const response = await fetch(`/api/landingpage/desaantikorupsi/${id}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const result = await response.json();
|
||||
if (result?.success) {
|
||||
const data = result.data;
|
||||
this.id = data.id;
|
||||
this.form = {
|
||||
name: data.name,
|
||||
deskripsi: data.deskripsi,
|
||||
kategoriId: data.kategoriId,
|
||||
fileId: data.fileId,
|
||||
};
|
||||
return data;
|
||||
} else {
|
||||
throw new Error(result?.message || "Gagal memuat data");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading desa anti korupsi:", error);
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "Gagal memuat data"
|
||||
);
|
||||
return null;
|
||||
} finally {
|
||||
desaAntikorupsi.edit.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async update() {
|
||||
const cek = templateDesaAntiKorupsiForm.safeParse(
|
||||
desaAntikorupsi.edit.form
|
||||
);
|
||||
if (!cek.success) {
|
||||
const err = `[${cek.error.issues
|
||||
.map((v) => `${v.path.join(".")}`)
|
||||
.join("\n")}] required`;
|
||||
return toast.error(err);
|
||||
}
|
||||
|
||||
try {
|
||||
desaAntikorupsi.edit.loading = true;
|
||||
const response = await fetch(
|
||||
`/api/landingpage/desaantikorupsi/${this.id}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: this.form.name,
|
||||
deskripsi: this.form.deskripsi,
|
||||
kategoriId: this.form.kategoriId,
|
||||
fileId: this.form.fileId,
|
||||
}),
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(
|
||||
errorData.message || `HTTP error! status: ${response.status}`
|
||||
);
|
||||
}
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
toast.success("Berhasil update desa anti korupsi");
|
||||
await desaAntikorupsi.findMany.load(); // refresh list
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(
|
||||
result.message || "Gagal mengupdate desa anti korupsi"
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating desa anti korupsi:", error);
|
||||
toast.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Gagal mengupdate desa anti korupsi"
|
||||
);
|
||||
return false;
|
||||
} finally {
|
||||
desaAntikorupsi.edit.loading = false;
|
||||
}
|
||||
},
|
||||
reset() {
|
||||
desaAntikorupsi.edit.id = "";
|
||||
desaAntikorupsi.edit.form = { ...defaultDesaAntiKorupsiForm };
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// ========================================= KATEGORI desa anti korupsi ========================================= //
|
||||
const kategoriDesaAntiKorupsiForm = z.object({
|
||||
name: z.string().min(1, "Nama minimal 1 karakter"),
|
||||
});
|
||||
|
||||
const kategoriDesaAntiKorupsiDefaultForm = {
|
||||
name: "",
|
||||
};
|
||||
|
||||
const kategoriDesaAntiKorupsi = proxy({
|
||||
create: {
|
||||
form: { ...kategoriDesaAntiKorupsiDefaultForm },
|
||||
loading: false,
|
||||
async create() {
|
||||
const cek = kategoriDesaAntiKorupsiForm.safeParse(
|
||||
kategoriDesaAntiKorupsi.create.form
|
||||
);
|
||||
if (!cek.success) {
|
||||
const err = `[${cek.error.issues
|
||||
.map((v) => `${v.path.join(".")}`)
|
||||
.join("\n")}] required`;
|
||||
return toast.error(err);
|
||||
}
|
||||
try {
|
||||
kategoriDesaAntiKorupsi.create.loading = true;
|
||||
const res = await ApiFetch.api.landingpage.kategoridak["create"].post(
|
||||
kategoriDesaAntiKorupsi.create.form
|
||||
);
|
||||
if (res.status === 200) {
|
||||
kategoriDesaAntiKorupsi.findMany.load();
|
||||
return toast.success("Data berhasil ditambahkan");
|
||||
}
|
||||
return toast.error("Gagal menambahkan data");
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
toast.error("Gagal menambahkan data");
|
||||
} finally {
|
||||
kategoriDesaAntiKorupsi.create.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
findMany: {
|
||||
data: null as Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
}> | null,
|
||||
async load() {
|
||||
const res = await ApiFetch.api.landingpage.kategoridak["find-many"].get();
|
||||
if (res.status === 200) {
|
||||
kategoriDesaAntiKorupsi.findMany.data = res.data?.data ?? [];
|
||||
}
|
||||
},
|
||||
},
|
||||
findUnique: {
|
||||
data: null as Prisma.KategoriDesaAntiKorupsiGetPayload<{
|
||||
omit: { isActive: true };
|
||||
}> | null,
|
||||
async load(id: string) {
|
||||
try {
|
||||
const res = await fetch(`/api/landingpage/kategoridak/${id}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
kategoriDesaAntiKorupsi.findUnique.data = data.data ?? null;
|
||||
} else {
|
||||
console.error("Failed to fetch data", res.status, res.statusText);
|
||||
kategoriDesaAntiKorupsi.findUnique.data = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
kategoriDesaAntiKorupsi.findUnique.data = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
delete: {
|
||||
loading: false,
|
||||
async byId(id: string) {
|
||||
if (!id) return toast.warn("ID tidak valid");
|
||||
|
||||
try {
|
||||
kategoriDesaAntiKorupsi.delete.loading = true;
|
||||
|
||||
const response = await fetch(
|
||||
`/api/landingpage/kategoridak/del/${id}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result?.success) {
|
||||
toast.success(result.message || "Kategori desa anti korupsi berhasil dihapus");
|
||||
await kategoriDesaAntiKorupsi.findMany.load(); // refresh list
|
||||
} else {
|
||||
toast.error(result?.message || "Gagal menghapus kategori desa anti korupsi");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Gagal delete:", error);
|
||||
toast.error("Terjadi kesalahan saat menghapus kategori desa anti korupsi");
|
||||
} finally {
|
||||
kategoriDesaAntiKorupsi.delete.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
edit: {
|
||||
id: "",
|
||||
form: { ...kategoriDesaAntiKorupsiDefaultForm },
|
||||
loading: false,
|
||||
|
||||
async load(id: string) {
|
||||
if (!id) {
|
||||
toast.warn("ID tidak valid");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/landingpage/kategoridak/${id}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const result = await response.json();
|
||||
if (result?.success) {
|
||||
const data = result.data;
|
||||
this.id = data.id;
|
||||
this.form = {
|
||||
name: data.name,
|
||||
};
|
||||
return data;
|
||||
} else {
|
||||
throw new Error(result?.message || "Gagal memuat data");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading kategori desa anti korupsi:", error);
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "Gagal memuat data"
|
||||
);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
async update() {
|
||||
const cek = kategoriDesaAntiKorupsiForm.safeParse(
|
||||
kategoriDesaAntiKorupsi.edit.form
|
||||
);
|
||||
if (!cek.success) {
|
||||
const err = `[${cek.error.issues
|
||||
.map((v) => `${v.path.join(".")}`)
|
||||
.join("\n")}] required`;
|
||||
toast.error(err);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
kategoriDesaAntiKorupsi.edit.loading = true;
|
||||
const response = await fetch(
|
||||
`/api/landingpage/kategoridak/${kategoriDesaAntiKorupsi.edit.id}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: kategoriDesaAntiKorupsi.edit.form.name,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
// Clone the response to avoid 'body already read' error
|
||||
const responseClone = response.clone();
|
||||
|
||||
try {
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(
|
||||
"Update failed with status:",
|
||||
response.status,
|
||||
"Response:",
|
||||
result
|
||||
);
|
||||
throw new Error(
|
||||
result?.message ||
|
||||
`Gagal mengupdate kategori desa anti korupsi (${response.status})`
|
||||
);
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
toast.success(
|
||||
result.message ||
|
||||
"Berhasil memperbarui kategori desa anti korupsi"
|
||||
);
|
||||
await kategoriDesaAntiKorupsi.findMany.load(); // refresh list
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(
|
||||
result.message || "Gagal mengupdate kategori desa anti korupsi"
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
// If JSON parsing fails, try to get the response text for better error messages
|
||||
try {
|
||||
const text = await responseClone.text();
|
||||
console.error("Error response text:", text);
|
||||
throw new Error(`Gagal memproses respons dari server: ${text}`);
|
||||
} catch (textError) {
|
||||
console.error("Error parsing response as text:", textError);
|
||||
console.error("Original error:", error);
|
||||
throw new Error("Gagal memproses respons dari server");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating kategori desa anti korupsi:", error);
|
||||
toast.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Gagal mengupdate kategori desa anti korupsi"
|
||||
);
|
||||
return false;
|
||||
} finally {
|
||||
kategoriDesaAntiKorupsi.edit.loading = false;
|
||||
}
|
||||
},
|
||||
reset() {
|
||||
kategoriDesaAntiKorupsi.edit.id = "";
|
||||
kategoriDesaAntiKorupsi.edit.form = {
|
||||
...kategoriDesaAntiKorupsiDefaultForm,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const korupsiState = proxy({
|
||||
desaAntikorupsi,
|
||||
kategoriDesaAntiKorupsi,
|
||||
});
|
||||
export default korupsiState;
|
||||
486
src/app/admin/(dashboard)/_state/landing-page/prestasi-desa.ts
Normal file
486
src/app/admin/(dashboard)/_state/landing-page/prestasi-desa.ts
Normal file
@@ -0,0 +1,486 @@
|
||||
import ApiFetch from "@/lib/api-fetch";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { toast } from "react-toastify";
|
||||
import { proxy } from "valtio";
|
||||
import { z } from "zod";
|
||||
|
||||
const templateprestasiDesaForm = z.object({
|
||||
name: z.string().min(1, "Judul minimal 1 karakter"),
|
||||
deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
|
||||
imageId: z.string().min(1, "File minimal 1"),
|
||||
kategoriId: z.string().min(1, "Kategori minimal 1 karakter"),
|
||||
});
|
||||
|
||||
const defaultprestasiDesaForm = {
|
||||
name: "",
|
||||
deskripsi: "",
|
||||
imageId: "",
|
||||
kategoriId: "",
|
||||
};
|
||||
|
||||
const prestasiDesa = proxy({
|
||||
create: {
|
||||
form: { ...defaultprestasiDesaForm },
|
||||
loading: false,
|
||||
async create() {
|
||||
const cek = templateprestasiDesaForm.safeParse(
|
||||
prestasiDesa.create.form
|
||||
);
|
||||
if (!cek.success) {
|
||||
const err = `[${cek.error.issues
|
||||
.map((v) => `${v.path.join(".")}`)
|
||||
.join("\n")}] required`;
|
||||
return toast.error(err);
|
||||
}
|
||||
try {
|
||||
prestasiDesa.create.loading = true;
|
||||
const res = await ApiFetch.api.landingpage.prestasidesa[
|
||||
"create"
|
||||
].post({
|
||||
...prestasiDesa.create.form,
|
||||
});
|
||||
|
||||
if (res.status === 200) {
|
||||
prestasiDesa.findMany.load();
|
||||
return toast.success("Data berhasil ditambahkan");
|
||||
}
|
||||
return toast.error("Gagal menambahkan data");
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
toast.error("Gagal menambahkan data");
|
||||
} finally {
|
||||
prestasiDesa.create.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
findMany: {
|
||||
data: null as Array<
|
||||
Prisma.PrestasiDesaGetPayload<{
|
||||
include: {
|
||||
image: true;
|
||||
kategori: true;
|
||||
};
|
||||
}>
|
||||
> | null,
|
||||
async load() {
|
||||
const res = await ApiFetch.api.landingpage.prestasidesa[
|
||||
"find-many"
|
||||
].get();
|
||||
if (res.status === 200) {
|
||||
prestasiDesa.findMany.data = res.data?.data ?? [];
|
||||
}
|
||||
},
|
||||
},
|
||||
findUnique: {
|
||||
data: null as Prisma.PrestasiDesaGetPayload<{
|
||||
include: {
|
||||
image: true;
|
||||
kategori: true;
|
||||
};
|
||||
}> | null,
|
||||
async load(id: string) {
|
||||
try {
|
||||
const res = await fetch(`/api/landingpage/prestasidesa/${id}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
prestasiDesa.findUnique.data = data.data ?? null;
|
||||
} else {
|
||||
console.error("Failed to fetch data", res.status, res.statusText);
|
||||
prestasiDesa.findUnique.data = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
prestasiDesa.findUnique.data = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
delete: {
|
||||
loading: false,
|
||||
async byId(id: string) {
|
||||
if (!id) return toast.warn("ID tidak valid");
|
||||
|
||||
try {
|
||||
prestasiDesa.delete.loading = true;
|
||||
|
||||
const response = await fetch(
|
||||
`/api/landingpage/prestasidesa/del/${id}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result?.success) {
|
||||
toast.success(result.message || "prestasi desa berhasil dihapus");
|
||||
await prestasiDesa.findMany.load(); // refresh list
|
||||
} else {
|
||||
toast.error(result?.message || "Gagal menghapus prestasi desa");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Gagal delete:", error);
|
||||
toast.error("Terjadi kesalahan saat menghapus prestasi desa");
|
||||
} finally {
|
||||
prestasiDesa.delete.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
edit: {
|
||||
id: "",
|
||||
form: { ...defaultprestasiDesaForm },
|
||||
loading: false,
|
||||
|
||||
async load(id: string) {
|
||||
if (!id) {
|
||||
toast.warn("ID tidak valid");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
prestasiDesa.edit.loading = true;
|
||||
|
||||
const response = await fetch(`/api/landingpage/prestasidesa/${id}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const result = await response.json();
|
||||
if (result?.success) {
|
||||
const data = result.data;
|
||||
this.id = data.id;
|
||||
this.form = {
|
||||
name: data.name,
|
||||
deskripsi: data.deskripsi,
|
||||
imageId: data.imageId,
|
||||
kategoriId: data.kategoriId,
|
||||
};
|
||||
return data;
|
||||
} else {
|
||||
throw new Error(result?.message || "Gagal memuat data");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading prestasi desa:", error);
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "Gagal memuat data"
|
||||
);
|
||||
return null;
|
||||
} finally {
|
||||
prestasiDesa.edit.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async update() {
|
||||
const cek = templateprestasiDesaForm.safeParse(
|
||||
prestasiDesa.edit.form
|
||||
);
|
||||
if (!cek.success) {
|
||||
const err = `[${cek.error.issues
|
||||
.map((v) => `${v.path.join(".")}`)
|
||||
.join("\n")}] required`;
|
||||
return toast.error(err);
|
||||
}
|
||||
|
||||
try {
|
||||
prestasiDesa.edit.loading = true;
|
||||
const response = await fetch(
|
||||
`/api/landingpage/prestasidesa/${this.id}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: this.form.name,
|
||||
deskripsi: this.form.deskripsi,
|
||||
imageId: this.form.imageId,
|
||||
kategoriId: this.form.kategoriId,
|
||||
}),
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(
|
||||
errorData.message || `HTTP error! status: ${response.status}`
|
||||
);
|
||||
}
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
toast.success("Berhasil update prestasi desa");
|
||||
await prestasiDesa.findMany.load(); // refresh list
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(
|
||||
result.message || "Gagal mengupdate prestasi desa"
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating prestasi desa:", error);
|
||||
toast.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Gagal mengupdate prestasi desa"
|
||||
);
|
||||
return false;
|
||||
} finally {
|
||||
prestasiDesa.edit.loading = false;
|
||||
}
|
||||
},
|
||||
reset() {
|
||||
prestasiDesa.edit.id = "";
|
||||
prestasiDesa.edit.form = { ...defaultprestasiDesaForm };
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// ========================================= KATEGORI kegiatan ========================================= //
|
||||
const kategoriPrestasiForm = z.object({
|
||||
name: z.string().min(1, "Nama minimal 1 karakter"),
|
||||
});
|
||||
|
||||
const kategoriPrestasiDefaultForm = {
|
||||
name: "",
|
||||
};
|
||||
|
||||
const kategoriPrestasi = proxy({
|
||||
create: {
|
||||
form: { ...kategoriPrestasiDefaultForm },
|
||||
loading: false,
|
||||
async create() {
|
||||
const cek = kategoriPrestasiForm.safeParse(kategoriPrestasi.create.form);
|
||||
if (!cek.success) {
|
||||
const err = `[${cek.error.issues
|
||||
.map((v) => `${v.path.join(".")}`)
|
||||
.join("\n")}] required`;
|
||||
return toast.error(err);
|
||||
}
|
||||
try {
|
||||
kategoriPrestasi.create.loading = true;
|
||||
const res = await ApiFetch.api.landingpage.kategoriprestasi[
|
||||
"create"
|
||||
].post(kategoriPrestasi.create.form);
|
||||
if (res.status === 200) {
|
||||
kategoriPrestasi.findMany.load();
|
||||
return toast.success("Data berhasil ditambahkan");
|
||||
}
|
||||
return toast.error("Gagal menambahkan data");
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
toast.error("Gagal menambahkan data");
|
||||
} finally {
|
||||
kategoriPrestasi.create.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
findMany: {
|
||||
data: null as Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
}> | null,
|
||||
async load() {
|
||||
const res = await ApiFetch.api.landingpage.kategoriprestasi[
|
||||
"find-many"
|
||||
].get();
|
||||
if (res.status === 200) {
|
||||
kategoriPrestasi.findMany.data = res.data?.data ?? [];
|
||||
}
|
||||
},
|
||||
},
|
||||
findUnique: {
|
||||
data: null as Prisma.KategoriPrestasiDesaGetPayload<{
|
||||
omit: { isActive: true };
|
||||
}> | null,
|
||||
async load(id: string) {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/landingpage/kategoriprestasi/${id}`
|
||||
);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
kategoriPrestasi.findUnique.data = data.data ?? null;
|
||||
} else {
|
||||
console.error("Failed to fetch data", res.status, res.statusText);
|
||||
kategoriPrestasi.findUnique.data = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
kategoriPrestasi.findUnique.data = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
delete: {
|
||||
loading: false,
|
||||
async byId(id: string) {
|
||||
if (!id) return toast.warn("ID tidak valid");
|
||||
|
||||
try {
|
||||
kategoriPrestasi.delete.loading = true;
|
||||
|
||||
const response = await fetch(
|
||||
`/api/landingpage/kategoriprestasi/del/${id}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result?.success) {
|
||||
toast.success(result.message || "Kategori prestasi berhasil dihapus");
|
||||
await kategoriPrestasi.findMany.load(); // refresh list
|
||||
} else {
|
||||
toast.error(result?.message || "Gagal menghapus kategori prestasi");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Gagal delete:", error);
|
||||
toast.error("Terjadi kesalahan saat menghapus kategori prestasi");
|
||||
} finally {
|
||||
kategoriPrestasi.delete.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
edit: {
|
||||
id: "",
|
||||
form: { ...kategoriPrestasiDefaultForm },
|
||||
loading: false,
|
||||
|
||||
async load(id: string) {
|
||||
if (!id) {
|
||||
toast.warn("ID tidak valid");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/landingpage/kategoriprestasi/${id}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const result = await response.json();
|
||||
if (result?.success) {
|
||||
const data = result.data;
|
||||
this.id = data.id;
|
||||
this.form = {
|
||||
name: data.name,
|
||||
};
|
||||
return data;
|
||||
} else {
|
||||
throw new Error(result?.message || "Gagal memuat data");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading kategori prestasi:", error);
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "Gagal memuat data"
|
||||
);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
async update() {
|
||||
const cek = kategoriPrestasiForm.safeParse(kategoriPrestasi.edit.form);
|
||||
if (!cek.success) {
|
||||
const err = `[${cek.error.issues
|
||||
.map((v) => `${v.path.join(".")}`)
|
||||
.join("\n")}] required`;
|
||||
toast.error(err);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
kategoriPrestasi.edit.loading = true;
|
||||
const response = await fetch(
|
||||
`/api/landingpage/kategoriprestasi/${kategoriPrestasi.edit.id}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: kategoriPrestasi.edit.form.name,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
// Clone the response to avoid 'body already read' error
|
||||
const responseClone = response.clone();
|
||||
|
||||
try {
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(
|
||||
"Update failed with status:",
|
||||
response.status,
|
||||
"Response:",
|
||||
result
|
||||
);
|
||||
throw new Error(
|
||||
result?.message ||
|
||||
`Gagal mengupdate kategori prestasi (${response.status})`
|
||||
);
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
toast.success(
|
||||
result.message || "Berhasil memperbarui kategori prestasi"
|
||||
);
|
||||
await kategoriPrestasi.findMany.load(); // refresh list
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(
|
||||
result.message || "Gagal mengupdate kategori prestasi"
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
// If JSON parsing fails, try to get the response text for better error messages
|
||||
try {
|
||||
const text = await responseClone.text();
|
||||
console.error("Error response text:", text);
|
||||
throw new Error(`Gagal memproses respons dari server: ${text}`);
|
||||
} catch (textError) {
|
||||
console.error("Error parsing response as text:", textError);
|
||||
console.error("Original error:", error);
|
||||
throw new Error("Gagal memproses respons dari server");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating kategori prestasi:", error);
|
||||
toast.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Gagal mengupdate kategori prestasi"
|
||||
);
|
||||
return false;
|
||||
} finally {
|
||||
kategoriPrestasi.edit.loading = false;
|
||||
}
|
||||
},
|
||||
reset() {
|
||||
kategoriPrestasi.edit.id = "";
|
||||
kategoriPrestasi.edit.form = { ...kategoriPrestasiDefaultForm };
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const prestasiState = proxy({
|
||||
prestasiDesa,
|
||||
kategoriPrestasi,
|
||||
});
|
||||
|
||||
export default prestasiState;
|
||||
@@ -404,11 +404,13 @@ const pejabatDesa = proxy({
|
||||
});
|
||||
|
||||
const templateMediaSosial = z.object({
|
||||
name: z.string().min(3, "Nama minimal 3 karakter"),
|
||||
imageId: z.string().min(1, "Gambar wajib dipilih"),
|
||||
iconUrl: z.string().min(3, "Icon URL minimal 3 karakter"),
|
||||
});
|
||||
|
||||
type MediaSosialForm = {
|
||||
name: string;
|
||||
imageId: string;
|
||||
iconUrl: string;
|
||||
};
|
||||
@@ -420,6 +422,7 @@ const mediaSosial = proxy({
|
||||
async create() {
|
||||
// Ensure all required fields are non-null
|
||||
const formData = {
|
||||
name: mediaSosial.create.form.name || "",
|
||||
imageId: mediaSosial.create.form.imageId || "",
|
||||
iconUrl: mediaSosial.create.form.iconUrl || "",
|
||||
};
|
||||
@@ -467,6 +470,12 @@ const mediaSosial = proxy({
|
||||
};
|
||||
}> | null,
|
||||
async load(id: string) {
|
||||
if (!id) {
|
||||
toast.warn("ID tidak valid");
|
||||
return null;
|
||||
}
|
||||
|
||||
mediaSosial.update.loading = true;
|
||||
try {
|
||||
const res = await fetch(`/api/landingpage/mediasosial/${id}`);
|
||||
if (res.ok) {
|
||||
@@ -523,7 +532,9 @@ const mediaSosial = proxy({
|
||||
toast.warn("ID tidak valid");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
mediaSosial.update.loading = true; // ✅ Tambahkan ini di awal
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/landingpage/mediasosial/${id}`, {
|
||||
method: "GET",
|
||||
@@ -531,18 +542,20 @@ const mediaSosial = proxy({
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
|
||||
if (result?.success) {
|
||||
const data = result.data;
|
||||
this.id = data.id;
|
||||
this.form = {
|
||||
imageId: data.imageId,
|
||||
iconUrl: data.iconUrl,
|
||||
name: data.name || "",
|
||||
imageId: data.imageId || "",
|
||||
iconUrl: data.iconUrl || "",
|
||||
};
|
||||
return data;
|
||||
} else {
|
||||
@@ -552,10 +565,10 @@ const mediaSosial = proxy({
|
||||
console.error((error as Error).message);
|
||||
toast.error("Terjadi kesalahan saat mengambil data media sosial");
|
||||
} finally {
|
||||
mediaSosial.update.loading = false;
|
||||
mediaSosial.update.loading = false; // ✅ Supaya berhenti loading walau error
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
async update() {
|
||||
const cek = templateMediaSosial.safeParse(mediaSosial.update.form);
|
||||
if (!cek.success) {
|
||||
@@ -575,6 +588,7 @@ const mediaSosial = proxy({
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: this.form.name,
|
||||
imageId: this.form.imageId,
|
||||
iconUrl: this.form.iconUrl,
|
||||
}),
|
||||
|
||||
236
src/app/admin/(dashboard)/_state/landing-page/sdgs-desa.ts
Normal file
236
src/app/admin/(dashboard)/_state/landing-page/sdgs-desa.ts
Normal file
@@ -0,0 +1,236 @@
|
||||
import ApiFetch from "@/lib/api-fetch";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { toast } from "react-toastify";
|
||||
import { proxy } from "valtio";
|
||||
import { z } from "zod";
|
||||
|
||||
const templatesdgsDesaForm = z.object({
|
||||
name: z.string().min(1, "Judul minimal 1 karakter"),
|
||||
jumlah: z.string().min(1, "Deskripsi minimal 1 karakter"),
|
||||
imageId: z.string().min(1, "File minimal 1"),
|
||||
});
|
||||
|
||||
const defaultsdgsDesaForm = {
|
||||
name: "",
|
||||
jumlah: "",
|
||||
imageId: "",
|
||||
};
|
||||
|
||||
const sdgsDesa = proxy({
|
||||
create: {
|
||||
form: { ...defaultsdgsDesaForm },
|
||||
loading: false,
|
||||
async create() {
|
||||
const cek = templatesdgsDesaForm.safeParse(
|
||||
sdgsDesa.create.form
|
||||
);
|
||||
if (!cek.success) {
|
||||
const err = `[${cek.error.issues
|
||||
.map((v) => `${v.path.join(".")}`)
|
||||
.join("\n")}] required`;
|
||||
return toast.error(err);
|
||||
}
|
||||
try {
|
||||
sdgsDesa.create.loading = true;
|
||||
const res = await ApiFetch.api.landingpage.sdgsdesa[
|
||||
"create"
|
||||
].post({
|
||||
...sdgsDesa.create.form,
|
||||
});
|
||||
|
||||
if (res.status === 200) {
|
||||
sdgsDesa.findMany.load();
|
||||
return toast.success("Data berhasil ditambahkan");
|
||||
}
|
||||
return toast.error("Gagal menambahkan data");
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
toast.error("Gagal menambahkan data");
|
||||
} finally {
|
||||
sdgsDesa.create.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
findMany: {
|
||||
data: null as Array<
|
||||
Prisma.SDGSDesaGetPayload<{
|
||||
include: {
|
||||
image: true;
|
||||
};
|
||||
}>
|
||||
> | null,
|
||||
async load() {
|
||||
const res = await ApiFetch.api.landingpage.sdgsdesa[
|
||||
"find-many"
|
||||
].get();
|
||||
if (res.status === 200) {
|
||||
sdgsDesa.findMany.data = res.data?.data ?? [];
|
||||
}
|
||||
},
|
||||
},
|
||||
findUnique: {
|
||||
data: null as Prisma.SDGSDesaGetPayload<{
|
||||
include: {
|
||||
image: true;
|
||||
};
|
||||
}> | null,
|
||||
async load(id: string) {
|
||||
try {
|
||||
const res = await fetch(`/api/landingpage/sdgsdesa/${id}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
sdgsDesa.findUnique.data = data.data ?? null;
|
||||
} else {
|
||||
console.error("Failed to fetch data", res.status, res.statusText);
|
||||
sdgsDesa.findUnique.data = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
sdgsDesa.findUnique.data = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
delete: {
|
||||
loading: false,
|
||||
async byId(id: string) {
|
||||
if (!id) return toast.warn("ID tidak valid");
|
||||
|
||||
try {
|
||||
sdgsDesa.delete.loading = true;
|
||||
|
||||
const response = await fetch(
|
||||
`/api/landingpage/sdgsdesa/del/${id}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result?.success) {
|
||||
toast.success(result.message || "sdgs desa berhasil dihapus");
|
||||
await sdgsDesa.findMany.load(); // refresh list
|
||||
} else {
|
||||
toast.error(result?.message || "Gagal menghapus sdgs desa");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Gagal delete:", error);
|
||||
toast.error("Terjadi kesalahan saat menghapus sdgs desa");
|
||||
} finally {
|
||||
sdgsDesa.delete.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
edit: {
|
||||
id: "",
|
||||
form: { ...defaultsdgsDesaForm },
|
||||
loading: false,
|
||||
|
||||
async load(id: string) {
|
||||
if (!id) {
|
||||
toast.warn("ID tidak valid");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
sdgsDesa.edit.loading = true;
|
||||
|
||||
const response = await fetch(`/api/landingpage/sdgsdesa/${id}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const result = await response.json();
|
||||
if (result?.success) {
|
||||
const data = result.data;
|
||||
this.id = data.id;
|
||||
this.form = {
|
||||
name: data.name,
|
||||
jumlah: data.jumlah,
|
||||
imageId: data.imageId,
|
||||
};
|
||||
return data;
|
||||
} else {
|
||||
throw new Error(result?.message || "Gagal memuat data");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading sdgs desa:", error);
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "Gagal memuat data"
|
||||
);
|
||||
return null;
|
||||
} finally {
|
||||
sdgsDesa.edit.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async update() {
|
||||
const cek = templatesdgsDesaForm.safeParse(
|
||||
sdgsDesa.edit.form
|
||||
);
|
||||
if (!cek.success) {
|
||||
const err = `[${cek.error.issues
|
||||
.map((v) => `${v.path.join(".")}`)
|
||||
.join("\n")}] required`;
|
||||
return toast.error(err);
|
||||
}
|
||||
|
||||
try {
|
||||
sdgsDesa.edit.loading = true;
|
||||
const response = await fetch(
|
||||
`/api/landingpage/sdgsdesa/${this.id}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: this.form.name,
|
||||
jumlah: this.form.jumlah,
|
||||
imageId: this.form.imageId,
|
||||
}),
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(
|
||||
errorData.message || `HTTP error! status: ${response.status}`
|
||||
);
|
||||
}
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
toast.success("Berhasil update sdgs desa");
|
||||
await sdgsDesa.findMany.load(); // refresh list
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(
|
||||
result.message || "Gagal mengupdate sdgs desa"
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating sdgs desa:", error);
|
||||
toast.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Gagal mengupdate sdgs desa"
|
||||
);
|
||||
return false;
|
||||
} finally {
|
||||
sdgsDesa.edit.loading = false;
|
||||
}
|
||||
},
|
||||
reset() {
|
||||
sdgsDesa.edit.id = "";
|
||||
sdgsDesa.edit.form = { ...defaultsdgsDesaForm };
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default sdgsDesa;
|
||||
258
src/app/admin/(dashboard)/landing-page/apbdes/[id]/edit/page.tsx
Normal file
258
src/app/admin/(dashboard)/landing-page/apbdes/[id]/edit/page.tsx
Normal file
@@ -0,0 +1,258 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
"use client";
|
||||
|
||||
|
||||
import apbdes from "@/app/admin/(dashboard)/_state/landing-page/apbdes";
|
||||
import colors from "@/con/colors";
|
||||
import ApiFetch from "@/lib/api-fetch";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Paper,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title
|
||||
} from "@mantine/core";
|
||||
import { Dropzone } from "@mantine/dropzone";
|
||||
import { IconArrowBack, IconFile, IconImageInPicture, 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 EditAPBDes() {
|
||||
const apbdesState = useProxy(apbdes);
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
|
||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||
const [previewDoc, setPreviewDoc] = useState<string | null>(null);
|
||||
const [imageFile, setImageFile] = useState<File | null>(null);
|
||||
const [docFile, setDocFile] = useState<File | null>(null);
|
||||
const [formData, setFormData] = useState({
|
||||
name: apbdesState.edit.form.name || '',
|
||||
jumlah: apbdesState.edit.form.jumlah || '',
|
||||
imageId: apbdesState.edit.form.imageId || '',
|
||||
fileId: apbdesState.edit.form.fileId || ''
|
||||
});
|
||||
|
||||
// Load sdgs desa by id saat pertama kali
|
||||
useEffect(() => {
|
||||
const loadKolaborasi = async () => {
|
||||
const id = params?.id as string;
|
||||
if (!id) return;
|
||||
|
||||
try {
|
||||
const data = await apbdesState.edit.load(id); // akses langsung, bukan dari proxy
|
||||
if (data) {
|
||||
setFormData({
|
||||
name: data.name || '',
|
||||
jumlah: data.jumlah || '',
|
||||
imageId: data.imageId || '',
|
||||
fileId: data.fileId || ''
|
||||
});
|
||||
if (data.image) {
|
||||
if (data?.image?.link) {
|
||||
setPreviewImage(data.image.link);
|
||||
}
|
||||
}
|
||||
if (data.file) {
|
||||
if (data?.file?.link) {
|
||||
setPreviewDoc(data.file.link);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading apbdes:", error);
|
||||
toast.error("Gagal memuat data apbdes");
|
||||
}
|
||||
};
|
||||
|
||||
loadKolaborasi();
|
||||
}, [params?.id]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
|
||||
try {
|
||||
// edit global state with form data
|
||||
apbdesState.edit.form = {
|
||||
...apbdesState.edit.form,
|
||||
name: formData.name,
|
||||
jumlah: formData.jumlah,
|
||||
imageId: formData.imageId // Keep existing imageId if not changed
|
||||
};
|
||||
|
||||
// Jika ada file image baru, upload
|
||||
if (imageFile) {
|
||||
const res = await ApiFetch.api.fileStorage.create.post({ file: imageFile, name: imageFile.name });
|
||||
const uploaded = res.data?.data;
|
||||
|
||||
if (!uploaded?.id) {
|
||||
return toast.error("Gagal upload gambar");
|
||||
}
|
||||
|
||||
// edit imageId in global state
|
||||
apbdesState.edit.form.imageId = uploaded.id;
|
||||
}
|
||||
|
||||
// Jika ada file doc baru, upload
|
||||
if (docFile) {
|
||||
const res = await ApiFetch.api.fileStorage.create.post({ file: docFile, name: docFile.name });
|
||||
const uploaded = res.data?.data;
|
||||
|
||||
if (!uploaded?.id) {
|
||||
return toast.error("Gagal upload doc");
|
||||
}
|
||||
|
||||
// edit fileId in global state
|
||||
apbdesState.edit.form.fileId = uploaded.id;
|
||||
}
|
||||
|
||||
await apbdesState.edit.update();
|
||||
toast.success("apbdes berhasil diperbarui!");
|
||||
router.push("/admin/landing-page/APBDes");
|
||||
} catch (error) {
|
||||
console.error("Error updating apbdes:", error);
|
||||
toast.error("Terjadi kesalahan saat memperbarui apbdes");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button variant="subtle" onClick={() => router.back()}>
|
||||
<IconArrowBack color={colors["blue-button"]} size={30} />
|
||||
</Button>
|
||||
</Box>
|
||||
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
|
||||
<Stack gap={"xs"}>
|
||||
<Title order={3}>Edit APBDes</Title>
|
||||
<TextInput
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
label={<Text fz={"sm"} fw={"bold"}>Nama</Text>}
|
||||
placeholder="masukkan nama"
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
value={formData.jumlah}
|
||||
onChange={(e) => setFormData({ ...formData, jumlah: e.target.value })}
|
||||
label={<Text fz={"sm"} fw={"bold"}>Jumlah</Text>}
|
||||
placeholder="masukkan jumlah"
|
||||
/>
|
||||
<Box>
|
||||
<Text fz={"md"} fw={"bold"}>File Image</Text>
|
||||
<Box>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const selectedFile = files[0]; // Ambil file pertama
|
||||
if (selectedFile) {
|
||||
setImageFile(selectedFile);
|
||||
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
|
||||
}
|
||||
}}
|
||||
onReject={() => toast.error('File tidak valid.')}
|
||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
||||
accept={{
|
||||
'application/*': ['.jpeg', '.jpg', '.png', '.gif', '.webp', '.svg'],
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
<IconImageInPicture size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
|
||||
<div>
|
||||
<Text size="xl" inline>
|
||||
Drag file ke sini atau klik untuk pilih image
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" inline mt={7}>
|
||||
Maksimal 5MB dan harus format image
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Image</Text>
|
||||
{previewImage ? (
|
||||
<iframe
|
||||
src={previewImage}
|
||||
width="100%"
|
||||
height="500px"
|
||||
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
|
||||
/>
|
||||
) : (
|
||||
<Text>Tidak ada image tersedia</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz={"md"} fw={"bold"}>File Doc</Text>
|
||||
<Box>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const selectedFile = files[0]; // Ambil file pertama
|
||||
if (selectedFile) {
|
||||
setDocFile(selectedFile);
|
||||
setPreviewDoc(URL.createObjectURL(selectedFile)); // Buat preview
|
||||
}
|
||||
}}
|
||||
onReject={() => toast.error('File tidak valid.')}
|
||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
||||
accept={{
|
||||
'application/*': ['.pdf', '.doc', '.docx'],
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
<IconFile size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
|
||||
<div>
|
||||
<Text size="xl" inline>
|
||||
Drag file ke sini atau klik untuk pilih file
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" inline mt={7}>
|
||||
Maksimal 5MB dan harus format doc
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Dokumen</Text>
|
||||
{previewDoc ? (
|
||||
<iframe
|
||||
src={previewDoc}
|
||||
width="100%"
|
||||
height="500px"
|
||||
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
|
||||
/>
|
||||
) : (
|
||||
<Text>Tidak ada doc tersedia</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Button onClick={handleSubmit}>Simpan</Button>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditAPBDes;
|
||||
126
src/app/admin/(dashboard)/landing-page/apbdes/[id]/page.tsx
Normal file
126
src/app/admin/(dashboard)/landing-page/apbdes/[id]/page.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
'use client'
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
import { ActionIcon, Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconArrowBack, IconEdit, IconFile, IconX } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
|
||||
import colors from '@/con/colors';
|
||||
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||
import apbdes from '../../../_state/landing-page/apbdes';
|
||||
|
||||
function DetailAPBDes() {
|
||||
const apbdesState = useProxy(apbdes)
|
||||
const [modalHapus, setModalHapus] = useState(false)
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||
const params = useParams()
|
||||
const router = useRouter()
|
||||
|
||||
useShallowEffect(() => {
|
||||
apbdesState.findUnique.load(params?.id as string)
|
||||
}, [])
|
||||
|
||||
|
||||
const handleHapus = () => {
|
||||
if (selectedId) {
|
||||
apbdesState.delete.byId(selectedId)
|
||||
setModalHapus(false)
|
||||
setSelectedId(null)
|
||||
router.push("/admin/landing-page/APBDes")
|
||||
}
|
||||
}
|
||||
|
||||
if (!apbdesState.findUnique.data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton h={40} />
|
||||
</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 APBDes</Text>
|
||||
{apbdesState.findUnique.data ? (
|
||||
<Paper key={apbdesState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
||||
<Stack gap={"xs"}>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Nama APBDes</Text>
|
||||
<Text fz={"lg"}>{apbdesState.findUnique.data?.name}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Jumlah</Text>
|
||||
<Text fz={"lg"}>{apbdesState.findUnique.data?.jumlah}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
|
||||
<Image w={{ base: 150, md: 150, lg: 150 }} src={apbdesState.findUnique.data?.image?.link} alt="gambar" />
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Dokumen</Text>
|
||||
{apbdesState.findUnique.data?.file?.link ? (
|
||||
<ActionIcon
|
||||
component="a"
|
||||
href={apbdesState.findUnique.data.file.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="transparent"
|
||||
>
|
||||
<IconFile size={25} color={colors['blue-button']}/>
|
||||
</ActionIcon>
|
||||
) : (
|
||||
<Text>Tidak ada dokumen tersedia</Text>
|
||||
)}
|
||||
</Box>
|
||||
<Flex gap={"xs"} mt={10}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (apbdesState.findUnique.data) {
|
||||
setSelectedId(apbdesState.findUnique.data.id);
|
||||
setModalHapus(true);
|
||||
}
|
||||
}}
|
||||
disabled={apbdesState.delete.loading || !apbdesState.findUnique.data}
|
||||
color={"red"}
|
||||
>
|
||||
<IconX size={20} />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (apbdesState.findUnique.data) {
|
||||
router.push(`/admin/landing-page/APBDes/${apbdesState.findUnique.data.id}/edit`);
|
||||
}
|
||||
}}
|
||||
disabled={!apbdesState.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 APBDes ini?'
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default DetailAPBDes;
|
||||
215
src/app/admin/(dashboard)/landing-page/apbdes/create/page.tsx
Normal file
215
src/app/admin/(dashboard)/landing-page/apbdes/create/page.tsx
Normal file
@@ -0,0 +1,215 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import ApiFetch from '@/lib/api-fetch';
|
||||
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import { Dropzone } from '@mantine/dropzone';
|
||||
import { IconArrowBack, IconFile, IconImageInPicture, IconUpload, IconX } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import apbdes from '../../../_state/landing-page/apbdes';
|
||||
|
||||
|
||||
function CreateAPBDes() {
|
||||
const router = useRouter();
|
||||
const stateAPBDes = useProxy(apbdes)
|
||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||
const [previewDoc, setPreviewDoc] = useState<string | null>(null);
|
||||
const [imageFile, setImageFile] = useState<File | null>(null);
|
||||
const [docFile, setDocFile] = useState<File | null>(null);
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
stateAPBDes.findMany.load();
|
||||
}, []);
|
||||
|
||||
const resetForm = () => {
|
||||
stateAPBDes.create.form = {
|
||||
name: "",
|
||||
jumlah: "",
|
||||
imageId: "",
|
||||
fileId: "",
|
||||
};
|
||||
setImageFile(null);
|
||||
setDocFile(null);
|
||||
setPreviewImage(null);
|
||||
};
|
||||
const handleSubmit = async () => {
|
||||
if (!imageFile || !docFile) {
|
||||
return toast.warn("Pilih gambar dan dokumen terlebih dahulu");
|
||||
}
|
||||
|
||||
try {
|
||||
const [uploadImageRes, uploadDocRes] = await Promise.all([
|
||||
ApiFetch.api.fileStorage.create.post({ file: imageFile, name: imageFile.name }),
|
||||
ApiFetch.api.fileStorage.create.post({ file: docFile, name: docFile.name }),
|
||||
]);
|
||||
|
||||
const imageId = uploadImageRes?.data?.data?.id;
|
||||
const fileId = uploadDocRes?.data?.data?.id;
|
||||
|
||||
if (!imageId || !fileId) {
|
||||
return toast.error("Gagal mengupload file");
|
||||
}
|
||||
|
||||
stateAPBDes.create.form.imageId = imageId;
|
||||
stateAPBDes.create.form.fileId = fileId;
|
||||
|
||||
await stateAPBDes.create.create();
|
||||
|
||||
toast.success("Berhasil menambahkan APBDes");
|
||||
resetForm();
|
||||
router.push("/admin/landing-page/APBDes");
|
||||
} catch (error) {
|
||||
console.error("Gagal submit:", error);
|
||||
toast.error("Gagal menyimpan data");
|
||||
}
|
||||
};
|
||||
|
||||
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 APBDes</Title>
|
||||
<Box>
|
||||
<Text fz={"md"} fw={"bold"}>File Image</Text>
|
||||
<Box>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const selectedFile = files[0]; // Ambil file pertama
|
||||
if (selectedFile) {
|
||||
setImageFile(selectedFile);
|
||||
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
|
||||
}
|
||||
}}
|
||||
onReject={() => toast.error('File tidak valid.')}
|
||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
||||
accept={{
|
||||
'application/*': ['.jpeg', '.jpg', '.png', '.gif', '.webp', '.svg'],
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
<IconImageInPicture size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
|
||||
<div>
|
||||
<Text size="xl" inline>
|
||||
Drag file ke sini atau klik untuk pilih file
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" inline mt={7}>
|
||||
Maksimal 5MB dan harus format image
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Dokumen</Text>
|
||||
{previewImage ? (
|
||||
<iframe
|
||||
src={previewImage}
|
||||
width="100%"
|
||||
height="500px"
|
||||
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
|
||||
/>
|
||||
) : (
|
||||
<Text>Tidak ada image tersedia</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz={"md"} fw={"bold"}>File Dokumen</Text>
|
||||
<Box>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const selectedFile = files[0]; // Ambil file pertama
|
||||
if (selectedFile) {
|
||||
setDocFile(selectedFile);
|
||||
setPreviewDoc(URL.createObjectURL(selectedFile)); // Buat preview
|
||||
}
|
||||
}}
|
||||
onReject={() => toast.error('File tidak valid.')}
|
||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
||||
accept={{
|
||||
'application/*': ['.pdf', '.doc', '.docx'],
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
<IconFile size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
|
||||
<div>
|
||||
<Text size="xl" inline>
|
||||
Drag file ke sini atau klik untuk pilih file
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" inline mt={7}>
|
||||
Maksimal 5MB dan harus format dokumen
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Dokumen</Text>
|
||||
{previewDoc ? (
|
||||
<iframe
|
||||
src={previewDoc}
|
||||
width="100%"
|
||||
height="500px"
|
||||
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
|
||||
/>
|
||||
) : (
|
||||
<Text>Tidak ada dokumen tersedia</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<TextInput
|
||||
value={stateAPBDes.create.form.name}
|
||||
onChange={(val) => {
|
||||
stateAPBDes.create.form.name = val.target.value;
|
||||
}}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>}
|
||||
placeholder='Masukkan judul'
|
||||
/>
|
||||
<TextInput
|
||||
type='number'
|
||||
value={stateAPBDes.create.form.jumlah}
|
||||
onChange={(val) => {
|
||||
stateAPBDes.create.form.jumlah = val.target.value;
|
||||
}}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Jumlah</Text>}
|
||||
placeholder='Masukkan jumlah'
|
||||
/>
|
||||
<Group>
|
||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateAPBDes;
|
||||
@@ -1,11 +1,113 @@
|
||||
import React from 'react';
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { ActionIcon, Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
||||
import { IconDeviceImacCog, IconFile, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '../../_com/header';
|
||||
import JudulList from '../../_com/judulList';
|
||||
import apbdes from '../../_state/landing-page/apbdes';
|
||||
|
||||
function Page() {
|
||||
|
||||
function APBDes() {
|
||||
const [search, setSearch] = useState('');
|
||||
return (
|
||||
<div>
|
||||
APBDes
|
||||
</div>
|
||||
<Box>
|
||||
<HeaderSearch
|
||||
title='APBDes'
|
||||
placeholder='pencarian'
|
||||
searchIcon={<IconSearch size={20} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
<ListAPBDes search={search} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
function ListAPBDes({ search }: { search: string }) {
|
||||
const listState = useProxy(apbdes)
|
||||
const router = useRouter();
|
||||
useEffect(() => {
|
||||
listState.findMany.load()
|
||||
}, [])
|
||||
|
||||
const filteredData = (listState.findMany.data || []).filter(item => {
|
||||
const keyword = search.toLowerCase();
|
||||
return (
|
||||
item.name.toLowerCase().includes(keyword) ||
|
||||
item.jumlah.toLowerCase().includes(keyword)
|
||||
)
|
||||
});
|
||||
|
||||
if (!listState.findMany.data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton h={500} />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper bg={colors['white-1']} p={'md'}>
|
||||
<Stack>
|
||||
<JudulList
|
||||
title='List APBDes'
|
||||
href='/admin/landing-page/APBDes/create'
|
||||
/>
|
||||
<Box style={{ overflowX: 'auto' }}>
|
||||
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>Nama APBDes</TableTh>
|
||||
<TableTh>Jumlah APBDes</TableTh>
|
||||
<TableTh>Document</TableTh>
|
||||
<TableTh>Detail</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>
|
||||
<Box w={100}>
|
||||
<Text truncate="end" fz={"sm"}>{item.name}</Text>
|
||||
</Box>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text truncate="end" fz={"sm"}>{item.jumlah}</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
{item.file?.link ? (
|
||||
<ActionIcon
|
||||
component="a"
|
||||
href={item.file.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant='transparent'
|
||||
>
|
||||
<IconFile size={25} color={colors['blue-button']}/>
|
||||
</ActionIcon>
|
||||
) : (
|
||||
<Text>Tidak ada dokumen tersedia</Text>
|
||||
)}
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button onClick={() => router.push(`/admin/landing-page/APBDes/${item.id}`)}>
|
||||
<IconDeviceImacCog size={25} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default APBDes;
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
function LayoutTabs({ children }: { children: React.ReactNode }) {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const tabs = [
|
||||
{
|
||||
label: "List Desa Anti Korupsi",
|
||||
value: "listDesaAntiKorupsi",
|
||||
href: "/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi"
|
||||
},
|
||||
{
|
||||
label: "Kategori Desa Anti Korupsi",
|
||||
value: "kategoriDesaAntiKorupsi",
|
||||
href: "/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi"
|
||||
},
|
||||
];
|
||||
const curentTab = tabs.find(tab => tab.href === pathname)
|
||||
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
|
||||
|
||||
const handleTabChange = (value: string | null) => {
|
||||
const tab = tabs.find(t => t.value === value)
|
||||
if (tab) {
|
||||
router.push(tab.href)
|
||||
}
|
||||
setActiveTab(value)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const match = tabs.find(tab => tab.href === pathname)
|
||||
if (match) {
|
||||
setActiveTab(match.value)
|
||||
}
|
||||
}, [pathname])
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Title order={3}>Desa Anti Korupsi</Title>
|
||||
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
|
||||
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
|
||||
{tabs.map((e, i) => (
|
||||
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
|
||||
))}
|
||||
</TabsList>
|
||||
{tabs.map((e, i) => (
|
||||
<TabsPanel key={i} value={e.value}>
|
||||
{/* Konten dummy, bisa diganti tergantung routing */}
|
||||
<></>
|
||||
</TabsPanel>
|
||||
))}
|
||||
</Tabs>
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default LayoutTabs;
|
||||
@@ -0,0 +1,98 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
function EditKategoriDesaAntiKorupsi() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const id = params?.id as string;
|
||||
const stateKategori = useProxy(korupsiState.kategoriDesaAntiKorupsi);
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const loadKategorikegiatan = async () => {
|
||||
if (!id) return;
|
||||
|
||||
try {
|
||||
const data = await stateKategori.edit.load(id);
|
||||
|
||||
if (data) {
|
||||
// pastikan id-nya masuk ke state edit
|
||||
stateKategori.edit.id = id;
|
||||
setFormData({
|
||||
name: data.name || '',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading kategori desa anti korupsi:", error);
|
||||
toast.error("Gagal memuat data kategori desa anti korupsi");
|
||||
}
|
||||
};
|
||||
|
||||
loadKategorikegiatan();
|
||||
}, [id]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
if (!formData.name.trim()) {
|
||||
toast.error('Nama kategori desa anti korupsi tidak boleh kosong');
|
||||
return;
|
||||
}
|
||||
|
||||
stateKategori.edit.form = {
|
||||
name: formData.name.trim(),
|
||||
};
|
||||
|
||||
// Safety check tambahan: pastikan ID tidak kosong
|
||||
if (!stateKategori.edit.id) {
|
||||
stateKategori.edit.id = id; // fallback
|
||||
}
|
||||
|
||||
const success = await stateKategori.edit.update();
|
||||
|
||||
if (success) {
|
||||
router.push("/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating kategori desa anti korupsi:", error);
|
||||
// toast akan ditampilkan dari fungsi update
|
||||
}
|
||||
};
|
||||
|
||||
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 Kategori Desa Anti Korupsi</Title>
|
||||
<TextInput
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Desa Anti Korupsi</Text>}
|
||||
placeholder='Masukkan nama kategori desa anti korupsi'
|
||||
/>
|
||||
<Group>
|
||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditKategoriDesaAntiKorupsi;
|
||||
@@ -0,0 +1,61 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import korupsiState from '../../../../_state/landing-page/desa-anti-korupsi';
|
||||
|
||||
function CreateKategoriDesaAntiKorupsi() {
|
||||
const router = useRouter();
|
||||
const stateKategori = useProxy(korupsiState.kategoriDesaAntiKorupsi)
|
||||
|
||||
useEffect(() => {
|
||||
stateKategori.findMany.load();
|
||||
}, []);
|
||||
|
||||
const resetForm = () => {
|
||||
stateKategori.create.form = {
|
||||
name: "",
|
||||
};
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
await stateKategori.create.create();
|
||||
resetForm();
|
||||
router.push("/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi")
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<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 Kategori Desa Anti Korupsi</Title>
|
||||
<TextInput
|
||||
value={stateKategori.create.form.name}
|
||||
onChange={(val) => {
|
||||
stateKategori.create.form.name = val.target.value;
|
||||
}}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Desa Anti Korupsi</Text>}
|
||||
placeholder='Masukkan nama kategori desa anti korupsi'
|
||||
/>
|
||||
<Group>
|
||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateKategoriDesaAntiKorupsi;
|
||||
@@ -0,0 +1,112 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconEdit, IconSearch, IconX } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '../../../_com/header';
|
||||
import JudulList from '../../../_com/judulList';
|
||||
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||
import korupsiState from '../../../_state/landing-page/desa-anti-korupsi';
|
||||
|
||||
|
||||
function KategoriDesaAntiKorupsi() {
|
||||
const [search, setSearch] = useState("")
|
||||
return (
|
||||
<Box>
|
||||
<HeaderSearch
|
||||
title='Kategori Desa Anti Korupsi'
|
||||
placeholder='pencarian'
|
||||
searchIcon={<IconSearch size={20} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
<ListKategoriKegiatan search={search} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function ListKategoriKegiatan({ search }: { search: string }) {
|
||||
const stateKategori = useProxy(korupsiState.kategoriDesaAntiKorupsi)
|
||||
const [modalHapus, setModalHapus] = useState(false)
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||
const router = useRouter()
|
||||
|
||||
const handleHapus = () => {
|
||||
if (selectedId) {
|
||||
stateKategori.delete.byId(selectedId)
|
||||
setModalHapus(false)
|
||||
setSelectedId(null)
|
||||
}
|
||||
}
|
||||
|
||||
useShallowEffect(() => {
|
||||
stateKategori.findMany.load()
|
||||
}, [])
|
||||
|
||||
const filteredData = (stateKategori.findMany.data || []).filter(item => {
|
||||
const keyword = search.toLowerCase();
|
||||
return (
|
||||
item.name.toLowerCase().includes(keyword)
|
||||
);
|
||||
});
|
||||
|
||||
if (!stateKategori.findMany.data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton h={500} />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper bg={colors['white-1']} p={'md'}>
|
||||
<JudulList
|
||||
title='List Kategori Kegiatan'
|
||||
href='/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/create'
|
||||
/>
|
||||
<Table striped withTableBorder withRowBorders>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>Nama Kategori</TableTh>
|
||||
<TableTh>Edit</TableTh>
|
||||
<TableTh>Delete</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>{item.name}</TableTd>
|
||||
<TableTd>
|
||||
<Button color="green" onClick={() => router.push(`/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/${item.id}`)}>
|
||||
<IconEdit size={20} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button color="red" onClick={() => {
|
||||
setSelectedId(item.id)
|
||||
setModalHapus(true)
|
||||
}}>
|
||||
<IconX size={20} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Paper>
|
||||
{/* Modal Konfirmasi Hapus */}
|
||||
<ModalKonfirmasiHapus
|
||||
opened={modalHapus}
|
||||
onClose={() => setModalHapus(false)}
|
||||
onConfirm={handleHapus}
|
||||
text='Apakah anda yakin ingin menghapus kategori kegiatan ini?'
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default KategoriDesaAntiKorupsi
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import LayoutTabs from './_lib/layouTabs';
|
||||
|
||||
function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<LayoutTabs>
|
||||
{children}
|
||||
</LayoutTabs>
|
||||
);
|
||||
}
|
||||
|
||||
export default Layout;
|
||||
@@ -0,0 +1,229 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
import { Box, Button, Group, Paper, Select, Stack, Text, TextInput } from '@mantine/core';
|
||||
import { IconArrowBack, IconFile, IconUpload, IconX } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import colors from '@/con/colors';
|
||||
|
||||
import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi';
|
||||
import ApiFetch from '@/lib/api-fetch';
|
||||
import { Dropzone } from '@mantine/dropzone';
|
||||
import { toast } from 'react-toastify';
|
||||
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
||||
|
||||
interface FormDesaAntiKorupsi {
|
||||
name: string;
|
||||
deskripsi: string;
|
||||
kategoriId: string;
|
||||
fileId: string;
|
||||
}
|
||||
|
||||
function EditDesaAntiKorupsi() {
|
||||
const desaAntiKorupsiState = useProxy(korupsiState.desaAntikorupsi)
|
||||
const [previewFile, setPreviewFile] = useState<string | null>(null);
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const params = useParams()
|
||||
const router = useRouter()
|
||||
const [formData, setFormData] = useState<FormDesaAntiKorupsi>({
|
||||
name: '',
|
||||
deskripsi: '',
|
||||
kategoriId: '',
|
||||
fileId: '',
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const loadDesaAntiKorupsi = async () => {
|
||||
const id = params?.id as string;
|
||||
if (!id) return;
|
||||
|
||||
try {
|
||||
const data = await desaAntiKorupsiState.edit.load(id);
|
||||
if (data) {
|
||||
// ⬇️ FIX PENTING: tambahkan ini
|
||||
desaAntiKorupsiState.edit.id = id;
|
||||
|
||||
desaAntiKorupsiState.edit.form = {
|
||||
name: data.name,
|
||||
deskripsi: data.deskripsi,
|
||||
kategoriId: data.kategoriId,
|
||||
fileId: data.fileId,
|
||||
};
|
||||
|
||||
setFormData({
|
||||
name: data.name,
|
||||
deskripsi: data.deskripsi,
|
||||
kategoriId: data.kategoriId,
|
||||
fileId: data.fileId,
|
||||
});
|
||||
|
||||
if (data?.file?.link) {
|
||||
setPreviewFile(data.file.link)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading program penghijauan:", error);
|
||||
toast.error("Gagal memuat data program penghijauan");
|
||||
}
|
||||
}
|
||||
|
||||
loadDesaAntiKorupsi();
|
||||
}, [params?.id]);
|
||||
|
||||
|
||||
const handleSubmit = async () => {
|
||||
|
||||
try {
|
||||
// Update global state with form data
|
||||
desaAntiKorupsiState.edit.form = {
|
||||
...desaAntiKorupsiState.edit.form,
|
||||
name: formData.name,
|
||||
deskripsi: formData.deskripsi,
|
||||
kategoriId: formData.kategoriId || '',
|
||||
fileId: formData.fileId // Keep existing imageId if not changed
|
||||
};
|
||||
|
||||
// Jika ada file baru, upload
|
||||
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");
|
||||
}
|
||||
|
||||
// Update imageId in global state
|
||||
desaAntiKorupsiState.edit.form.fileId = uploaded.id;
|
||||
}
|
||||
|
||||
await desaAntiKorupsiState.edit.update();
|
||||
toast.success("desa anti korupsi berhasil diperbarui!");
|
||||
router.push("/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi");
|
||||
} catch (error) {
|
||||
console.error("Error updating desa anti korupsi:", error);
|
||||
toast.error("Terjadi kesalahan saat memperbarui desa anti korupsi");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<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"}>Edit List Desa Anti Korupsi</Text>
|
||||
{desaAntiKorupsiState.findUnique.data ? (
|
||||
<Paper key={desaAntiKorupsiState.findUnique.data.id}>
|
||||
<Stack gap={"xs"}>
|
||||
<TextInput
|
||||
value={formData.name}
|
||||
onChange={(val) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
name: val.target.value
|
||||
})
|
||||
}}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>}
|
||||
placeholder='Masukkan judul'
|
||||
/>
|
||||
<Box>
|
||||
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
|
||||
<EditEditor
|
||||
value={formData.deskripsi}
|
||||
onChange={(val) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
deskripsi: val
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Select
|
||||
value={formData.kategoriId}
|
||||
onChange={(val) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
kategoriId: val ?? ""
|
||||
})
|
||||
}}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>}
|
||||
placeholder="Pilih kategori"
|
||||
data={
|
||||
korupsiState.kategoriDesaAntiKorupsi.findMany.data?.map((v) => ({
|
||||
value: v.id,
|
||||
label: v.name,
|
||||
})) || []
|
||||
}
|
||||
/>
|
||||
<Box>
|
||||
<Text fz={"md"} fw={"bold"}>File Document</Text>
|
||||
<Box>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const selectedFile = files[0]; // Ambil file pertama
|
||||
if (selectedFile) {
|
||||
setFile(selectedFile);
|
||||
setPreviewFile(URL.createObjectURL(selectedFile)); // Buat preview
|
||||
}
|
||||
}}
|
||||
onReject={() => toast.error('File tidak valid.')}
|
||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
||||
accept={{
|
||||
'application/*': ['.pdf', '.doc', '.docx'],
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
<IconFile size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
|
||||
<div>
|
||||
<Text size="xl" inline>
|
||||
Drag file ke sini atau klik untuk pilih file
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" inline mt={7}>
|
||||
Maksimal 5MB dan harus format document
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Dokumen</Text>
|
||||
{previewFile ? (
|
||||
<iframe
|
||||
src={previewFile}
|
||||
width="100%"
|
||||
height="500px"
|
||||
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
|
||||
/>
|
||||
) : (
|
||||
<Text>Tidak ada dokumen tersedia</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Group>
|
||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
) : null}
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditDesaAntiKorupsi;
|
||||
@@ -0,0 +1,122 @@
|
||||
'use client'
|
||||
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||
import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
|
||||
function DetailKegiatanDesa() {
|
||||
const detailState = useProxy(korupsiState.desaAntikorupsi)
|
||||
const [modalHapus, setModalHapus] = useState(false)
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||
const params = useParams()
|
||||
const router = useRouter()
|
||||
|
||||
useShallowEffect(() => {
|
||||
detailState.findUnique.load(params?.id as string)
|
||||
}, [])
|
||||
|
||||
|
||||
const handleHapus = () => {
|
||||
if (selectedId) {
|
||||
detailState.delete.byId(selectedId)
|
||||
setModalHapus(false)
|
||||
setSelectedId(null)
|
||||
router.push("/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi")
|
||||
}
|
||||
}
|
||||
|
||||
if (!detailState.findUnique.data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton h={40} />
|
||||
</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 List Desa Anti Korupsi</Text>
|
||||
{detailState.findUnique.data ? (
|
||||
<Paper key={detailState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
||||
<Stack gap={"xs"}>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Judul</Text>
|
||||
<Text fz={"lg"}>{detailState.findUnique.data?.name}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
|
||||
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: detailState.findUnique.data?.deskripsi }} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Kategori</Text>
|
||||
<Text fz={"lg"}>{detailState.findUnique.data?.kategori?.name}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Dokumen</Text>
|
||||
{detailState.findUnique.data?.file?.link ? (
|
||||
<iframe
|
||||
src={detailState.findUnique.data.file.link}
|
||||
width="100%"
|
||||
height="500px"
|
||||
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
|
||||
/>
|
||||
) : (
|
||||
<Text>Tidak ada dokumen tersedia</Text>
|
||||
)}
|
||||
</Box>
|
||||
<Flex gap={"xs"} mt={10}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (detailState.findUnique.data) {
|
||||
setSelectedId(detailState.findUnique.data.id);
|
||||
setModalHapus(true);
|
||||
}
|
||||
}}
|
||||
disabled={detailState.delete.loading || !detailState.findUnique.data}
|
||||
color={"red"}
|
||||
>
|
||||
<IconX size={20} />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (detailState.findUnique.data) {
|
||||
router.push(`/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/${detailState.findUnique.data.id}/edit`);
|
||||
}
|
||||
}}
|
||||
disabled={!detailState.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 desa anti korupsi ini?'
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default DetailKegiatanDesa;
|
||||
@@ -0,0 +1,166 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
||||
import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi';
|
||||
import colors from '@/con/colors';
|
||||
import ApiFetch from '@/lib/api-fetch';
|
||||
import { Box, Button, Group, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import { Dropzone } from '@mantine/dropzone';
|
||||
import { IconArrowBack, IconFile, IconUpload, IconX } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
|
||||
function CreateDesaAntiKorupsi() {
|
||||
const router = useRouter();
|
||||
const stateKorupsi = useProxy(korupsiState.desaAntikorupsi)
|
||||
const [previewFile, setPreviewFile] = useState<string | null>(null);
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
stateKorupsi.findMany.load();
|
||||
korupsiState.kategoriDesaAntiKorupsi.findMany.load();
|
||||
}, []);
|
||||
|
||||
const resetForm = () => {
|
||||
stateKorupsi.create.form = {
|
||||
name: "",
|
||||
deskripsi: "",
|
||||
kategoriId: "",
|
||||
fileId: "",
|
||||
};
|
||||
setFile(null);
|
||||
setPreviewFile(null);
|
||||
};
|
||||
const handleSubmit = async () => {
|
||||
if (!file) {
|
||||
return toast.warn("Pilih file pdf 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 mengupload file");
|
||||
}
|
||||
|
||||
stateKorupsi.create.form.fileId = uploaded.id;
|
||||
|
||||
await stateKorupsi.create.create();
|
||||
|
||||
resetForm();
|
||||
router.push("/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi")
|
||||
}
|
||||
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 Kegiatan Desa</Title>
|
||||
<Box>
|
||||
<Text fz={"md"} fw={"bold"}>File Document</Text>
|
||||
<Box>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const selectedFile = files[0]; // Ambil file pertama
|
||||
if (selectedFile) {
|
||||
setFile(selectedFile);
|
||||
setPreviewFile(URL.createObjectURL(selectedFile)); // Buat preview
|
||||
}
|
||||
}}
|
||||
onReject={() => toast.error('File tidak valid.')}
|
||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
||||
accept={{
|
||||
'application/*': ['.pdf', '.doc', '.docx'],
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
<IconFile size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
|
||||
<div>
|
||||
<Text size="xl" inline>
|
||||
Drag file ke sini atau klik untuk pilih file
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" inline mt={7}>
|
||||
Maksimal 5MB dan harus format document
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Dokumen</Text>
|
||||
{previewFile ? (
|
||||
<iframe
|
||||
src={previewFile}
|
||||
width="100%"
|
||||
height="500px"
|
||||
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
|
||||
/>
|
||||
) : (
|
||||
<Text>Tidak ada dokumen tersedia</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<TextInput
|
||||
value={stateKorupsi.create.form.name}
|
||||
onChange={(val) => {
|
||||
stateKorupsi.create.form.name = val.target.value;
|
||||
}}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>}
|
||||
placeholder='Masukkan judul'
|
||||
/>
|
||||
<Box>
|
||||
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
|
||||
<CreateEditor
|
||||
value={stateKorupsi.create.form.deskripsi}
|
||||
onChange={(val) => {
|
||||
stateKorupsi.create.form.deskripsi = val;
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Select
|
||||
value={stateKorupsi.create.form.kategoriId}
|
||||
onChange={(val) => {
|
||||
stateKorupsi.create.form.kategoriId = val ?? "";
|
||||
}}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>}
|
||||
placeholder="Pilih kategori"
|
||||
data={
|
||||
korupsiState.kategoriDesaAntiKorupsi.findMany.data?.map((v) => ({
|
||||
value: v.id,
|
||||
label: v.name,
|
||||
})) || []
|
||||
}
|
||||
/>
|
||||
|
||||
<Group>
|
||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateDesaAntiKorupsi;
|
||||
@@ -0,0 +1,99 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
||||
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '../../../_com/header';
|
||||
import JudulList from '../../../_com/judulList';
|
||||
import korupsiState from '../../../_state/landing-page/desa-anti-korupsi';
|
||||
|
||||
function DesaAntiKorupsi() {
|
||||
const [search, setSearch] = useState('');
|
||||
return (
|
||||
<Box>
|
||||
<HeaderSearch
|
||||
title='List Desa Anti Korupsi'
|
||||
placeholder='pencarian'
|
||||
searchIcon={<IconSearch size={20} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
<ListDesaAntiKorupsi search={search} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function ListDesaAntiKorupsi({ search }: { search: string }) {
|
||||
const listState = useProxy(korupsiState.desaAntikorupsi)
|
||||
const router = useRouter();
|
||||
useEffect(() => {
|
||||
listState.findMany.load()
|
||||
}, [])
|
||||
|
||||
const filteredData = (listState.findMany.data || []).filter(item => {
|
||||
const keyword = search.toLowerCase();
|
||||
return (
|
||||
item.name.toLowerCase().includes(keyword) ||
|
||||
item.deskripsi.toLowerCase().includes(keyword) ||
|
||||
item.kategori?.name?.toLowerCase().includes(keyword)
|
||||
);
|
||||
});
|
||||
|
||||
if (!listState.findMany.data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton h={500} />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper bg={colors['white-1']} p={'md'}>
|
||||
<Stack>
|
||||
<JudulList
|
||||
title='List Desa Anti Korupsi'
|
||||
href='/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/create'
|
||||
/>
|
||||
<Box style={{ overflowX: 'auto' }}>
|
||||
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>Nama Desa Anti Korupsi</TableTh>
|
||||
<TableTh>Deskripsi Desa Anti Korupsi</TableTh>
|
||||
<TableTh>Kategori Desa Anti Korupsi</TableTh>
|
||||
<TableTh>Detail</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>
|
||||
<Box w={100}>
|
||||
<Text truncate="end" fz={"sm"}>{item.name}</Text>
|
||||
</Box>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||
</TableTd>
|
||||
<TableTd>{item.kategori?.name}</TableTd>
|
||||
<TableTd>
|
||||
<Button onClick={() => router.push(`/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/${item.id}`)}>
|
||||
<IconDeviceImacCog size={25} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default DesaAntiKorupsi;
|
||||
@@ -1,11 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
function Page() {
|
||||
return (
|
||||
<div>
|
||||
Desa Anti Korupsi
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
@@ -0,0 +1,62 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
function LayoutTabs({ children }: { children: React.ReactNode }) {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const tabs = [
|
||||
{
|
||||
label: "List Prestasi Desa",
|
||||
value: "listPrestasiDesa",
|
||||
href: "/admin/landing-page/prestasi-desa/list-prestasi-desa"
|
||||
},
|
||||
{
|
||||
label: "Kategori Prestasi Desa",
|
||||
value: "kategoriPrestasiDesa",
|
||||
href: "/admin/landing-page/prestasi-desa/kategori-prestasi-desa"
|
||||
},
|
||||
];
|
||||
const curentTab = tabs.find(tab => tab.href === pathname)
|
||||
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
|
||||
|
||||
const handleTabChange = (value: string | null) => {
|
||||
const tab = tabs.find(t => t.value === value)
|
||||
if (tab) {
|
||||
router.push(tab.href)
|
||||
}
|
||||
setActiveTab(value)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const match = tabs.find(tab => tab.href === pathname)
|
||||
if (match) {
|
||||
setActiveTab(match.value)
|
||||
}
|
||||
}, [pathname])
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Title order={3}>Prestasi Desa</Title>
|
||||
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
|
||||
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
|
||||
{tabs.map((e, i) => (
|
||||
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
|
||||
))}
|
||||
</TabsList>
|
||||
{tabs.map((e, i) => (
|
||||
<TabsPanel key={i} value={e.value}>
|
||||
{/* Konten dummy, bisa diganti tergantung routing */}
|
||||
<></>
|
||||
</TabsPanel>
|
||||
))}
|
||||
</Tabs>
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default LayoutTabs;
|
||||
@@ -0,0 +1,98 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import prestasiState from '@/app/admin/(dashboard)/_state/landing-page/prestasi-desa';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
function EditKategoriPrestasi() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const id = params?.id as string;
|
||||
const stateKategori = useProxy(prestasiState.kategoriPrestasi);
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const loadKategoriprestasi = async () => {
|
||||
if (!id) return;
|
||||
|
||||
try {
|
||||
const data = await stateKategori.edit.load(id);
|
||||
|
||||
if (data) {
|
||||
// pastikan id-nya masuk ke state edit
|
||||
stateKategori.edit.id = id;
|
||||
setFormData({
|
||||
name: data.name || '',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading kategori prestasi desa:", error);
|
||||
toast.error("Gagal memuat data kategori prestasi desa");
|
||||
}
|
||||
};
|
||||
|
||||
loadKategoriprestasi();
|
||||
}, [id]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
if (!formData.name.trim()) {
|
||||
toast.error('Nama kategori prestasi desa tidak boleh kosong');
|
||||
return;
|
||||
}
|
||||
|
||||
stateKategori.edit.form = {
|
||||
name: formData.name.trim(),
|
||||
};
|
||||
|
||||
// Safety check tambahan: pastikan ID tidak kosong
|
||||
if (!stateKategori.edit.id) {
|
||||
stateKategori.edit.id = id; // fallback
|
||||
}
|
||||
|
||||
const success = await stateKategori.edit.update();
|
||||
|
||||
if (success) {
|
||||
router.push("/admin/landing-page/prestasi-desa/kategori-prestasi-desa");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating kategori prestasi desa:", error);
|
||||
// toast akan ditampilkan dari fungsi update
|
||||
}
|
||||
};
|
||||
|
||||
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 Kategori Prestasi Desa</Title>
|
||||
<TextInput
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Prestasi Desa</Text>}
|
||||
placeholder='Masukkan nama kategori prestasi desa'
|
||||
/>
|
||||
<Group>
|
||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditKategoriPrestasi;
|
||||
@@ -0,0 +1,62 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import prestasiState from '@/app/admin/(dashboard)/_state/landing-page/prestasi-desa';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
|
||||
function CreateKategoriPrestasi() {
|
||||
const router = useRouter();
|
||||
const stateKategori = useProxy(prestasiState.kategoriPrestasi)
|
||||
|
||||
useEffect(() => {
|
||||
stateKategori.findMany.load();
|
||||
}, []);
|
||||
|
||||
const resetForm = () => {
|
||||
stateKategori.create.form = {
|
||||
name: "",
|
||||
};
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
await stateKategori.create.create();
|
||||
resetForm();
|
||||
router.push("/admin/landing-page/prestasi-desa/kategori-prestasi-desa")
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<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 Kategori Prestasi Desa</Title>
|
||||
<TextInput
|
||||
value={stateKategori.create.form.name}
|
||||
onChange={(val) => {
|
||||
stateKategori.create.form.name = val.target.value;
|
||||
}}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Prestasi Desa</Text>}
|
||||
placeholder='Masukkan nama kategori prestasi desa'
|
||||
/>
|
||||
<Group>
|
||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateKategoriPrestasi;
|
||||
@@ -0,0 +1,112 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconEdit, IconSearch, IconX } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '../../../_com/header';
|
||||
import JudulList from '../../../_com/judulList';
|
||||
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||
import prestasiState from '../../../_state/landing-page/prestasi-desa';
|
||||
|
||||
|
||||
function KategoriPrestasiDesa() {
|
||||
const [search, setSearch] = useState("")
|
||||
return (
|
||||
<Box>
|
||||
<HeaderSearch
|
||||
title='Kategori Prestasi Desa'
|
||||
placeholder='pencarian'
|
||||
searchIcon={<IconSearch size={20} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
<ListKategoriPrestasi search={search} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function ListKategoriPrestasi({ search }: { search: string }) {
|
||||
const stateKategori = useProxy(prestasiState.kategoriPrestasi)
|
||||
const [modalHapus, setModalHapus] = useState(false)
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||
const router = useRouter()
|
||||
|
||||
const handleHapus = () => {
|
||||
if (selectedId) {
|
||||
stateKategori.delete.byId(selectedId)
|
||||
setModalHapus(false)
|
||||
setSelectedId(null)
|
||||
}
|
||||
}
|
||||
|
||||
useShallowEffect(() => {
|
||||
stateKategori.findMany.load()
|
||||
}, [])
|
||||
|
||||
const filteredData = (stateKategori.findMany.data || []).filter(item => {
|
||||
const keyword = search.toLowerCase();
|
||||
return (
|
||||
item.name.toLowerCase().includes(keyword)
|
||||
);
|
||||
});
|
||||
|
||||
if (!stateKategori.findMany.data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton h={500} />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper bg={colors['white-1']} p={'md'}>
|
||||
<JudulList
|
||||
title='List Kategori Prestasi Desa'
|
||||
href='/admin/landing-page/prestasi-desa/kategori-prestasi-desa/create'
|
||||
/>
|
||||
<Table striped withTableBorder withRowBorders>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>Nama Kategori</TableTh>
|
||||
<TableTh>Edit</TableTh>
|
||||
<TableTh>Delete</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>{item.name}</TableTd>
|
||||
<TableTd>
|
||||
<Button color="green" onClick={() => router.push(`/admin/landing-page/prestasi-desa/kategori-prestasi-desa/${item.id}`)}>
|
||||
<IconEdit size={20} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button color="red" onClick={() => {
|
||||
setSelectedId(item.id)
|
||||
setModalHapus(true)
|
||||
}}>
|
||||
<IconX size={20} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Paper>
|
||||
{/* Modal Konfirmasi Hapus */}
|
||||
<ModalKonfirmasiHapus
|
||||
opened={modalHapus}
|
||||
onClose={() => setModalHapus(false)}
|
||||
onConfirm={handleHapus}
|
||||
text='Apakah anda yakin ingin menghapus kategori prestasi desa ini?'
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default KategoriPrestasiDesa
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import LayoutTabs from './_lib/layoutTabs';
|
||||
|
||||
function Layout({children} : {children: React.ReactNode}) {
|
||||
return (
|
||||
<LayoutTabs>
|
||||
{children}
|
||||
</LayoutTabs>
|
||||
);
|
||||
}
|
||||
|
||||
export default Layout;
|
||||
@@ -0,0 +1,230 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
import { Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput } from '@mantine/core';
|
||||
import { IconArrowBack, IconFile, IconUpload, IconX } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import colors from '@/con/colors';
|
||||
|
||||
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
||||
import prestasiState from '@/app/admin/(dashboard)/_state/landing-page/prestasi-desa';
|
||||
import ApiFetch from '@/lib/api-fetch';
|
||||
import { Dropzone } from '@mantine/dropzone';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
interface FormPrestasiDesa {
|
||||
name: string;
|
||||
deskripsi: string;
|
||||
kategoriId: string;
|
||||
imageId: string;
|
||||
}
|
||||
|
||||
function EditPrestasiDesa() {
|
||||
const editState = useProxy(prestasiState.prestasiDesa)
|
||||
const [previewFile, setPreviewFile] = useState<string | null>(null);
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const params = useParams()
|
||||
const router = useRouter()
|
||||
const [formData, setFormData] = useState<FormPrestasiDesa>({
|
||||
name: '',
|
||||
deskripsi: '',
|
||||
kategoriId: '',
|
||||
imageId: '',
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const loadDesaAntiKorupsi = async () => {
|
||||
const id = params?.id as string;
|
||||
if (!id) return;
|
||||
|
||||
try {
|
||||
const data = await editState.edit.load(id);
|
||||
if (data) {
|
||||
// ⬇️ FIX PENTING: tambahkan ini
|
||||
editState.edit.id = id;
|
||||
|
||||
editState.edit.form = {
|
||||
name: data.name,
|
||||
deskripsi: data.deskripsi,
|
||||
kategoriId: data.kategoriId,
|
||||
imageId: data.imageId,
|
||||
};
|
||||
|
||||
setFormData({
|
||||
name: data.name,
|
||||
deskripsi: data.deskripsi,
|
||||
kategoriId: data.kategoriId,
|
||||
imageId: data.imageId,
|
||||
});
|
||||
|
||||
if (data?.image?.link) {
|
||||
setPreviewFile(data.image.link)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading prestasi desa:", error);
|
||||
toast.error("Gagal memuat data prestasi desa");
|
||||
}
|
||||
}
|
||||
|
||||
loadDesaAntiKorupsi();
|
||||
}, [params?.id]);
|
||||
|
||||
|
||||
const handleSubmit = async () => {
|
||||
|
||||
try {
|
||||
// Update global state with form data
|
||||
editState.edit.form = {
|
||||
...editState.edit.form,
|
||||
name: formData.name,
|
||||
deskripsi: formData.deskripsi,
|
||||
kategoriId: formData.kategoriId || '',
|
||||
imageId: formData.imageId // Keep existing imageId if not changed
|
||||
};
|
||||
|
||||
// Jika ada file baru, upload
|
||||
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");
|
||||
}
|
||||
|
||||
// Update imageId in global state
|
||||
editState.edit.form.imageId = uploaded.id;
|
||||
}
|
||||
|
||||
await editState.edit.update();
|
||||
toast.success("prestasi desa berhasil diperbarui!");
|
||||
router.push("/admin/landing-page/prestasi-desa/list-prestasi-desa");
|
||||
} catch (error) {
|
||||
console.error("Error updating prestasi desa:", error);
|
||||
toast.error("Terjadi kesalahan saat memperbarui prestasi desa");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<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"}>Edit List Prestasi Desa</Text>
|
||||
{editState.findUnique.data ? (
|
||||
<Paper key={editState.findUnique.data.id}>
|
||||
<Stack gap={"xs"}>
|
||||
<TextInput
|
||||
value={formData.name}
|
||||
onChange={(val) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
name: val.target.value
|
||||
})
|
||||
}}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>}
|
||||
placeholder='Masukkan judul'
|
||||
/>
|
||||
<Box>
|
||||
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
|
||||
<EditEditor
|
||||
value={formData.deskripsi}
|
||||
onChange={(val) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
deskripsi: val
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Select
|
||||
value={formData.kategoriId}
|
||||
onChange={(val) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
kategoriId: val ?? ""
|
||||
})
|
||||
}}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>}
|
||||
placeholder="Pilih kategori"
|
||||
data={
|
||||
prestasiState.kategoriPrestasi.findMany.data?.map((v) => ({
|
||||
value: v.id,
|
||||
label: v.name,
|
||||
})) || []
|
||||
}
|
||||
/>
|
||||
<Box>
|
||||
<Text fz={"md"} fw={"bold"}>File Image</Text>
|
||||
<Stack gap={"xs"}>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const selectedFile = files[0]; // Ambil file pertama
|
||||
if (selectedFile) {
|
||||
setFile(selectedFile);
|
||||
setPreviewFile(URL.createObjectURL(selectedFile)); // Buat preview
|
||||
}
|
||||
}}
|
||||
onReject={() => toast.error('File tidak valid.')}
|
||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
||||
accept={{
|
||||
'image/*': ['.png', '.jpg', '.jpeg', '.gif', '.webp'],
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
<IconFile size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
|
||||
<div>
|
||||
<Text size="xl" inline>
|
||||
Drag file ke sini atau klik untuk pilih file
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" inline mt={7}>
|
||||
Maksimal 5MB dan harus format image
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Image</Text>
|
||||
{previewFile ? (
|
||||
<Image
|
||||
alt=''
|
||||
src={previewFile}
|
||||
width="100%"
|
||||
height="500px"
|
||||
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
|
||||
/>
|
||||
) : (
|
||||
<Text>Tidak ada image tersedia</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Group>
|
||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
) : null}
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditPrestasiDesa;
|
||||
@@ -0,0 +1,122 @@
|
||||
'use client'
|
||||
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||
import prestasiState from '@/app/admin/(dashboard)/_state/landing-page/prestasi-desa';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
|
||||
function DetailPrestasiDesa() {
|
||||
const detailState = useProxy(prestasiState.prestasiDesa)
|
||||
const [modalHapus, setModalHapus] = useState(false)
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||
const params = useParams()
|
||||
const router = useRouter()
|
||||
|
||||
useShallowEffect(() => {
|
||||
detailState.findUnique.load(params?.id as string)
|
||||
}, [])
|
||||
|
||||
|
||||
const handleHapus = () => {
|
||||
if (selectedId) {
|
||||
detailState.delete.byId(selectedId)
|
||||
setModalHapus(false)
|
||||
setSelectedId(null)
|
||||
router.push("/admin/landing-page/prestasi-desa/list-prestasi-desa")
|
||||
}
|
||||
}
|
||||
|
||||
if (!detailState.findUnique.data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton h={40} />
|
||||
</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 List Prestasi Desa</Text>
|
||||
{detailState.findUnique.data ? (
|
||||
<Paper key={detailState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
||||
<Stack gap={"xs"}>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Judul</Text>
|
||||
<Text fz={"lg"}>{detailState.findUnique.data?.name}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
|
||||
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: detailState.findUnique.data?.deskripsi }} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Kategori</Text>
|
||||
<Text fz={"lg"}>{detailState.findUnique.data?.kategori?.name}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Image</Text>
|
||||
{detailState.findUnique.data?.image?.link ? (
|
||||
<iframe
|
||||
src={detailState.findUnique.data.image.link}
|
||||
width="100%"
|
||||
height="500px"
|
||||
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
|
||||
/>
|
||||
) : (
|
||||
<Text>Tidak ada image tersedia</Text>
|
||||
)}
|
||||
</Box>
|
||||
<Flex gap={"xs"} mt={10}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (detailState.findUnique.data) {
|
||||
setSelectedId(detailState.findUnique.data.id);
|
||||
setModalHapus(true);
|
||||
}
|
||||
}}
|
||||
disabled={detailState.delete.loading || !detailState.findUnique.data}
|
||||
color={"red"}
|
||||
>
|
||||
<IconX size={20} />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (detailState.findUnique.data) {
|
||||
router.push(`/admin/landing-page/prestasi-desa/list-prestasi-desa/${detailState.findUnique.data.id}/edit`);
|
||||
}
|
||||
}}
|
||||
disabled={!detailState.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 prestasi desa ini?'
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default DetailPrestasiDesa;
|
||||
@@ -0,0 +1,167 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
||||
import prestasiState from '@/app/admin/(dashboard)/_state/landing-page/prestasi-desa';
|
||||
|
||||
import colors from '@/con/colors';
|
||||
import ApiFetch from '@/lib/api-fetch';
|
||||
import { Box, Button, Group, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import { Dropzone } from '@mantine/dropzone';
|
||||
import { IconArrowBack, IconImageInPicture, IconUpload, IconX } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
|
||||
function CreatePrestasiDesa() {
|
||||
const router = useRouter();
|
||||
const stateCreate = useProxy(prestasiState.prestasiDesa)
|
||||
const [previewFile, setPreviewFile] = useState<string | null>(null);
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
stateCreate.findMany.load();
|
||||
prestasiState.kategoriPrestasi.findMany.load();
|
||||
}, []);
|
||||
|
||||
const resetForm = () => {
|
||||
stateCreate.create.form = {
|
||||
name: "",
|
||||
deskripsi: "",
|
||||
kategoriId: "",
|
||||
imageId: "",
|
||||
};
|
||||
setFile(null);
|
||||
setPreviewFile(null);
|
||||
};
|
||||
const handleSubmit = async () => {
|
||||
if (!file) {
|
||||
return toast.warn("Pilih file image 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 mengupload image");
|
||||
}
|
||||
|
||||
stateCreate.create.form.imageId = uploaded.id;
|
||||
|
||||
await stateCreate.create.create();
|
||||
|
||||
resetForm();
|
||||
router.push("/admin/landing-page/prestasi-desa/list-prestasi-desa")
|
||||
}
|
||||
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 Prestasi Desa</Title>
|
||||
<Box>
|
||||
<Text fz={"md"} fw={"bold"}>File Image</Text>
|
||||
<Box>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const selectedFile = files[0]; // Ambil file pertama
|
||||
if (selectedFile) {
|
||||
setFile(selectedFile);
|
||||
setPreviewFile(URL.createObjectURL(selectedFile)); // Buat preview
|
||||
}
|
||||
}}
|
||||
onReject={() => toast.error('File tidak valid.')}
|
||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
||||
accept={{
|
||||
'application/*': ['.jpg', '.jpeg', '.png'],
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
<IconImageInPicture size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
|
||||
<div>
|
||||
<Text size="xl" inline>
|
||||
Drag file ke sini atau klik untuk pilih file
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" inline mt={7}>
|
||||
Maksimal 5MB dan harus format image
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Image</Text>
|
||||
{previewFile ? (
|
||||
<iframe
|
||||
src={previewFile}
|
||||
width="100%"
|
||||
height="500px"
|
||||
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
|
||||
/>
|
||||
) : (
|
||||
<Text>Tidak ada image tersedia</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<TextInput
|
||||
value={stateCreate.create.form.name}
|
||||
onChange={(val) => {
|
||||
stateCreate.create.form.name = val.target.value;
|
||||
}}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>}
|
||||
placeholder='Masukkan judul'
|
||||
/>
|
||||
<Box>
|
||||
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
|
||||
<CreateEditor
|
||||
value={stateCreate.create.form.deskripsi}
|
||||
onChange={(val) => {
|
||||
stateCreate.create.form.deskripsi = val;
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Select
|
||||
value={stateCreate.create.form.kategoriId}
|
||||
onChange={(val) => {
|
||||
stateCreate.create.form.kategoriId = val ?? "";
|
||||
}}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>}
|
||||
placeholder="Pilih kategori"
|
||||
data={
|
||||
prestasiState.kategoriPrestasi.findMany.data?.map((v) => ({
|
||||
value: v.id,
|
||||
label: v.name,
|
||||
})) || []
|
||||
}
|
||||
/>
|
||||
|
||||
<Group>
|
||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreatePrestasiDesa;
|
||||
@@ -0,0 +1,100 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
||||
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '../../../_com/header';
|
||||
import JudulList from '../../../_com/judulList';
|
||||
import prestasiState from '../../../_state/landing-page/prestasi-desa';
|
||||
|
||||
|
||||
function ListPrestasiDesa() {
|
||||
const [search, setSearch] = useState('');
|
||||
return (
|
||||
<Box>
|
||||
<HeaderSearch
|
||||
title='List Prestasi Desa'
|
||||
placeholder='pencarian'
|
||||
searchIcon={<IconSearch size={20} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
<ListPrestasi search={search} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function ListPrestasi({ search }: { search: string }) {
|
||||
const listState = useProxy(prestasiState.prestasiDesa)
|
||||
const router = useRouter();
|
||||
useEffect(() => {
|
||||
listState.findMany.load()
|
||||
}, [])
|
||||
|
||||
const filteredData = (listState.findMany.data || []).filter(item => {
|
||||
const keyword = search.toLowerCase();
|
||||
return (
|
||||
item.name.toLowerCase().includes(keyword) ||
|
||||
item.deskripsi.toLowerCase().includes(keyword) ||
|
||||
item.kategori?.name?.toLowerCase().includes(keyword)
|
||||
);
|
||||
});
|
||||
|
||||
if (!listState.findMany.data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton h={500} />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper bg={colors['white-1']} p={'md'}>
|
||||
<Stack>
|
||||
<JudulList
|
||||
title='List Prestasi Desa'
|
||||
href='/admin/landing-page/prestasi-desa/list-prestasi-desa/create'
|
||||
/>
|
||||
<Box style={{ overflowX: 'auto' }}>
|
||||
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>Nama Prestasi Desa</TableTh>
|
||||
<TableTh>Deskripsi Prestasi Desa</TableTh>
|
||||
<TableTh>Kategori Prestasi Desa</TableTh>
|
||||
<TableTh>Detail</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>
|
||||
<Box w={100}>
|
||||
<Text truncate="end" fz={"sm"}>{item.name}</Text>
|
||||
</Box>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||
</TableTd>
|
||||
<TableTd>{item.kategori?.name}</TableTd>
|
||||
<TableTd>
|
||||
<Button onClick={() => router.push(`/admin/landing-page/prestasi-desa/list-prestasi-desa/${item.id}`)}>
|
||||
<IconDeviceImacCog size={25} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default ListPrestasiDesa;
|
||||
@@ -1,11 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
function Page() {
|
||||
return (
|
||||
<div>
|
||||
Prestasi Desa
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
@@ -1,11 +1,174 @@
|
||||
import React from 'react';
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
|
||||
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 { useParams, useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
function EditMediaSosial() {
|
||||
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial)
|
||||
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: stateMediaSosial.update.form.name || "",
|
||||
iconUrl: stateMediaSosial.update.form.iconUrl || "",
|
||||
imageId: stateMediaSosial.update.form.imageId || ""
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const id = params?.id as string;
|
||||
if (!id) return;
|
||||
|
||||
const loadMediaSosial = async () => {
|
||||
try {
|
||||
const data = await stateMediaSosial.update.load(id);
|
||||
|
||||
if (data) {
|
||||
setFormData({
|
||||
name: data.name || "",
|
||||
iconUrl: data.iconUrl || "",
|
||||
imageId: data.imageId || "",
|
||||
});
|
||||
// Tampilkan preview gambar
|
||||
if (data.image?.link) {
|
||||
setPreviewImage(data.image.link);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading program inovasi:", error);
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "Gagal mengambil data program inovasi"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
loadMediaSosial();
|
||||
}, [params?.id]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
stateMediaSosial.update.form = {
|
||||
...stateMediaSosial.update.form,
|
||||
name: formData.name,
|
||||
iconUrl: formData.iconUrl,
|
||||
imageId: formData.imageId ?? "",
|
||||
}
|
||||
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");
|
||||
}
|
||||
|
||||
// Update imageId in global state
|
||||
stateMediaSosial.update.form.imageId = uploaded.id;
|
||||
}
|
||||
|
||||
await stateMediaSosial.update.update();
|
||||
toast.success("Media Sosial berhasil diperbarui!");
|
||||
router.push("/admin/landing-page/profile/media-sosial");
|
||||
} catch (error) {
|
||||
console.error("Error updating media sosial:", error);
|
||||
toast.error("Terjadi kesalahan saat memperbarui media sosial");
|
||||
}
|
||||
};
|
||||
|
||||
function Page() {
|
||||
return (
|
||||
<div>
|
||||
Page
|
||||
</div>
|
||||
<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 Media Sosial</Title>
|
||||
<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>
|
||||
<TextInput
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Nama Media Sosial / Nama Kontak</Text>}
|
||||
placeholder='Masukkan nama media sosial'
|
||||
/>
|
||||
<TextInput
|
||||
value={formData.iconUrl}
|
||||
onChange={(e) => setFormData({ ...formData, iconUrl: e.target.value })}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Icon URL / No Telephone</Text>}
|
||||
placeholder='Masukkan icon url'
|
||||
/>
|
||||
<Group>
|
||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
export default EditMediaSosial;
|
||||
|
||||
@@ -1,11 +1,107 @@
|
||||
import React from 'react';
|
||||
'use client'
|
||||
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
function DetailMediaSosial() {
|
||||
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial)
|
||||
const [modalHapus, setModalHapus] = useState(false)
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||
const params = useParams()
|
||||
const router = useRouter();
|
||||
|
||||
useShallowEffect(() => {
|
||||
stateMediaSosial.findUnique.load(params?.id as string)
|
||||
}, [])
|
||||
|
||||
const handleHapus = () => {
|
||||
if (selectedId) {
|
||||
stateMediaSosial.delete.byId(selectedId)
|
||||
setModalHapus(false)
|
||||
setSelectedId(null)
|
||||
router.push("/admin/landing-page/profile/media-sosial")
|
||||
}
|
||||
}
|
||||
|
||||
if (!stateMediaSosial.findUnique.data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton h={500} />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
function Page() {
|
||||
return (
|
||||
<div>
|
||||
Page
|
||||
</div>
|
||||
<Box>
|
||||
<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 Media Sosial</Text>
|
||||
<Paper bg={colors['BG-trans']} p={'md'}>
|
||||
<Stack gap={"xs"}>
|
||||
<Box>
|
||||
<Text fz={"lg"} fw={"bold"}>Nama Media Sosial / Nama Kontak</Text>
|
||||
<Text fz={"lg"}>{stateMediaSosial.findUnique.data?.name}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz={"lg"} fw={"bold"}>Icon URL / No Telephone</Text>
|
||||
<Text fz={"lg"}>{stateMediaSosial.findUnique.data?.iconUrl}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
|
||||
<Box w={100} h={100}>
|
||||
<Image src={stateMediaSosial.findUnique.data?.image?.link} alt="gambar" />
|
||||
</Box>
|
||||
</Box>
|
||||
<Box>
|
||||
<Flex gap={"xs"}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (stateMediaSosial.findUnique.data) {
|
||||
setSelectedId(stateMediaSosial.findUnique.data.id);
|
||||
setModalHapus(true);
|
||||
}
|
||||
}}
|
||||
disabled={!stateMediaSosial.findUnique.data}
|
||||
color="red">
|
||||
<IconX size={20} />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (stateMediaSosial.findUnique.data) {
|
||||
router.push(`/admin/landing-page/profile/media-sosial/${stateMediaSosial.findUnique.data.id}/edit`);
|
||||
}
|
||||
}}
|
||||
disabled={!stateMediaSosial.findUnique.data}
|
||||
color="green">
|
||||
<IconEdit size={20} />
|
||||
</Button>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Modal Hapus */}
|
||||
<ModalKonfirmasiHapus
|
||||
opened={modalHapus}
|
||||
onClose={() => setModalHapus(false)}
|
||||
onConfirm={handleHapus}
|
||||
text="Apakah anda yakin ingin menghapus media sosial ini?"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
export default DetailMediaSosial;
|
||||
|
||||
@@ -1,11 +1,148 @@
|
||||
import React from 'react';
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
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 { useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import profileLandingPageState from '../../../../_state/landing-page/profile';
|
||||
|
||||
function Page() {
|
||||
function CreateMediaSosial() {
|
||||
const router = useRouter();
|
||||
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial)
|
||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
stateMediaSosial.findMany.load();
|
||||
}, []);
|
||||
|
||||
const resetForm = () => {
|
||||
stateMediaSosial.create.form = {
|
||||
name: "",
|
||||
imageId: "",
|
||||
iconUrl: "",
|
||||
};
|
||||
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 mengupload file");
|
||||
}
|
||||
|
||||
stateMediaSosial.create.form.imageId = uploaded.id;
|
||||
|
||||
await stateMediaSosial.create.create();
|
||||
|
||||
resetForm();
|
||||
router.push("/admin/landing-page/profile/media-sosial")
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
Page
|
||||
</div>
|
||||
<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 Media Sosial</Title>
|
||||
<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>
|
||||
<TextInput
|
||||
value={stateMediaSosial.create.form.name || ''}
|
||||
onChange={(val) => {
|
||||
stateMediaSosial.create.form.name = val.target.value;
|
||||
}}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Nama Media Sosial / Nama Kontak</Text>}
|
||||
placeholder='Masukkan nama media sosial / nama kontak'
|
||||
/>
|
||||
<TextInput
|
||||
value={stateMediaSosial.create.form.iconUrl || ''}
|
||||
onChange={(val) => {
|
||||
stateMediaSosial.create.form.iconUrl = val.target.value;
|
||||
}}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Link Media Sosial / No Telephone</Text>}
|
||||
placeholder='Masukkan link media sosial / no telephone'
|
||||
/>
|
||||
<Group>
|
||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
export default CreateMediaSosial;
|
||||
|
||||
@@ -1,9 +1,93 @@
|
||||
import { Box, Title } from "@mantine/core";
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Image, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '../../../_com/header';
|
||||
import JudulList from '../../../_com/judulList';
|
||||
import profileLandingPageState from '../../../_state/landing-page/profile';
|
||||
|
||||
export default function MediaSosial() {
|
||||
function MediaSosial() {
|
||||
const [search, setSearch] = useState("");
|
||||
return (
|
||||
<Box>
|
||||
<HeaderSearch
|
||||
title='Media Sosial'
|
||||
placeholder='pencarian'
|
||||
searchIcon={<IconSearch size={20} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
<ListMediaSosial search={search} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function ListMediaSosial({ search }: { search: string }) {
|
||||
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial)
|
||||
const router = useRouter();
|
||||
|
||||
useShallowEffect(() => {
|
||||
stateMediaSosial.findMany.load()
|
||||
}, [])
|
||||
|
||||
const filteredData = (stateMediaSosial.findMany.data || []).filter(item => {
|
||||
const keyword = search.toLowerCase();
|
||||
return (
|
||||
<Box>
|
||||
<Title order={3}>Media Sosial</Title>
|
||||
</Box>
|
||||
item.name.toLowerCase().includes(keyword) ||
|
||||
item.iconUrl?.toLowerCase().includes(keyword)
|
||||
);
|
||||
});
|
||||
|
||||
if (!stateMediaSosial.findMany.data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton h={500} />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper bg={colors['white-1']} p={'md'}>
|
||||
<JudulList
|
||||
title='List Media Sosial'
|
||||
href='/admin/landing-page/profile/media-sosial/create'
|
||||
/>
|
||||
<Table striped withTableBorder withRowBorders>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>Nama Media Sosial / Nama Kontak</TableTh>
|
||||
<TableTh>Image</TableTh>
|
||||
<TableTh>Icon URL / No Telephone</TableTh>
|
||||
<TableTh>Detail</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>{item.name}</TableTd>
|
||||
<TableTd>
|
||||
<Box w={50} h={50}>
|
||||
<Image src={item.image?.link} alt={item.name} />
|
||||
</Box>
|
||||
</TableTd>
|
||||
<TableTd>{item.iconUrl}</TableTd>
|
||||
<TableTd>
|
||||
<Button onClick={() => router.push(`/admin/landing-page/profile/media-sosial/${item.id}`)}>
|
||||
<IconDeviceImac size={20} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default MediaSosial;
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Alert, Box, Button, Center, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
|
||||
import ApiFetch from '@/lib/api-fetch';
|
||||
import { Dropzone } from '@mantine/dropzone';
|
||||
import { IconAlertCircle, IconArrowBack, IconImageInPicture, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
function EditPejabatDesa() {
|
||||
const allState = useProxy(profileLandingPageState.pejabatDesa);
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
|
||||
// UI States
|
||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
// Load data on mount
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
const id = params?.id as string;
|
||||
if (!id) {
|
||||
toast.error("ID tidak valid");
|
||||
router.push("/admin/landing-page/profile/pejabat-desa");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const profileData = await profileLandingPageState.pejabatDesa.findUnique.load(id);
|
||||
profileLandingPageState.pejabatDesa.edit.initialize(profileData);
|
||||
|
||||
|
||||
if (profileData && profileData.image?.link) {
|
||||
setPreviewImage(profileData.image.link);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading profile:", error);
|
||||
toast.error("Gagal memuat data profile");
|
||||
}
|
||||
};
|
||||
|
||||
loadData();
|
||||
|
||||
return () => {
|
||||
profileLandingPageState.pejabatDesa.edit.reset(); // cleanup form
|
||||
};
|
||||
}, [params?.id, router]);
|
||||
|
||||
const handleFieldChange = (field: string, value: string) => {
|
||||
profileLandingPageState.pejabatDesa.edit.updateField(field as any, value);
|
||||
};
|
||||
|
||||
const handleFileChange = (newFile: File | null) => {
|
||||
if (!newFile) {
|
||||
setFile(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setFile(newFile);
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
setPreviewImage(event.target?.result as string);
|
||||
};
|
||||
reader.readAsDataURL(newFile);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (isSubmitting || !profileLandingPageState.pejabatDesa.edit.form.name.trim()) {
|
||||
toast.error("Nama wajib diisi");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
// Upload file jika ada
|
||||
if (file) {
|
||||
const uploadResponse = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
|
||||
const uploaded = uploadResponse.data?.data;
|
||||
|
||||
if (!uploaded?.id) {
|
||||
toast.error("Gagal upload gambar");
|
||||
return;
|
||||
}
|
||||
|
||||
profileLandingPageState.pejabatDesa.edit.form.imageId = uploaded.id;
|
||||
}
|
||||
|
||||
// Submit form
|
||||
const success = await profileLandingPageState.pejabatDesa.edit.submit();
|
||||
|
||||
if (success) {
|
||||
toast.success("Berhasil menyimpan perubahan");
|
||||
router.push("/admin/landing-page/profile/pejabat-desa");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error submitting form:", error);
|
||||
toast.error("Gagal menyimpan profile");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
router.back();
|
||||
};
|
||||
|
||||
// Loading state
|
||||
if (allState.edit.loading) {
|
||||
return (
|
||||
<Box>
|
||||
<Center h={400}>
|
||||
<Text>Memuat data profile...</Text>
|
||||
</Center>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Error state
|
||||
if (allState.edit.error) {
|
||||
return (
|
||||
<Box>
|
||||
<Stack gap="md">
|
||||
<Button variant="subtle" onClick={handleBack}>
|
||||
<IconArrowBack color={colors['blue-button']} size={20} />
|
||||
</Button>
|
||||
<Alert icon={<IconAlertCircle size={16} />} color="red">
|
||||
<Text fw="bold">Error</Text>
|
||||
<Text>{allState.edit.error}</Text>
|
||||
</Alert>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Stack gap="xs">
|
||||
<Box>
|
||||
<Button variant="subtle" onClick={handleBack}>
|
||||
<IconArrowBack color={colors['blue-button']} size={20} />
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p="md" radius={10}>
|
||||
<Stack gap="xs">
|
||||
<Title order={3}>Edit Profile Pejabat Desa</Title>
|
||||
|
||||
{/* Nama Field */}
|
||||
<TextInput
|
||||
label={<Text fw="bold">Nama Perbekel</Text>}
|
||||
placeholder="Masukkan nama perbekel"
|
||||
value={allState.edit.form.name}
|
||||
onChange={(e) => handleFieldChange('name', e.currentTarget.value)}
|
||||
error={!allState.edit.form.name && "Nama wajib diisi"}
|
||||
/>
|
||||
|
||||
{/* Posisi Field */}
|
||||
<TextInput
|
||||
label={<Text fw="bold">Posisi</Text>}
|
||||
placeholder="Masukkan posisi"
|
||||
value={allState.edit.form.position}
|
||||
onChange={(e) => handleFieldChange('position', e.currentTarget.value)}
|
||||
error={!allState.edit.form.position && "Posisi wajib diisi"}
|
||||
/>
|
||||
|
||||
{/* File Upload */}
|
||||
<Box>
|
||||
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
||||
<Box>
|
||||
<Dropzone
|
||||
onDrop={(files) => handleFileChange(files[0])}
|
||||
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>
|
||||
|
||||
{/* Preview Gambar */}
|
||||
<Box>
|
||||
<Text fz="sm" fw="bold" mb="xs">Preview Gambar</Text>
|
||||
{previewImage ? (
|
||||
<Image alt="Profile preview" src={previewImage} w={200} h={200} fit="cover" radius="md" />
|
||||
) : (
|
||||
<Center w={200} h={200} bg="gray.2">
|
||||
<Stack align="center" gap="xs">
|
||||
<IconImageInPicture size={48} color="gray" />
|
||||
<Text size="sm" c="gray">Tidak ada gambar</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Submit Button */}
|
||||
<Group>
|
||||
<Button
|
||||
bg={colors['blue-button']}
|
||||
onClick={handleSubmit}
|
||||
loading={isSubmitting || allState.edit.loading}
|
||||
disabled={!allState.edit.form.name}
|
||||
>
|
||||
{isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleBack}
|
||||
disabled={isSubmitting || allState.edit.loading}
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditPejabatDesa;
|
||||
@@ -1,11 +1,104 @@
|
||||
import React from 'react';
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Center, Divider, Grid, GridCol, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconEdit } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
|
||||
|
||||
|
||||
function Page() {
|
||||
const router = useRouter()
|
||||
const allList = useProxy(profileLandingPageState.pejabatDesa)
|
||||
useShallowEffect(() => {
|
||||
allList.findUnique.load("edit") // Assuming "1" is your default ID, adjust as needed
|
||||
}, [])
|
||||
|
||||
if (!allList.findUnique.data) {
|
||||
return <Stack>
|
||||
<Skeleton radius={10} h={800} />
|
||||
</Stack>
|
||||
}
|
||||
|
||||
const dataArray = Array.isArray(allList.findUnique.data)
|
||||
? allList.findUnique.data
|
||||
: [allList.findUnique.data];
|
||||
|
||||
return (
|
||||
<div>
|
||||
Page
|
||||
</div>
|
||||
);
|
||||
<Paper bg={colors['white-1']} p={'md'}>
|
||||
<Stack gap={"xs"}>
|
||||
<Grid>
|
||||
<GridCol span={{ base: 12, md: 11 }}>
|
||||
<Title order={3}>Preview Pejabat Desa</Title>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 1 }}>
|
||||
<Button bg={colors['blue-button']} onClick={() => router.push(`/admin/landing-page/profile/pejabat-desa/${allList.findUnique.data?.id}`)}>
|
||||
<IconEdit size={16} />
|
||||
</Button>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
{dataArray.map((item) => (
|
||||
<Box key={item.id} >
|
||||
<Paper p={"xl"} bg={colors['BG-trans']}>
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Grid>
|
||||
<GridCol span={{ base: 12, md: 12 }}>
|
||||
<Center>
|
||||
<Image src={"/darmasaba-icon.png"} w={{ base: 100, md: 150 }} alt='' />
|
||||
</Center>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 12 }}>
|
||||
<Text ta={"center"} fz={{ base: "1.2rem", md: "1.8rem" }} fw={'bold'}>PROFIL PIMPINAN BADAN PUBLIK DESA DARMASABA </Text>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
</Box>
|
||||
<Divider my={"md"} color={colors['blue-button']} />
|
||||
{/* biodata perbekel */}
|
||||
<Box px={{ base: 0, md: 50 }} pb={30}>
|
||||
<Box pb={20} px={{ base: 0, md: 50 }}>
|
||||
<Paper bg={colors['BG-trans']} w={{ base: "100%", md: "100%" }}>
|
||||
<Stack gap={0}>
|
||||
<Center>
|
||||
<Image
|
||||
pt={{ base: 0, md: 90 }}
|
||||
src={item.image?.link || "/perbekel.png"}
|
||||
w={{ base: 250, md: 350 }}
|
||||
alt='Foto Profil PPID'
|
||||
onError={(e) => {
|
||||
e.currentTarget.src = "/perbekel.png";
|
||||
}}
|
||||
/>
|
||||
</Center>
|
||||
<Paper
|
||||
bg={colors['blue-button']}
|
||||
py={20}
|
||||
className="glass3"
|
||||
px={{ base: 10, md: 10 }}
|
||||
|
||||
>
|
||||
<Text ta={"center"} c={colors['white-1']} fw={"bolder"} fz={{ base: "1.2rem", md: "1.6rem" }}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
<Box pt={10}>
|
||||
<Box>
|
||||
<Text fz={{ base: "1.125rem", md: "1.6rem" }} fw={'bold'}>Position</Text>
|
||||
<Text fz={{ base: "1rem", md: "1.5rem" }} ta={"justify"}>{item.position}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
|
||||
export default Page;
|
||||
|
||||
|
||||
export default Page;
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
|
||||
import colors from '@/con/colors';
|
||||
@@ -52,7 +53,7 @@ function EditProgramInovasi() {
|
||||
}
|
||||
|
||||
loadProgramInovasi();
|
||||
}, [params?.id, stateProgramInovasi.update]);
|
||||
}, [params?.id]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
"use client";
|
||||
|
||||
import sdgsDesa from "@/app/admin/(dashboard)/_state/landing-page/sdgs-desa";
|
||||
import colors from "@/con/colors";
|
||||
import ApiFetch from "@/lib/api-fetch";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Paper,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title
|
||||
} from "@mantine/core";
|
||||
import { Dropzone } from "@mantine/dropzone";
|
||||
import { IconArrowBack, IconImageInPicture, 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 EditKolaborasiInovasi() {
|
||||
const sdgsState = useProxy(sdgsDesa);
|
||||
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: sdgsState.edit.form.name || '',
|
||||
jumlah: sdgsState.edit.form.jumlah || '',
|
||||
imageId: sdgsState.edit.form.imageId || ''
|
||||
});
|
||||
|
||||
// Load sdgs desa by id saat pertama kali
|
||||
useEffect(() => {
|
||||
const loadKolaborasi = async () => {
|
||||
const id = params?.id as string;
|
||||
if (!id) return;
|
||||
|
||||
try {
|
||||
const data = await sdgsState.edit.load(id); // akses langsung, bukan dari proxy
|
||||
if (data) {
|
||||
setFormData({
|
||||
name: data.name || '',
|
||||
jumlah: data.jumlah || '',
|
||||
imageId: data.imageId || '',
|
||||
});
|
||||
if (data.image) {
|
||||
if (data?.image?.link) {
|
||||
setPreviewImage(data.image.link);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading sdgs desa:", error);
|
||||
toast.error("Gagal memuat data sdgs desa");
|
||||
}
|
||||
};
|
||||
|
||||
loadKolaborasi();
|
||||
}, [params?.id]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
|
||||
try {
|
||||
// edit global state with form data
|
||||
sdgsState.edit.form = {
|
||||
...sdgsState.edit.form,
|
||||
name: formData.name,
|
||||
jumlah: formData.jumlah,
|
||||
imageId: formData.imageId // Keep existing imageId if not changed
|
||||
};
|
||||
|
||||
// Jika ada file baru, upload
|
||||
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");
|
||||
}
|
||||
|
||||
// edit imageId in global state
|
||||
sdgsState.edit.form.imageId = uploaded.id;
|
||||
}
|
||||
|
||||
await sdgsState.edit.update();
|
||||
toast.success("sdgs desa berhasil diperbarui!");
|
||||
router.push("/admin/landing-page/SDGs-Desa");
|
||||
} catch (error) {
|
||||
console.error("Error updating sdgs desa:", error);
|
||||
toast.error("Terjadi kesalahan saat memperbarui sdgs desa");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button variant="subtle" onClick={() => router.back()}>
|
||||
<IconArrowBack color={colors["blue-button"]} size={30} />
|
||||
</Button>
|
||||
</Box>
|
||||
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
|
||||
<Stack gap={"xs"}>
|
||||
<Title order={3}>Edit SDGs Desa</Title>
|
||||
<TextInput
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
label={<Text fz={"sm"} fw={"bold"}>Nama</Text>}
|
||||
placeholder="masukkan nama"
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
value={formData.jumlah}
|
||||
onChange={(e) => setFormData({ ...formData, jumlah: e.target.value })}
|
||||
label={<Text fz={"sm"} fw={"bold"}>Jumlah</Text>}
|
||||
placeholder="masukkan jumlah"
|
||||
/>
|
||||
<Box>
|
||||
<Text fz={"md"} fw={"bold"}>File Image</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={{
|
||||
'application/*': ['.jpeg', '.jpg', '.png', '.gif', '.webp', '.svg'],
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
<IconImageInPicture size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
|
||||
<div>
|
||||
<Text size="xl" inline>
|
||||
Drag file ke sini atau klik untuk pilih file
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" inline mt={7}>
|
||||
Maksimal 5MB dan harus format image
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Dokumen</Text>
|
||||
{previewImage ? (
|
||||
<iframe
|
||||
src={previewImage}
|
||||
width="100%"
|
||||
height="500px"
|
||||
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
|
||||
/>
|
||||
) : (
|
||||
<Text>Tidak ada image tersedia</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Button onClick={handleSubmit}>Simpan</Button>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditKolaborasiInovasi;
|
||||
110
src/app/admin/(dashboard)/landing-page/sdgs-desa/[id]/page.tsx
Normal file
110
src/app/admin/(dashboard)/landing-page/sdgs-desa/[id]/page.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
'use client'
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
|
||||
import colors from '@/con/colors';
|
||||
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||
import sdgsDesa from '../../../_state/landing-page/sdgs-desa';
|
||||
|
||||
function DetailSDGSDesa() {
|
||||
const sdgsState = useProxy(sdgsDesa)
|
||||
const [modalHapus, setModalHapus] = useState(false)
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||
const params = useParams()
|
||||
const router = useRouter()
|
||||
|
||||
useShallowEffect(() => {
|
||||
sdgsState.findUnique.load(params?.id as string)
|
||||
}, [])
|
||||
|
||||
|
||||
const handleHapus = () => {
|
||||
if (selectedId) {
|
||||
sdgsState.delete.byId(selectedId)
|
||||
setModalHapus(false)
|
||||
setSelectedId(null)
|
||||
router.push("/admin/landing-page/SDGs-Desa")
|
||||
}
|
||||
}
|
||||
|
||||
if (!sdgsState.findUnique.data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton h={40} />
|
||||
</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 SDGS Desa</Text>
|
||||
{sdgsState.findUnique.data ? (
|
||||
<Paper key={sdgsState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
||||
<Stack gap={"xs"}>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Nama SDGS Desa</Text>
|
||||
<Text fz={"lg"}>{sdgsState.findUnique.data?.name}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Jumlah</Text>
|
||||
<Text fz={"lg"}>{sdgsState.findUnique.data?.jumlah}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
|
||||
<Image w={{ base: 150, md: 150, lg: 150 }} src={sdgsState.findUnique.data?.image?.link} alt="gambar" />
|
||||
</Box>
|
||||
<Flex gap={"xs"} mt={10}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (sdgsState.findUnique.data) {
|
||||
setSelectedId(sdgsState.findUnique.data.id);
|
||||
setModalHapus(true);
|
||||
}
|
||||
}}
|
||||
disabled={sdgsState.delete.loading || !sdgsState.findUnique.data}
|
||||
color={"red"}
|
||||
>
|
||||
<IconX size={20} />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (sdgsState.findUnique.data) {
|
||||
router.push(`/admin/landing-page/SDGs-Desa/${sdgsState.findUnique.data.id}/edit`);
|
||||
}
|
||||
}}
|
||||
disabled={!sdgsState.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 SDGS Desa ini?'
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default DetailSDGSDesa;
|
||||
148
src/app/admin/(dashboard)/landing-page/sdgs-desa/create/page.tsx
Normal file
148
src/app/admin/(dashboard)/landing-page/sdgs-desa/create/page.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import ApiFetch from '@/lib/api-fetch';
|
||||
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import { Dropzone } from '@mantine/dropzone';
|
||||
import { IconArrowBack, IconImageInPicture, IconUpload, IconX } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import sdgsDesa from '../../../_state/landing-page/sdgs-desa';
|
||||
|
||||
|
||||
function CreateSDGsDesa() {
|
||||
const router = useRouter();
|
||||
const stateSDGSDesa = useProxy(sdgsDesa)
|
||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
stateSDGSDesa.findMany.load();
|
||||
}, []);
|
||||
|
||||
const resetForm = () => {
|
||||
stateSDGSDesa.create.form = {
|
||||
name: "",
|
||||
jumlah: "",
|
||||
imageId: "",
|
||||
};
|
||||
setFile(null);
|
||||
setPreviewImage(null);
|
||||
};
|
||||
const handleSubmit = async () => {
|
||||
if (!file) {
|
||||
return toast.warn("Pilih file image 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 mengupload file");
|
||||
}
|
||||
|
||||
stateSDGSDesa.create.form.imageId = uploaded.id;
|
||||
|
||||
await stateSDGSDesa.create.create();
|
||||
|
||||
resetForm();
|
||||
router.push("/admin/landing-page/SDGs-Desa")
|
||||
}
|
||||
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 SDGs Desa</Title>
|
||||
<Box>
|
||||
<Text fz={"md"} fw={"bold"}>File Image</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={{
|
||||
'application/*': ['.jpeg', '.jpg', '.png', '.gif', '.webp', '.svg'],
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
<IconImageInPicture size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
|
||||
<div>
|
||||
<Text size="xl" inline>
|
||||
Drag file ke sini atau klik untuk pilih file
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" inline mt={7}>
|
||||
Maksimal 5MB dan harus format image
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Dokumen</Text>
|
||||
{previewImage ? (
|
||||
<iframe
|
||||
src={previewImage}
|
||||
width="100%"
|
||||
height="500px"
|
||||
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
|
||||
/>
|
||||
) : (
|
||||
<Text>Tidak ada image tersedia</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<TextInput
|
||||
value={stateSDGSDesa.create.form.name}
|
||||
onChange={(val) => {
|
||||
stateSDGSDesa.create.form.name = val.target.value;
|
||||
}}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>}
|
||||
placeholder='Masukkan judul'
|
||||
/>
|
||||
<TextInput
|
||||
type='number'
|
||||
value={stateSDGSDesa.create.form.jumlah}
|
||||
onChange={(val) => {
|
||||
stateSDGSDesa.create.form.jumlah = val.target.value;
|
||||
}}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Jumlah</Text>}
|
||||
placeholder='Masukkan jumlah'
|
||||
/>
|
||||
<Group>
|
||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateSDGsDesa;
|
||||
@@ -1,11 +1,97 @@
|
||||
import React from 'react';
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
||||
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '../../_com/header';
|
||||
import sdgsDesa from '../../_state/landing-page/sdgs-desa';
|
||||
import JudulList from '../../_com/judulList';
|
||||
|
||||
function Page() {
|
||||
|
||||
function SdgsDesa() {
|
||||
const [search, setSearch] = useState('');
|
||||
return (
|
||||
<div>
|
||||
SDGS Desa
|
||||
</div>
|
||||
<Box>
|
||||
<HeaderSearch
|
||||
title='SDGs Desa'
|
||||
placeholder='pencarian'
|
||||
searchIcon={<IconSearch size={20} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
<ListSdgsDesa search={search} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
function ListSdgsDesa({ search }: { search: string }) {
|
||||
const listState = useProxy(sdgsDesa)
|
||||
const router = useRouter();
|
||||
useEffect(() => {
|
||||
listState.findMany.load()
|
||||
}, [])
|
||||
|
||||
const filteredData = (listState.findMany.data || []).filter(item => {
|
||||
const keyword = search.toLowerCase();
|
||||
return (
|
||||
item.name.toLowerCase().includes(keyword) ||
|
||||
item.jumlah.toLowerCase().includes(keyword)
|
||||
)
|
||||
});
|
||||
|
||||
if (!listState.findMany.data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton h={500} />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper bg={colors['white-1']} p={'md'}>
|
||||
<Stack>
|
||||
<JudulList
|
||||
title='SDGs Desa'
|
||||
href='/admin/landing-page/SDGs-Desa/create'
|
||||
/>
|
||||
<Box style={{ overflowX: 'auto' }}>
|
||||
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>Nama SDGs Desa</TableTh>
|
||||
<TableTh>Jumlah SDGs Desa</TableTh>
|
||||
<TableTh>Detail</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>
|
||||
<Box w={100}>
|
||||
<Text truncate="end" fz={"sm"}>{item.name}</Text>
|
||||
</Box>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text truncate="end" fz={"sm"}>{item.jumlah}</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button onClick={() => router.push(`/admin/landing-page/SDGs-Desa/${item.id}`)}>
|
||||
<IconDeviceImacCog size={25} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default SdgsDesa;
|
||||
|
||||
@@ -50,7 +50,7 @@ function DetailKegiatanDesa() {
|
||||
</Box>
|
||||
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
|
||||
<Stack>
|
||||
<Text fz={"xl"} fw={"bold"}>Detail kegiatanDesa Inovasi</Text>
|
||||
<Text fz={"xl"} fw={"bold"}>Detail Kegiatan Desa Inovasi</Text>
|
||||
{kegiatanDesaState.findUnique.data ? (
|
||||
<Paper key={kegiatanDesaState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
||||
<Stack gap={"xs"}>
|
||||
|
||||
@@ -12,7 +12,7 @@ export const navBar = [
|
||||
{
|
||||
id: "Landing_Page_2",
|
||||
name: "Desa Anti Korupsi",
|
||||
path: "/admin/landing-page/desa-anti-korupsi"
|
||||
path: "/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi"
|
||||
},
|
||||
{
|
||||
id: "Landing_Page_3",
|
||||
@@ -22,17 +22,17 @@ export const navBar = [
|
||||
{
|
||||
id: "Landing_Page_4",
|
||||
name: "SDGs Desa",
|
||||
path: "/admin/landing-page/sdgs-desa"
|
||||
path: "/admin/landing-page/SDGs-Desa"
|
||||
},
|
||||
{
|
||||
id: "Landing_Page_5",
|
||||
name: "APBDes",
|
||||
path: "/admin/landing-page/apbdes"
|
||||
path: "/admin/landing-page/APBDes"
|
||||
},
|
||||
{
|
||||
id: "Landing_Page_6",
|
||||
name: "Prestasi Desa",
|
||||
path: "/admin/landing-page/prestasi-desa"
|
||||
path: "/admin/landing-page/prestasi-desa/list-prestasi-desa"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -7,7 +7,9 @@ import {
|
||||
AppShellHeader,
|
||||
AppShellMain,
|
||||
AppShellNavbar,
|
||||
Box,
|
||||
Burger,
|
||||
Flex,
|
||||
Group,
|
||||
Image,
|
||||
NavLink,
|
||||
@@ -15,16 +17,16 @@ import {
|
||||
Text
|
||||
} from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { IconChevronLeft, IconChevronRight } from "@tabler/icons-react";
|
||||
import { IconChevronLeft, IconChevronRight, IconDoorExit } from "@tabler/icons-react";
|
||||
import _ from 'lodash';
|
||||
import Link from "next/link";
|
||||
import { useSelectedLayoutSegments } from "next/navigation";
|
||||
import { useRouter, useSelectedLayoutSegments } from "next/navigation";
|
||||
import { navBar } from "./_com/list_PageAdmin";
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
const [opened, { toggle }] = useDisclosure();
|
||||
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
|
||||
|
||||
const router = useRouter()
|
||||
// Normalisasi semua segmen jadi lowercase
|
||||
const segments = useSelectedLayoutSegments().map(s => _.lowerCase(s));
|
||||
|
||||
@@ -44,6 +46,18 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
>
|
||||
<AppShellHeader bg={colors["white-1"]}>
|
||||
<Group px={10} align="center">
|
||||
<Flex align="center" gap={'xs'}>
|
||||
<Image
|
||||
py={5}
|
||||
src={'/assets/images/darmasaba-icon.png'}
|
||||
alt=""
|
||||
width={50}
|
||||
height={50}
|
||||
/>
|
||||
<Text fw={'bold'} c={colors["blue-button"]} fz={'lg'}>
|
||||
Dashboard Admin
|
||||
</Text>
|
||||
</Flex>
|
||||
{!desktopOpened && (
|
||||
<ActionIcon variant="light" onClick={toggleDesktop}>
|
||||
<IconChevronRight />
|
||||
@@ -55,6 +69,13 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
hiddenFrom="sm"
|
||||
size={'sm'}
|
||||
/>
|
||||
<Box>
|
||||
<ActionIcon onClick={() => {
|
||||
router.push("/darmasaba")
|
||||
}} color={colors["blue-button"]} radius={'xl'}>
|
||||
<IconDoorExit size={24} />
|
||||
</ActionIcon>
|
||||
</Box>
|
||||
<ActionIcon
|
||||
w={50}
|
||||
h={50}
|
||||
@@ -62,17 +83,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
component={Link}
|
||||
href="/admin"
|
||||
>
|
||||
<Image
|
||||
py={5}
|
||||
src={'/assets/images/darmasaba-icon.png'}
|
||||
alt=""
|
||||
width={50}
|
||||
height={50}
|
||||
/>
|
||||
</ActionIcon>
|
||||
<Text fw={'bold'} c={colors["blue-button"]} fz={'lg'}>
|
||||
Dashboard Admin
|
||||
</Text>
|
||||
</Group>
|
||||
</AppShellHeader>
|
||||
|
||||
|
||||
@@ -14,27 +14,15 @@ const fileStorageCreate = async (context: Context) => {
|
||||
const file = body.file;
|
||||
const name = body.name;
|
||||
|
||||
if (!file) {
|
||||
return {
|
||||
status: 400,
|
||||
body: "No file uploaded",
|
||||
};
|
||||
}
|
||||
if (!file) return { status: 400, body: "No file uploaded" };
|
||||
if (!name) return { status: 400, body: "No name provided" };
|
||||
if (!UPLOAD_DIR) return { status: 500, body: "UPLOAD_DIR is not defined" };
|
||||
|
||||
if (!name) {
|
||||
return {
|
||||
status: 400,
|
||||
body: "No name provided",
|
||||
};
|
||||
}
|
||||
// Tentukan kategori berdasarkan mimeType
|
||||
const isImage = file.type.startsWith("image/");
|
||||
const category = isImage ? "image" : "document";
|
||||
|
||||
if (!UPLOAD_DIR) {
|
||||
return {
|
||||
status: 500,
|
||||
body: "UPLOAD_DIR is not defined",
|
||||
};
|
||||
}
|
||||
const pathName = "allFile";
|
||||
const pathName = category === "image" ? "images" : "documents";
|
||||
const rootPath = path.join(UPLOAD_DIR, pathName);
|
||||
await fs.mkdir(rootPath, { recursive: true });
|
||||
|
||||
@@ -44,9 +32,10 @@ const fileStorageCreate = async (context: Context) => {
|
||||
const data = await prisma.fileStorage.create({
|
||||
data: {
|
||||
name: newName,
|
||||
realName: file.name, // Add the original file name as realName
|
||||
realName: file.name,
|
||||
path: rootPath,
|
||||
mimeType: file.type,
|
||||
category,
|
||||
link: `/api/fileStorage/findUnique/${newName}`,
|
||||
},
|
||||
});
|
||||
@@ -56,9 +45,7 @@ const fileStorageCreate = async (context: Context) => {
|
||||
Buffer.from(await file.arrayBuffer())
|
||||
);
|
||||
|
||||
return {
|
||||
data,
|
||||
};
|
||||
return { data };
|
||||
};
|
||||
|
||||
export default fileStorageCreate;
|
||||
export default fileStorageCreate;
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
export const fileStorageFindMany = async () => {
|
||||
const data = await prisma.fileStorage.findMany();
|
||||
return data;
|
||||
export const fileStorageFindMany = async (context: Context) => {
|
||||
const category = context.query?.category as string | undefined;
|
||||
|
||||
const data = await prisma.fileStorage.findMany({
|
||||
where: category ? { category } : {},
|
||||
});
|
||||
|
||||
return { data };
|
||||
};
|
||||
|
||||
36
src/app/api/[[...slugs]]/_lib/landing_page/apbdes/create.ts
Normal file
36
src/app/api/[[...slugs]]/_lib/landing_page/apbdes/create.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
type FormCreate = {
|
||||
name: string;
|
||||
jumlah: string;
|
||||
imageId: string;
|
||||
fileId: string;
|
||||
};
|
||||
|
||||
export default async function apbdesCreate(context: Context) {
|
||||
const body = context.body as FormCreate;
|
||||
|
||||
try {
|
||||
const result = await prisma.aPBDes.create({
|
||||
data: {
|
||||
name: body.name,
|
||||
jumlah: body.jumlah,
|
||||
imageId: body.imageId,
|
||||
fileId: body.fileId,
|
||||
},
|
||||
include: {
|
||||
image: true,
|
||||
file: true,
|
||||
},
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
message: "Berhasil membuat APB Des",
|
||||
data: result,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error creating APB Des:", error);
|
||||
throw new Error("Gagal membuat APB Des: " + (error as Error).message);
|
||||
}
|
||||
}
|
||||
21
src/app/api/[[...slugs]]/_lib/landing_page/apbdes/del.ts
Normal file
21
src/app/api/[[...slugs]]/_lib/landing_page/apbdes/del.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
export default async function apbdesDelete(context: Context) {
|
||||
const { params } = context;
|
||||
const id = params?.id as string;
|
||||
|
||||
if (!id) {
|
||||
throw new Error("ID tidak ditemukan dalam parameter");
|
||||
}
|
||||
|
||||
const deleted = await prisma.aPBDes.delete({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Berhasil menghapus APB Des",
|
||||
data: deleted,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// /api/berita/findManyPaginated.ts
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
async function apbdesFindMany(context: Context) {
|
||||
const page = Number(context.query.page) || 1;
|
||||
const limit = Number(context.query.limit) || 10;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
try {
|
||||
const [data, total] = await Promise.all([
|
||||
prisma.aPBDes.findMany({
|
||||
where: { isActive: true },
|
||||
include: {
|
||||
image: true,
|
||||
file: true,
|
||||
},
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: { createdAt: "desc" }, // opsional, kalau mau urut berdasarkan waktu
|
||||
}),
|
||||
prisma.aPBDes.count({
|
||||
where: { isActive: true },
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Success fetch APB Des with pagination",
|
||||
data,
|
||||
page,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
total,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("Find many paginated error:", e);
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed fetch APB Des with pagination",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default apbdesFindMany;
|
||||
@@ -0,0 +1,29 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
export default async function apbdesFindUnique(context: Context) {
|
||||
const { params } = context;
|
||||
const id = params?.id as string;
|
||||
|
||||
if (!id) {
|
||||
throw new Error("ID tidak ditemukan dalam parameter");
|
||||
}
|
||||
|
||||
const data = await prisma.aPBDes.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
image: true,
|
||||
file: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
throw new Error("APB Des tidak ditemukan");
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Data APB Des ditemukan",
|
||||
data,
|
||||
};
|
||||
}
|
||||
41
src/app/api/[[...slugs]]/_lib/landing_page/apbdes/index.ts
Normal file
41
src/app/api/[[...slugs]]/_lib/landing_page/apbdes/index.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import Elysia, { t } from "elysia";
|
||||
import apbdesCreate from "./create";
|
||||
import apbdesDelete from "./del";
|
||||
import apbdesFindMany from "./findMany";
|
||||
import apbdesFindUnique from "./findUnique";
|
||||
import apbdesUpdate from "./updt";
|
||||
|
||||
const APBDes = new Elysia({
|
||||
prefix: "/apbdes",
|
||||
tags: ["Landing Page/Profile/APB Des"],
|
||||
})
|
||||
|
||||
// ✅ Find all
|
||||
.get("/find-many", apbdesFindMany)
|
||||
|
||||
// ✅ Find by ID
|
||||
.get("/:id", apbdesFindUnique)
|
||||
|
||||
// ✅ Create
|
||||
.post("/create", apbdesCreate, {
|
||||
body: t.Object({
|
||||
name: t.String(),
|
||||
imageId: t.String(),
|
||||
fileId: t.String(),
|
||||
jumlah: t.String(),
|
||||
}),
|
||||
})
|
||||
|
||||
// ✅ Update
|
||||
.put("/:id", apbdesUpdate, {
|
||||
body: t.Object({
|
||||
name: t.String(),
|
||||
imageId: t.Optional(t.String()),
|
||||
fileId: t.Optional(t.String()),
|
||||
jumlah: t.Optional(t.String()),
|
||||
}),
|
||||
})
|
||||
// ✅ Delete
|
||||
.delete("/del/:id", apbdesDelete);
|
||||
|
||||
export default APBDes;
|
||||
51
src/app/api/[[...slugs]]/_lib/landing_page/apbdes/updt.ts
Normal file
51
src/app/api/[[...slugs]]/_lib/landing_page/apbdes/updt.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
type FormUpdateAPBDes = {
|
||||
name?: string;
|
||||
imageId?: string;
|
||||
fileId?: string;
|
||||
jumlah?: string;
|
||||
};
|
||||
|
||||
export default async function apbdesUpdate(context: Context) {
|
||||
const body = context.body as FormUpdateAPBDes;
|
||||
|
||||
const id = context.params.id;
|
||||
|
||||
if (!id) {
|
||||
return {
|
||||
success: false,
|
||||
message: "ID APB Des wajib diisi",
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const updated = await prisma.aPBDes.update({
|
||||
where: { id },
|
||||
data: {
|
||||
name: body.name,
|
||||
imageId: body.imageId,
|
||||
fileId: body.fileId,
|
||||
jumlah: body.jumlah,
|
||||
},
|
||||
include: {
|
||||
image: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "APB Des berhasil diperbarui",
|
||||
data: updated,
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error("❌ Error update APB Des:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "Gagal memperbarui data APB Des",
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
type FormCreate = {
|
||||
name: string;
|
||||
deskripsi: string;
|
||||
fileId: string;
|
||||
kategoriId: string; // minimal satu kategori
|
||||
};
|
||||
|
||||
export default async function desaAntiKorupsiCreate(context: Context) {
|
||||
const body = context.body as FormCreate;
|
||||
|
||||
if (!body.kategoriId) {
|
||||
throw new Error("kategoriId wajib diisi");
|
||||
}
|
||||
|
||||
try {
|
||||
// Create langsung data AdministrasiOnline
|
||||
const result = await prisma.desaAntiKorupsi.create({
|
||||
data: {
|
||||
name: body.name,
|
||||
deskripsi: body.deskripsi,
|
||||
fileId: body.fileId,
|
||||
kategoriId: body.kategoriId, // relasi ke JenisLayanan
|
||||
},
|
||||
include: {
|
||||
kategori: true, // Include data relasi
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Berhasil membuat desa anti korupsi",
|
||||
data: result,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error creating desa anti korupsi:", error);
|
||||
throw new Error(
|
||||
"Gagal membuat desa anti korupsi: " + (error as Error).message
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
export default async function desaAntiKorupsiDelete(context: Context) {
|
||||
const { params } = context;
|
||||
const id = params?.id as string;
|
||||
|
||||
if (!id) {
|
||||
throw new Error("ID tidak ditemukan dalam parameter");
|
||||
}
|
||||
|
||||
const deleted = await prisma.desaAntiKorupsi.delete({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Berhasil menghapus desa anti korupsi",
|
||||
data: deleted,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// /api/berita/findManyPaginated.ts
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
async function desaAntiKorupsiFindMany(context: Context) {
|
||||
const page = Number(context.query.page) || 1;
|
||||
const limit = Number(context.query.limit) || 10;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
try {
|
||||
const [data, total] = await Promise.all([
|
||||
prisma.desaAntiKorupsi.findMany({
|
||||
where: { isActive: true },
|
||||
include: {
|
||||
kategori: true,
|
||||
file: true,
|
||||
},
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: { createdAt: 'desc' }, // opsional, kalau mau urut berdasarkan waktu
|
||||
}),
|
||||
prisma.desaAntiKorupsi.count({
|
||||
where: { isActive: true }
|
||||
})
|
||||
]);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Success fetch desa anti korupsi with pagination",
|
||||
data,
|
||||
page,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
total,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("Find many paginated error:", e);
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed fetch desa anti korupsi with pagination",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default desaAntiKorupsiFindMany;
|
||||
@@ -0,0 +1,29 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
export default async function desaAntiKorupsiFindUnique(context: Context) {
|
||||
const { params } = context;
|
||||
const id = params?.id as string;
|
||||
|
||||
if (!id) {
|
||||
throw new Error("ID tidak ditemukan dalam parameter");
|
||||
}
|
||||
|
||||
const data = await prisma.desaAntiKorupsi.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
kategori: true,
|
||||
file: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
throw new Error("Desa anti korupsi tidak ditemukan");
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Data desa anti korupsi ditemukan",
|
||||
data,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import Elysia, { t } from "elysia";
|
||||
import desaAntiKorupsiDelete from "./del";
|
||||
import desaAntiKorupsiFindMany from "./findMany";
|
||||
import desaAntiKorupsiFindUnique from "./findUnique";
|
||||
import desaAntiKorupsiUpdate from "./updt";
|
||||
import desaAntiKorupsiCreate from "./create";
|
||||
|
||||
const DesaAntiKorupsi = new Elysia({
|
||||
prefix: "/desaantikorupsi",
|
||||
tags: ["Landing Page/Desa Anti Korupsi"],
|
||||
})
|
||||
|
||||
// ✅ Find all
|
||||
.get("/find-many", desaAntiKorupsiFindMany)
|
||||
|
||||
// ✅ Find by ID
|
||||
.get("/:id", desaAntiKorupsiFindUnique)
|
||||
|
||||
// ✅ Create
|
||||
.post("/create", desaAntiKorupsiCreate, {
|
||||
body: t.Object({
|
||||
name: t.String(),
|
||||
deskripsi: t.String(),
|
||||
fileId: t.String(),
|
||||
kategoriId: t.String(),
|
||||
}),
|
||||
})
|
||||
|
||||
// ✅ Update
|
||||
.put("/:id", desaAntiKorupsiUpdate, {
|
||||
body: t.Object({
|
||||
name: t.String(),
|
||||
deskripsi: t.String(),
|
||||
fileId: t.String(),
|
||||
kategoriId: t.String(),
|
||||
}),
|
||||
})
|
||||
// ✅ Delete
|
||||
.delete("/del/:id", desaAntiKorupsiDelete);
|
||||
|
||||
export default DesaAntiKorupsi;
|
||||
@@ -0,0 +1,25 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
|
||||
export default async function kategoriDesaAntiKorupsiCreate(context: Context) {
|
||||
const body = context.body as {name: string};
|
||||
|
||||
if (!body.name) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Nama is required",
|
||||
};
|
||||
}
|
||||
|
||||
const kategoriDesaAntiKorupsi = await prisma.kategoriDesaAntiKorupsi.create({
|
||||
data: {
|
||||
name: body.name,
|
||||
},
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
message: "Success create kategori desa anti korupsi",
|
||||
data: kategoriDesaAntiKorupsi
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
export default async function kategoriDesaAntiKorupsiDelete(context: Context) {
|
||||
const { params } = context;
|
||||
const id = params?.id as string;
|
||||
|
||||
if (!id) {
|
||||
throw new Error("ID tidak ditemukan dalam parameter");
|
||||
}
|
||||
|
||||
const deleted = await prisma.kategoriDesaAntiKorupsi.delete({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Berhasil menghapus kategori desa anti korupsi",
|
||||
data: deleted,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
export default async function kategoriDesaAntiKorupsiFindMany() {
|
||||
const data = await prisma.kategoriDesaAntiKorupsi.findMany();
|
||||
return {
|
||||
success: true,
|
||||
data: data.map((item: any) => {
|
||||
return {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
}
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Context } from "elysia";
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
export default async function kategoriDesaAntiKorupsiFindUnique(context: Context) {
|
||||
const url = new URL(context.request.url);
|
||||
const pathSegments = url.pathname.split('/');
|
||||
const id = pathSegments[pathSegments.length - 1];
|
||||
|
||||
if (!id) {
|
||||
return {
|
||||
success: false,
|
||||
message: "ID is required",
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof id !== 'string') {
|
||||
return {
|
||||
success: false,
|
||||
message: "ID is required",
|
||||
}
|
||||
}
|
||||
|
||||
const data = await prisma.kategoriDesaAntiKorupsi.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Kategori desa anti korupsi tidak ditemukan",
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Success find kategori desa anti korupsi",
|
||||
data,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Find by ID error:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "Gagal mengambil kategori desa anti korupsi: " + (error instanceof Error ? error.message : 'Unknown error'),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import Elysia, { t } from "elysia";
|
||||
import kategoriDesaAntiKorupsiCreate from "./create";
|
||||
import kategoriDesaAntiKorupsiDelete from "./del";
|
||||
import kategoriDesaAntiKorupsiFindMany from "./findMany";
|
||||
import kategoriDesaAntiKorupsiFindUnique from "./findUnique";
|
||||
import kategoriDesaAntiKorupsiUpdate from "./updt";
|
||||
|
||||
const KategoriDesaAntiKorupsi = new Elysia({
|
||||
prefix: "/kategoridak",
|
||||
tags: ["Landing Page/Desa Anti Korupsi"],
|
||||
})
|
||||
.get("/find-many", kategoriDesaAntiKorupsiFindMany)
|
||||
.get("/:id", async (context) => {
|
||||
const response = await kategoriDesaAntiKorupsiFindUnique(context);
|
||||
return response;
|
||||
})
|
||||
.delete("/del/:id", kategoriDesaAntiKorupsiDelete)
|
||||
.post("/create", kategoriDesaAntiKorupsiCreate, {
|
||||
body: t.Object({
|
||||
name: t.String(),
|
||||
}),
|
||||
})
|
||||
.put("/:id", kategoriDesaAntiKorupsiUpdate, {
|
||||
body: t.Object({
|
||||
name: t.String(),
|
||||
}),
|
||||
});
|
||||
|
||||
export default KategoriDesaAntiKorupsi;
|
||||
@@ -0,0 +1,44 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
export default async function kategoriDesaAntiKorupsiUpdate(context: Context) {
|
||||
const body = context.body as { name: string };
|
||||
const id = context.params?.id as string;
|
||||
|
||||
// Validasi ID dan nama
|
||||
if (!id) {
|
||||
return {
|
||||
success: false,
|
||||
message: "ID is required",
|
||||
};
|
||||
}
|
||||
|
||||
if (!body.name) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Name is required",
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const kategoriDesaAntiKorupsi = await prisma.kategoriDesaAntiKorupsi.update({
|
||||
where: { id },
|
||||
data: {
|
||||
name: body.name,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Success update kategori desa anti korupsi",
|
||||
data: kategoriDesaAntiKorupsi,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Update error:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "Gagal update kategori desa anti korupsi",
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
type FormUpdateDesaAntiKorupsi = {
|
||||
name?: string;
|
||||
deskripsi?: string;
|
||||
fileId?: string;
|
||||
kategoriId?: string; // minimal satu kategori
|
||||
};
|
||||
|
||||
export default async function desaAntiKorupsiUpdate(context: Context) {
|
||||
const body = context.body as FormUpdateDesaAntiKorupsi;
|
||||
|
||||
const id = context.params.id;
|
||||
|
||||
if (!id) {
|
||||
return {
|
||||
success: false,
|
||||
message: "ID desa anti korupsi wajib diisi",
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const updated = await prisma.desaAntiKorupsi.update({
|
||||
where: { id },
|
||||
data: {
|
||||
name: body.name,
|
||||
deskripsi: body.deskripsi,
|
||||
fileId: body.fileId,
|
||||
kategoriId: body.kategoriId,
|
||||
},
|
||||
include: {
|
||||
file: true,
|
||||
kategori: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Desa anti korupsi berhasil diperbarui",
|
||||
data: updated,
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error("❌ Error update desa anti korupsi:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "Gagal memperbarui data desa anti korupsi",
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,12 @@ import Elysia from "elysia";
|
||||
import MediaSosial from "./profile/media-sosial";
|
||||
import ProgramInovasi from "./profile/program-inovasi";
|
||||
import PejabatDesa from "./profile/pejabat-desa";
|
||||
import KategoriDesaAntiKorupsi from "./desa-anti-korupsi/kategori-dak";
|
||||
import DesaAntiKorupsi from "./desa-anti-korupsi";
|
||||
import SDGSDesa from "./sdgs-desa";
|
||||
import APBDes from "./apbdes";
|
||||
import PrestasiDesa from "./prestasi-desa";
|
||||
import KategoriPrestasi from "./prestasi-desa/kategori-prestasi";
|
||||
|
||||
const LandingPage = new Elysia({
|
||||
prefix: "/api/landingpage",
|
||||
@@ -11,5 +17,11 @@ const LandingPage = new Elysia({
|
||||
.use(MediaSosial)
|
||||
.use(ProgramInovasi)
|
||||
.use(PejabatDesa)
|
||||
.use(KategoriDesaAntiKorupsi)
|
||||
.use(DesaAntiKorupsi)
|
||||
.use(SDGSDesa)
|
||||
.use(APBDes)
|
||||
.use(PrestasiDesa)
|
||||
.use(KategoriPrestasi)
|
||||
|
||||
export default LandingPage
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
type FormCreate = {
|
||||
name: string;
|
||||
deskripsi: string;
|
||||
imageId: string;
|
||||
kategoriId: string;
|
||||
};
|
||||
|
||||
export default async function prestasiDesaCreate(context: Context) {
|
||||
const body = context.body as FormCreate;
|
||||
|
||||
try {
|
||||
const result = await prisma.prestasiDesa.create({
|
||||
data: {
|
||||
name: body.name,
|
||||
deskripsi: body.deskripsi,
|
||||
imageId: body.imageId,
|
||||
kategoriId: body.kategoriId,
|
||||
},
|
||||
include: {
|
||||
image: true,
|
||||
kategori: true,
|
||||
},
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
message: "Berhasil membuat Prestasi Desa",
|
||||
data: result,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error creating Prestasi Desa:", error);
|
||||
throw new Error(
|
||||
"Gagal membuat Prestasi Desa: " + (error as Error).message
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
export default async function prestasiDesaDelete(context: Context) {
|
||||
const { params } = context;
|
||||
const id = params?.id as string;
|
||||
|
||||
if (!id) {
|
||||
throw new Error("ID tidak ditemukan dalam parameter");
|
||||
}
|
||||
|
||||
const deleted = await prisma.prestasiDesa.delete({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Berhasil menghapus Prestasi Desa",
|
||||
data: deleted,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// /api/berita/findManyPaginated.ts
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
async function prestasiDesaFindMany(context: Context) {
|
||||
const page = Number(context.query.page) || 1;
|
||||
const limit = Number(context.query.limit) || 10;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
try {
|
||||
const [data, total] = await Promise.all([
|
||||
prisma.prestasiDesa.findMany({
|
||||
where: { isActive: true },
|
||||
include: {
|
||||
image: true,
|
||||
kategori: true,
|
||||
},
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: { createdAt: "desc" }, // opsional, kalau mau urut berdasarkan waktu
|
||||
}),
|
||||
prisma.prestasiDesa.count({
|
||||
where: { isActive: true },
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Success fetch Prestasi Desa with pagination",
|
||||
data,
|
||||
page,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
total,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("Find many paginated error:", e);
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed fetch Prestasi Desa with pagination",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default prestasiDesaFindMany;
|
||||
@@ -0,0 +1,29 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
export default async function prestasiDesaFindUnique(context: Context) {
|
||||
const { params } = context;
|
||||
const id = params?.id as string;
|
||||
|
||||
if (!id) {
|
||||
throw new Error("ID tidak ditemukan dalam parameter");
|
||||
}
|
||||
|
||||
const data = await prisma.prestasiDesa.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
image: true,
|
||||
kategori: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
throw new Error("Prestasi Desa tidak ditemukan");
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Data Prestasi Desa ditemukan",
|
||||
data,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import Elysia, { t } from "elysia";
|
||||
import prestasiDesaCreate from "./create";
|
||||
import prestasiDesaDelete from "./del";
|
||||
import prestasiDesaFindMany from "./findMany";
|
||||
import prestasiDesaFindUnique from "./findUnique";
|
||||
import prestasiDesaUpdate from "./updt";
|
||||
|
||||
const PrestasiDesa = new Elysia({
|
||||
prefix: "/prestasidesa",
|
||||
tags: ["Landing Page/Profile/Prestasi Desa"],
|
||||
})
|
||||
|
||||
// ✅ Find all
|
||||
.get("/find-many", prestasiDesaFindMany)
|
||||
|
||||
// ✅ Find by ID
|
||||
.get("/:id", prestasiDesaFindUnique)
|
||||
|
||||
// ✅ Create
|
||||
.post("/create", prestasiDesaCreate, {
|
||||
body: t.Object({
|
||||
name: t.String(),
|
||||
imageId: t.String(),
|
||||
deskripsi: t.String(),
|
||||
kategoriId: t.String(),
|
||||
}),
|
||||
})
|
||||
|
||||
// ✅ Update
|
||||
.put("/:id", prestasiDesaUpdate, {
|
||||
body: t.Object({
|
||||
name: t.String(),
|
||||
imageId: t.Optional(t.String()),
|
||||
deskripsi: t.Optional(t.String()),
|
||||
kategoriId: t.Optional(t.String()),
|
||||
}),
|
||||
})
|
||||
// ✅ Delete
|
||||
.delete("/del/:id", prestasiDesaDelete);
|
||||
|
||||
export default PrestasiDesa;
|
||||
@@ -0,0 +1,25 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
|
||||
export default async function kategoriPrestasiCreate(context: Context) {
|
||||
const body = context.body as {name: string};
|
||||
|
||||
if (!body.name) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Nama is required",
|
||||
};
|
||||
}
|
||||
|
||||
const kategoriPrestasi = await prisma.kategoriPrestasiDesa.create({
|
||||
data: {
|
||||
name: body.name,
|
||||
},
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
message: "Success create kategori prestasi",
|
||||
data: kategoriPrestasi
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
const kategoriPrestasiDelete = async (context: Context) => {
|
||||
const id = context.params.id;
|
||||
if (!id) {
|
||||
return {
|
||||
success: false,
|
||||
message: "ID is required",
|
||||
}
|
||||
}
|
||||
|
||||
const kategoriPrestasi = await prisma.kategoriPrestasiDesa.delete({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
})
|
||||
|
||||
if(!kategoriPrestasi) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Kategori Prestasi tidak ditemukan",
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Success delete kategori prestasi",
|
||||
data: kategoriPrestasi,
|
||||
}
|
||||
}
|
||||
|
||||
export default kategoriPrestasiDelete
|
||||
@@ -0,0 +1,15 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
export default async function kategoriPrestasiFindMany() {
|
||||
const data = await prisma.kategoriPrestasiDesa.findMany();
|
||||
return {
|
||||
success: true,
|
||||
data: data.map((item: any) => {
|
||||
return {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
}
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Context } from "elysia";
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
export default async function kategoriPrestasiFindUnique(context: Context) {
|
||||
const url = new URL(context.request.url);
|
||||
const pathSegments = url.pathname.split('/');
|
||||
const id = pathSegments[pathSegments.length - 1];
|
||||
|
||||
if (!id) {
|
||||
return {
|
||||
success: false,
|
||||
message: "ID is required",
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof id !== 'string') {
|
||||
return {
|
||||
success: false,
|
||||
message: "ID is required",
|
||||
}
|
||||
}
|
||||
|
||||
const data = await prisma.kategoriPrestasiDesa.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Kategori prestasi tidak ditemukan",
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Success find kategori prestasi",
|
||||
data,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Find by ID error:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "Gagal mengambil kategori prestasi: " + (error instanceof Error ? error.message : 'Unknown error'),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import Elysia, { t } from "elysia";
|
||||
import kategoriPrestasiCreate from "./create";
|
||||
import kategoriPrestasiDelete from "./del";
|
||||
import kategoriPrestasiFindMany from "./findMany";
|
||||
import kategoriPrestasiFindUnique from "./findUnique";
|
||||
import kategoriPrestasiUpdate from "./updt";
|
||||
|
||||
const KategoriPrestasi = new Elysia({
|
||||
prefix: "/kategoriprestasi",
|
||||
tags: ["Lingkungan/Kategori Kegiatan"],
|
||||
})
|
||||
.get("/find-many", kategoriPrestasiFindMany)
|
||||
.get("/:id", async (context) => {
|
||||
const response = await kategoriPrestasiFindUnique(context);
|
||||
return response;
|
||||
})
|
||||
.delete("/del/:id", kategoriPrestasiDelete)
|
||||
.post("/create", kategoriPrestasiCreate, {
|
||||
body: t.Object({
|
||||
name: t.String(),
|
||||
}),
|
||||
})
|
||||
.put("/:id", kategoriPrestasiUpdate, {
|
||||
body: t.Object({
|
||||
name: t.String(),
|
||||
}),
|
||||
});
|
||||
|
||||
export default KategoriPrestasi;
|
||||
@@ -0,0 +1,44 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
export default async function kategoriPrestasiUpdate(context: Context) {
|
||||
const body = context.body as { name: string };
|
||||
const id = context.params?.id as string;
|
||||
|
||||
// Validasi ID dan nama
|
||||
if (!id) {
|
||||
return {
|
||||
success: false,
|
||||
message: "ID is required",
|
||||
};
|
||||
}
|
||||
|
||||
if (!body.name) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Nama is required",
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const kategoriPrestasi = await prisma.kategoriPrestasiDesa.update({
|
||||
where: { id },
|
||||
data: {
|
||||
name: body.name,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Success update kategori prestasi",
|
||||
data: kategoriPrestasi,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Update error:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "Gagal update kategori prestasi",
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
type FormUpdatePrestasiDesa = {
|
||||
name?: string;
|
||||
imageId?: string;
|
||||
deskripsi?: string;
|
||||
kategoriId?: string;
|
||||
};
|
||||
|
||||
export default async function prestasiDesaUpdate(context: Context) {
|
||||
const body = context.body as FormUpdatePrestasiDesa;
|
||||
|
||||
const id = context.params.id;
|
||||
|
||||
if (!id) {
|
||||
return {
|
||||
success: false,
|
||||
message: "ID Prestasi Desa wajib diisi",
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const updated = await prisma.prestasiDesa.update({
|
||||
where: { id },
|
||||
data: {
|
||||
name: body.name,
|
||||
imageId: body.imageId,
|
||||
deskripsi: body.deskripsi,
|
||||
kategoriId: body.kategoriId,
|
||||
},
|
||||
include: {
|
||||
image: true,
|
||||
kategori: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Prestasi Desa berhasil diperbarui",
|
||||
data: updated,
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error("❌ Error update Prestasi Desa:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "Gagal memperbarui data Prestasi Desa",
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
type FormCreate = {
|
||||
name: string;
|
||||
imageId: string;
|
||||
iconUrl: string;
|
||||
};
|
||||
@@ -12,6 +13,7 @@ export default async function mediaSosialCreate(context: Context) {
|
||||
try {
|
||||
const result = await prisma.mediaSosial.create({
|
||||
data: {
|
||||
name: body.name,
|
||||
imageId: body.imageId,
|
||||
iconUrl: body.iconUrl,
|
||||
},
|
||||
|
||||
@@ -19,6 +19,7 @@ const MediaSosial = new Elysia({
|
||||
// ✅ Create
|
||||
.post("/create", MediaSosialCreate, {
|
||||
body: t.Object({
|
||||
name: t.String(),
|
||||
imageId: t.String(),
|
||||
iconUrl: t.String(),
|
||||
}),
|
||||
@@ -27,6 +28,7 @@ const MediaSosial = new Elysia({
|
||||
// ✅ Update
|
||||
.put("/:id", MediaSosialUpdate, {
|
||||
body: t.Object({
|
||||
name: t.String(),
|
||||
imageId: t.Optional(t.String()),
|
||||
iconUrl: t.Optional(t.String()),
|
||||
}),
|
||||
|
||||
@@ -3,6 +3,7 @@ import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
type FormUpdateMediaSosial = {
|
||||
name?: string;
|
||||
imageId?: string;
|
||||
iconUrl?: string;
|
||||
};
|
||||
@@ -23,6 +24,7 @@ export default async function mediaSosialUpdate(context: Context) {
|
||||
const updated = await prisma.mediaSosial.update({
|
||||
where: { id },
|
||||
data: {
|
||||
name: body.name,
|
||||
imageId: body.imageId,
|
||||
iconUrl: body.iconUrl,
|
||||
},
|
||||
|
||||
@@ -1,23 +1,9 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { Context } from "elysia";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
|
||||
type FormUpdate = Prisma.PejabatDesaGetPayload<{
|
||||
select: {
|
||||
id: true;
|
||||
name: true;
|
||||
position: true;
|
||||
imageId: true;
|
||||
};
|
||||
}>;
|
||||
export default async function pejabatDesaFindUnique(context: Context) {
|
||||
try {
|
||||
const id = context.params?.id as string;
|
||||
const body = (await context.body) as Omit<FormUpdate, "id">;
|
||||
|
||||
const { name, position, imageId } = body;
|
||||
|
||||
if (!id) {
|
||||
return new Response(
|
||||
@@ -35,12 +21,8 @@ export default async function pejabatDesaFindUnique(context: Context) {
|
||||
}
|
||||
|
||||
const existing = await prisma.pejabatDesa.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
include: {
|
||||
image: true,
|
||||
},
|
||||
where: { id },
|
||||
include: { image: true },
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
@@ -58,37 +40,11 @@ export default async function pejabatDesaFindUnique(context: Context) {
|
||||
);
|
||||
}
|
||||
|
||||
if (existing.imageId !== imageId) {
|
||||
const oldImage = existing.image;
|
||||
if (oldImage) {
|
||||
try {
|
||||
const filePath = path.join(oldImage.path, oldImage.name);
|
||||
await fs.unlink(filePath);
|
||||
await prisma.fileStorage.delete({
|
||||
where: { id: oldImage.id },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Gagal hapus gambar lama:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updated = await prisma.pejabatDesa.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
name,
|
||||
position,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
message: "Data pejabat desa berhasil ditemukan",
|
||||
data: updated,
|
||||
data: existing,
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
@@ -98,11 +54,11 @@ export default async function pejabatDesaFindUnique(context: Context) {
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error updating pejabat desa:", error);
|
||||
console.error("Error fetching pejabat desa:", error);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
message: "Terjadi kesalahan saat mengupdate pejabat desa",
|
||||
message: "Terjadi kesalahan saat mengambil data pejabat desa",
|
||||
}),
|
||||
{
|
||||
status: 500,
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
type FormCreate = {
|
||||
name: string;
|
||||
jumlah: string;
|
||||
imageId: string;
|
||||
};
|
||||
|
||||
export default async function sdgsDesaCreate(context: Context) {
|
||||
const body = context.body as FormCreate;
|
||||
|
||||
try {
|
||||
const result = await prisma.sDGSDesa.create({
|
||||
data: {
|
||||
name: body.name,
|
||||
jumlah: body.jumlah,
|
||||
imageId: body.imageId,
|
||||
},
|
||||
include: {
|
||||
image: true,
|
||||
},
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
message: "Berhasil membuat SDGS Desa",
|
||||
data: result,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error creating SDGS Desa:", error);
|
||||
throw new Error("Gagal membuat SDGS Desa: " + (error as Error).message);
|
||||
}
|
||||
}
|
||||
21
src/app/api/[[...slugs]]/_lib/landing_page/sdgs-desa/del.ts
Normal file
21
src/app/api/[[...slugs]]/_lib/landing_page/sdgs-desa/del.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
export default async function sdgsDesaDelete(context: Context) {
|
||||
const { params } = context;
|
||||
const id = params?.id as string;
|
||||
|
||||
if (!id) {
|
||||
throw new Error("ID tidak ditemukan dalam parameter");
|
||||
}
|
||||
|
||||
const deleted = await prisma.sDGSDesa.delete({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Berhasil menghapus SDGS Desa",
|
||||
data: deleted,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// /api/berita/findManyPaginated.ts
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
async function sdgsDesaFindMany(context: Context) {
|
||||
const page = Number(context.query.page) || 1;
|
||||
const limit = Number(context.query.limit) || 10;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
try {
|
||||
const [data, total] = await Promise.all([
|
||||
prisma.sDGSDesa.findMany({
|
||||
where: { isActive: true },
|
||||
include: {
|
||||
image: true,
|
||||
},
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: { createdAt: "desc" }, // opsional, kalau mau urut berdasarkan waktu
|
||||
}),
|
||||
prisma.sDGSDesa.count({
|
||||
where: { isActive: true },
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Success fetch SDGS Desa with pagination",
|
||||
data,
|
||||
page,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
total,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("Find many paginated error:", e);
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed fetch SDGS Desa with pagination",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default sdgsDesaFindMany;
|
||||
@@ -0,0 +1,28 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
export default async function sdgsDesaFindUnique(context: Context) {
|
||||
const { params } = context;
|
||||
const id = params?.id as string;
|
||||
|
||||
if (!id) {
|
||||
throw new Error("ID tidak ditemukan dalam parameter");
|
||||
}
|
||||
|
||||
const data = await prisma.sDGSDesa.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
image: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
throw new Error("SDGS Desa tidak ditemukan");
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Data SDGS Desa ditemukan",
|
||||
data,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import Elysia, { t } from "elysia";
|
||||
import sdgsDesaCreate from "./create";
|
||||
import sdgsDesaDelete from "./del";
|
||||
import sdgsDesaFindMany from "./findMany";
|
||||
import sdgsDesaFindUnique from "./findUnique";
|
||||
import sdgsDesaUpdate from "./updt";
|
||||
|
||||
const SDGSDesa = new Elysia({
|
||||
prefix: "/sdgsdesa",
|
||||
tags: ["Landing Page/Profile/SDGS Desa"],
|
||||
})
|
||||
|
||||
// ✅ Find all
|
||||
.get("/find-many", sdgsDesaFindMany)
|
||||
|
||||
// ✅ Find by ID
|
||||
.get("/:id", sdgsDesaFindUnique)
|
||||
|
||||
// ✅ Create
|
||||
.post("/create", sdgsDesaCreate, {
|
||||
body: t.Object({
|
||||
name: t.String(),
|
||||
imageId: t.String(),
|
||||
jumlah: t.String(),
|
||||
}),
|
||||
})
|
||||
|
||||
// ✅ Update
|
||||
.put("/:id", sdgsDesaUpdate, {
|
||||
body: t.Object({
|
||||
name: t.String(),
|
||||
imageId: t.Optional(t.String()),
|
||||
jumlah: t.Optional(t.String()),
|
||||
}),
|
||||
})
|
||||
// ✅ Delete
|
||||
.delete("/del/:id", sdgsDesaDelete);
|
||||
|
||||
export default SDGSDesa;
|
||||
49
src/app/api/[[...slugs]]/_lib/landing_page/sdgs-desa/updt.ts
Normal file
49
src/app/api/[[...slugs]]/_lib/landing_page/sdgs-desa/updt.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
type FormUpdateSDGSDesa = {
|
||||
name?: string;
|
||||
imageId?: string;
|
||||
jumlah?: string;
|
||||
};
|
||||
|
||||
export default async function sdgsDesaUpdate(context: Context) {
|
||||
const body = context.body as FormUpdateSDGSDesa;
|
||||
|
||||
const id = context.params.id;
|
||||
|
||||
if (!id) {
|
||||
return {
|
||||
success: false,
|
||||
message: "ID SDGS Desa wajib diisi",
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const updated = await prisma.sDGSDesa.update({
|
||||
where: { id },
|
||||
data: {
|
||||
name: body.name,
|
||||
imageId: body.imageId,
|
||||
jumlah: body.jumlah,
|
||||
},
|
||||
include: {
|
||||
image: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "SDGS Desa berhasil diperbarui",
|
||||
data: updated,
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error("❌ Error update SDGS Desa:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "Gagal memperbarui data SDGS Desa",
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ export function Navbar() {
|
||||
}}
|
||||
>
|
||||
<NavbarMainMenu listNavbar={navbarListMenu} />
|
||||
|
||||
<Stack hiddenFrom="sm" bg={colors.grey[2]}>
|
||||
<Group justify="space-between">
|
||||
<ActionIcon variant="transparent" onClick={() => {
|
||||
@@ -50,6 +51,7 @@ export function Navbar() {
|
||||
<NavbarMobile listNavbar={navbarListMenu} />
|
||||
</motion.div>}
|
||||
</Stack>
|
||||
|
||||
</Box>
|
||||
{(item || isSearch) && <Box className="glass" />}
|
||||
</Box>
|
||||
|
||||
@@ -4,18 +4,20 @@ import colors from "@/con/colors"
|
||||
import stateNav from "@/state/state-nav"
|
||||
import { ActionIcon, Button, Container, Flex, Image, Stack } from "@mantine/core"
|
||||
import { useHover } from "@mantine/hooks"
|
||||
import { IconSearch, IconUser } from "@tabler/icons-react"
|
||||
import { useTransitionRouter } from 'next-view-transitions'
|
||||
import { useSnapshot } from "valtio"
|
||||
import { MenuItem } from "../../../../types/menu-item"
|
||||
import { NavbarSearch } from "./NavBarSearch"
|
||||
import { NavbarSubMenu } from "./NavbarSubMenu"
|
||||
import { IconSearch } from "@tabler/icons-react"
|
||||
import { useRouter } from "next/navigation"
|
||||
|
||||
export function NavbarMainMenu({ listNavbar }: {
|
||||
listNavbar: MenuItem[]
|
||||
}) {
|
||||
const { item, isSearch } = useSnapshot(stateNav)
|
||||
const router = useTransitionRouter()
|
||||
const next = useRouter()
|
||||
return <Stack gap={0} visibleFrom="sm" bg={colors["white-trans-1"]}>
|
||||
<Container pos={"relative"} w={{
|
||||
base: '100%',
|
||||
@@ -28,7 +30,7 @@ export function NavbarMainMenu({ listNavbar }: {
|
||||
<ActionIcon radius={"100"} variant="transparent" onClick={() => {
|
||||
router.push("/darmasaba")
|
||||
stateNav.clear()
|
||||
|
||||
|
||||
}} >
|
||||
<Image radius={"100"} src={"/assets/images/darmasaba-icon.png"} alt="icon" w={24} h={24} loading="lazy" />
|
||||
</ActionIcon>
|
||||
@@ -44,6 +46,11 @@ export function NavbarMainMenu({ listNavbar }: {
|
||||
{/* TODO: add icon search */}
|
||||
<IconSearch size={"1.5rem"} />
|
||||
</ActionIcon>
|
||||
<ActionIcon onClick={() => {
|
||||
next.push("/admin/landing-page/profile/program-inovasi")
|
||||
}} color={colors["blue-button"]} radius={'xl'}>
|
||||
<IconUser size={24} />
|
||||
</ActionIcon>
|
||||
</Flex>
|
||||
</Container>
|
||||
{item && <NavbarSubMenu item={item as MenuItem[]} />}
|
||||
|
||||
@@ -14,7 +14,7 @@ function Page() {
|
||||
|
||||
const loadListFile = async () => {
|
||||
const { data } = await ApiFetch.api.fileStorage.findMany.get()
|
||||
setListFile(data?.map((item) => item.link) || [])
|
||||
setListFile(data?.data?.map((item) => item.link) || [])
|
||||
}
|
||||
useShallowEffect(() => {
|
||||
loadListFile()
|
||||
|
||||
@@ -2,7 +2,7 @@ const navbarListMenu = [
|
||||
{
|
||||
id: "1",
|
||||
name: "PPID",
|
||||
href: "/darmasaba/ppid",
|
||||
href: "/darmasaba/ppid/profile-ppid",
|
||||
children: [
|
||||
{
|
||||
id: "1.1",
|
||||
@@ -51,7 +51,7 @@ const navbarListMenu = [
|
||||
{
|
||||
id: "2",
|
||||
name: "Desa",
|
||||
href: "/darmasaba/desa",
|
||||
href: "/darmasaba/desa/profile",
|
||||
children: [
|
||||
{
|
||||
id: "2.1",
|
||||
@@ -94,7 +94,7 @@ const navbarListMenu = [
|
||||
{
|
||||
id: "3",
|
||||
name: "Kesehatan",
|
||||
href: "/darmasaba/kesehatan",
|
||||
href: "/darmasaba/kesehatan/posyandu",
|
||||
children: [
|
||||
{
|
||||
id: "3.1",
|
||||
@@ -136,7 +136,7 @@ const navbarListMenu = [
|
||||
{
|
||||
id: "4",
|
||||
name: "Keamanan",
|
||||
href: "/darmasaba/keamanan",
|
||||
href: "/darmasaba/keamanan/keamanan-lingkungan-pecalang-patwal",
|
||||
children: [
|
||||
{
|
||||
id: "4.1",
|
||||
@@ -173,7 +173,7 @@ const navbarListMenu = [
|
||||
{
|
||||
id: "5",
|
||||
name: "Ekonomi",
|
||||
href: "/darmasaba/ekonomi",
|
||||
href: "/darmasaba/ekonomi/pasar-desa",
|
||||
children: [
|
||||
{
|
||||
id: "5.1",
|
||||
@@ -229,7 +229,7 @@ const navbarListMenu = [
|
||||
}, {
|
||||
id: "6",
|
||||
name: "Inovasi",
|
||||
href: "/darmasaba/inovasi",
|
||||
href: "/darmasaba/inovasi/desa-digital-smart-village",
|
||||
children: [
|
||||
{
|
||||
id: "6.1",
|
||||
@@ -266,7 +266,7 @@ const navbarListMenu = [
|
||||
}, {
|
||||
id: "7",
|
||||
name: "Lingkungan",
|
||||
href: "/darmasaba/lingkungan",
|
||||
href: "/darmasaba/lingkungan/pengelolaan-sampah-bank-sampah",
|
||||
children: [
|
||||
{
|
||||
id: "7.1",
|
||||
@@ -302,7 +302,7 @@ const navbarListMenu = [
|
||||
}, {
|
||||
id: "8",
|
||||
name: "Pendidikan",
|
||||
href: "/darmasaba/pendidikan",
|
||||
href: "/darmasaba/pendidikan/info-sekolah-paud",
|
||||
children: [
|
||||
{
|
||||
id: "8.1",
|
||||
|
||||
Reference in New Issue
Block a user