API & UI Menu Landing Page, Submenu Prestasi Desa
This commit is contained in:
@@ -95,6 +95,8 @@ model FileStorage {
|
|||||||
SDGSDesa SDGSDesa[]
|
SDGSDesa SDGSDesa[]
|
||||||
APBDesImage APBDes[] @relation("APBDesImage")
|
APBDesImage APBDes[] @relation("APBDesImage")
|
||||||
APBDesFile APBDes[] @relation("APBDesFile")
|
APBDesFile APBDes[] @relation("APBDesFile")
|
||||||
|
|
||||||
|
PrestasiDesa PrestasiDesa[]
|
||||||
}
|
}
|
||||||
|
|
||||||
//========================================= MENU LANDING PAGE ========================================= //
|
//========================================= MENU LANDING PAGE ========================================= //
|
||||||
@@ -189,6 +191,31 @@ model APBDes {
|
|||||||
isActive Boolean @default(true)
|
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 ========================================= //
|
//========================================= MENU PPID ========================================= //
|
||||||
|
|
||||||
//========================================= STRUKTUR PPID ========================================= //
|
//========================================= STRUKTUR PPID ========================================= //
|
||||||
|
|||||||
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;
|
||||||
@@ -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;
|
|
||||||
@@ -32,7 +32,7 @@ export const navBar = [
|
|||||||
{
|
{
|
||||||
id: "Landing_Page_6",
|
id: "Landing_Page_6",
|
||||||
name: "Prestasi Desa",
|
name: "Prestasi Desa",
|
||||||
path: "/admin/landing-page/prestasi-desa"
|
path: "/admin/landing-page/prestasi-desa/list-prestasi-desa"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import KategoriDesaAntiKorupsi from "./desa-anti-korupsi/kategori-dak";
|
|||||||
import DesaAntiKorupsi from "./desa-anti-korupsi";
|
import DesaAntiKorupsi from "./desa-anti-korupsi";
|
||||||
import SDGSDesa from "./sdgs-desa";
|
import SDGSDesa from "./sdgs-desa";
|
||||||
import APBDes from "./apbdes";
|
import APBDes from "./apbdes";
|
||||||
|
import PrestasiDesa from "./prestasi-desa";
|
||||||
|
import KategoriPrestasi from "./prestasi-desa/kategori-prestasi";
|
||||||
|
|
||||||
const LandingPage = new Elysia({
|
const LandingPage = new Elysia({
|
||||||
prefix: "/api/landingpage",
|
prefix: "/api/landingpage",
|
||||||
@@ -19,5 +21,7 @@ const LandingPage = new Elysia({
|
|||||||
.use(DesaAntiKorupsi)
|
.use(DesaAntiKorupsi)
|
||||||
.use(SDGSDesa)
|
.use(SDGSDesa)
|
||||||
.use(APBDes)
|
.use(APBDes)
|
||||||
|
.use(PrestasiDesa)
|
||||||
|
.use(KategoriPrestasi)
|
||||||
|
|
||||||
export default LandingPage
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user