Compare commits

...

4 Commits

101 changed files with 6414 additions and 2993 deletions

View File

@@ -269,7 +269,7 @@ const keteranganSampah = proxy({
try { try {
keteranganSampah.create.loading = true; keteranganSampah.create.loading = true;
const res = const res =
await ApiFetch.api.lingkungan.pengelolaansampah.keteranganbankterdekat[ await ApiFetch.api.lingkungan.keteranganbankterdekat[
"create" "create"
].post(keteranganSampah.create.form); ].post(keteranganSampah.create.form);
if (res.status === 200) { if (res.status === 200) {
@@ -291,14 +291,47 @@ const keteranganSampah = proxy({
omit: { isActive: true }; omit: { isActive: true };
}>[] }>[]
| null, | null,
async load() { page: 1,
const res = await ApiFetch.api.lingkungan.pengelolaansampah.keteranganbankterdekat[ totalPages: 1,
"find-many" total: 0,
].get(); loading: false,
if (res.status === 200) { search: "",
keteranganSampah.findMany.data = res.data?.data ?? []; load: async (page = 1, limit = 10, search = "") => {
} // Change to arrow function
}, keteranganSampah.findMany.loading = true; // Use the full path to access the property
keteranganSampah.findMany.page = page;
keteranganSampah.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.lingkungan.keteranganbankterdekat[
"find-many"
].get({
query,
});
if (res.status === 200 && res.data?.success) {
keteranganSampah.findMany.data = res.data.data || [];
keteranganSampah.findMany.total = res.data.total || 0;
keteranganSampah.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error(
"Failed to load keterangan bank sampah terdekat:",
res.data?.message
);
keteranganSampah.findMany.data = [];
keteranganSampah.findMany.total = 0;
keteranganSampah.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading keterangan bank sampah terdekat:", error);
keteranganSampah.findMany.data = [];
keteranganSampah.findMany.total = 0;
keteranganSampah.findMany.totalPages = 1;
} finally {
keteranganSampah.findMany.loading = false;
}
},
}, },
findUnique: { findUnique: {
data: null as Prisma.KeteranganBankSampahTerdekatGetPayload<{ data: null as Prisma.KeteranganBankSampahTerdekatGetPayload<{
@@ -306,7 +339,7 @@ const keteranganSampah = proxy({
}> | null, }> | null,
async load(id: string) { async load(id: string) {
try { try {
const res = await fetch(`/api/lingkungan/pengelolaansampah/keteranganbankterdekat/${id}`); const res = await fetch(`/api/lingkungan/keteranganbankterdekat/${id}`);
if (res.ok) { if (res.ok) {
const data = await res.json(); const data = await res.json();
keteranganSampah.findUnique.data = data.data ?? null; keteranganSampah.findUnique.data = data.data ?? null;
@@ -328,7 +361,7 @@ const keteranganSampah = proxy({
try { try {
keteranganSampah.delete.loading = true; keteranganSampah.delete.loading = true;
const response = await fetch(`/api/lingkungan/pengelolaansampah/keteranganbankterdekat/del/${id}`, { const response = await fetch(`/api/lingkungan/keteranganbankterdekat/del/${id}`, {
method: "DELETE", method: "DELETE",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -363,7 +396,7 @@ const keteranganSampah = proxy({
} }
try { try {
const response = await fetch(`/api/lingkungan/pengelolaansampah/keteranganbankterdekat/${id}`, { const response = await fetch(`/api/lingkungan/keteranganbankterdekat/${id}`, {
method: "GET", method: "GET",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -408,7 +441,7 @@ const keteranganSampah = proxy({
try { try {
keteranganSampah.edit.loading = true; keteranganSampah.edit.loading = true;
const response = await fetch( const response = await fetch(
`/api/lingkungan/pengelolaansampah/keteranganbankterdekat/${this.id}`, `/api/lingkungan/keteranganbankterdekat/${this.id}`,
{ {
method: "PUT", method: "PUT",
headers: { headers: {

View File

@@ -1,9 +1,12 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch"; import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { proxy } from "valtio"; import { proxy } from "valtio";
import { z } from "zod"; import { z } from "zod";
// ========================================= BEASISWA PENDAFTAR ========================================= //
const templateBeasiswaPendaftar = z.object({ const templateBeasiswaPendaftar = z.object({
namaLengkap: z.string().min(1, "Nama harus diisi"), namaLengkap: z.string().min(1, "Nama harus diisi"),
nik: z.string().min(1, "NIK harus diisi"), nik: z.string().min(1, "NIK harus diisi"),
@@ -76,13 +79,34 @@ const beasiswaPendaftar = proxy({
isActive: true; isActive: true;
}; };
}>[], }>[],
page: 1,
totalPages: 1,
loading: false, loading: false,
async load() { search: "",
const res = await ApiFetch.api.pendidikan.beasiswa.beasiswapendaftar[ load: async (page = 1, limit = 10, search = "") => {
"findMany" beasiswaPendaftar.findMany.loading = true; // ✅ Akses langsung via nama path
].get(); beasiswaPendaftar.findMany.page = page;
if (res.status === 200) { beasiswaPendaftar.findMany.search = search;
beasiswaPendaftar.findMany.data = res.data?.data ?? [];
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.pendidikan.beasiswa.beasiswapendaftar["findMany"].get({ query });
if (res.status === 200 && res.data?.success) {
beasiswaPendaftar.findMany.data = res.data.data ?? [];
beasiswaPendaftar.findMany.totalPages = res.data.totalPages ?? 1;
} else {
beasiswaPendaftar.findMany.data = [];
beasiswaPendaftar.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch beasiswa pendaftar paginated:", err);
beasiswaPendaftar.findMany.data = [];
beasiswaPendaftar.findMany.totalPages = 1;
} finally {
beasiswaPendaftar.findMany.loading = false;
} }
}, },
}, },
@@ -275,8 +299,260 @@ const beasiswaPendaftar = proxy({
}, },
}); });
// ========================================= KEUNGGULAN PROGRAM ========================================= //
const templateKeunggulanProgram = z.object({
judul: z.string().min(1, "Judul harus diisi"),
deskripsi: z.string().min(1, "Deskripsi harus diisi"),
});
const defaultKeunggulanProgram = {
judul: "",
deskripsi: "",
};
const keunggulanProgram = proxy({
create: {
form: { ...defaultKeunggulanProgram },
loading: false,
async create() {
const cek = templateKeunggulanProgram.safeParse(
keunggulanProgram.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
keunggulanProgram.create.loading = true;
const res = await ApiFetch.api.pendidikan.beasiswa.keunggulanprogram[
"create"
].post(keunggulanProgram.create.form);
if (res.status === 200) {
keunggulanProgram.findMany.load();
return toast.success("Data Berhasil Dibuat, Silahkan Menunggu Konfirmasi dari Admin di WhatsApp");
}
console.log(res);
return toast.error("failed create");
} catch (error) {
console.log(error);
return toast.error("failed create");
} finally {
keunggulanProgram.create.loading = false;
}
},
},
findMany: {
data: [] as Prisma.KeunggulanProgramGetPayload<{
omit: {
isActive: true;
};
}>[],
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
keunggulanProgram.findMany.loading = true; // ✅ Akses langsung via nama path
keunggulanProgram.findMany.page = page;
keunggulanProgram.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.pendidikan.beasiswa.keunggulanprogram["findMany"].get({ query });
if (res.status === 200 && res.data?.success) {
keunggulanProgram.findMany.data = res.data.data ?? [];
keunggulanProgram.findMany.totalPages = res.data.totalPages ?? 1;
} else {
keunggulanProgram.findMany.data = [];
keunggulanProgram.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch keunggulan program paginated:", err);
keunggulanProgram.findMany.data = [];
keunggulanProgram.findMany.totalPages = 1;
} finally {
keunggulanProgram.findMany.loading = false;
}
},
},
findUnique: {
data: null as Prisma.KeunggulanProgramGetPayload<{
omit: {
isActive: true;
};
}> | null,
loading: false,
async load(id: string) {
try {
const res = await fetch(
`/api/pendidikan/beasiswa/keunggulanprogram/${id}`
);
if (res.ok) {
const data = await res.json();
keunggulanProgram.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
keunggulanProgram.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
keunggulanProgram.findUnique.data = null;
}
},
},
delete: {
loading: false,
async delete(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
keunggulanProgram.delete.loading = true;
const response = await fetch(
`/api/pendidikan/beasiswa/keunggulanprogram/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Keunggulan Program berhasil dihapus");
await keunggulanProgram.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus keunggulan program");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus keunggulan program");
} finally {
keunggulanProgram.delete.loading = false;
}
},
},
update: {
id: "",
form: { ...defaultKeunggulanProgram },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(
`/api/pendidikan/beasiswa/keunggulanprogram/${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 = {
judul: data.judul,
deskripsi: data.deskripsi,
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading keunggulan program:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateKeunggulanProgram.safeParse(
keunggulanProgram.update.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
keunggulanProgram.update.loading = true;
const response = await fetch(
`/api/pendidikan/beasiswa/keunggulanprogram/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
judul: this.form.judul,
deskripsi: this.form.deskripsi,
}),
}
);
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 keunggulan program");
await keunggulanProgram.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal update keunggulan program");
}
} catch (error) {
console.error("Error updating keunggulan program:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update keunggulan program"
);
return false;
} finally {
keunggulanProgram.update.loading = false;
}
},
reset() {
keunggulanProgram.update.id = "";
keunggulanProgram.update.form = { ...defaultKeunggulanProgram };
},
},
});
const beasiswaDesaState = proxy({ const beasiswaDesaState = proxy({
beasiswaPendaftar, beasiswaPendaftar,
keunggulanProgram
}); });
export default beasiswaDesaState; export default beasiswaDesaState;

View File

@@ -333,43 +333,56 @@ const lembagaPendidikan = proxy({
Prisma.LembagaGetPayload<{ Prisma.LembagaGetPayload<{
include: { include: {
jenjangPendidikan: true; jenjangPendidikan: true;
siswa: true;
pengajar: true;
}; };
}> }> & {
siswa?: [];
pengajar?: [];
}
> | null, > | null,
page: 1, page: 1,
totalPages: 1, totalPages: 1,
total: 0, total: 0,
loading: false, loading: false,
search: "", search: "",
load: async (page = 1, limit = 10, search = "") => { load: async (page = 1, limit = 10, search = "", jenjangPendidikan = "") => {
// Change to arrow function lembagaPendidikan.findMany.loading = true;
lembagaPendidikan.findMany.loading = true; // Use the full path to access the property
lembagaPendidikan.findMany.page = page; lembagaPendidikan.findMany.page = page;
lembagaPendidikan.findMany.search = search; lembagaPendidikan.findMany.search = search;
try { try {
const query: any = { page, limit }; const query: any = {
if (search) query.search = search; page,
const res = limit,
await ApiFetch.api.pendidikan.infosekolahpaud.lembagapendidikan[ ...(search && { search }),
"find-many" ...(jenjangPendidikan && { jenjangPendidikanId: jenjangPendidikan })
].get({ };
query,
}); console.log('Fetching lembaga with query:', query);
const res = await ApiFetch.api.pendidikan.infosekolahpaud.lembagapendidikan["find-many"].get({ query });
console.log('API Response:', res);
if (res.status === 200 && res.data?.success) { if (res.status === 200 && res.data?.success) {
lembagaPendidikan.findMany.data = res.data.data || []; const data = Array.isArray(res.data.data) ? res.data.data : [];
lembagaPendidikan.findMany.total = res.data.total || 0; const total = typeof res.data.total === 'number' ? res.data.total : 0;
lembagaPendidikan.findMany.totalPages = res.data.totalPages || 1; const totalPages = typeof res.data.totalPages === 'number' ? res.data.totalPages : 1;
lembagaPendidikan.findMany.data = data;
lembagaPendidikan.findMany.total = total;
lembagaPendidikan.findMany.totalPages = totalPages;
console.log('Successfully loaded lembaga data:', {
count: data.length,
total,
totalPages
});
} else { } else {
console.error( console.error(
"Failed to load lembaga pendidikan:", "Failed to load lembaga pendidikan:",
res.data?.message res.data?.message || 'No error message provided'
); );
lembagaPendidikan.findMany.data = []; throw new Error(res.data?.message || 'Failed to load lembaga pendidikan');
lembagaPendidikan.findMany.total = 0;
lembagaPendidikan.findMany.totalPages = 1;
} }
} catch (error) { } catch (error) {
console.error("Error loading lembaga pendidikan:", error); console.error("Error loading lembaga pendidikan:", error);
@@ -621,7 +634,11 @@ const siswa = proxy({
data: null as Array< data: null as Array<
Prisma.SiswaGetPayload<{ Prisma.SiswaGetPayload<{
include: { include: {
lembaga: true; lembaga: {
include: {
jenjangPendidikan: true;
};
};
}; };
}> }>
> | null, > | null,
@@ -630,14 +647,16 @@ const siswa = proxy({
total: 0, total: 0,
loading: false, loading: false,
search: "", search: "",
load: async (page = 1, limit = 10, search = "") => { jenjangPendidikan: "",
// Change to arrow function load: async (page = 1, limit = 10, search = "", jenjangPendidikan = "") => {
siswa.findMany.loading = true; // Use the full path to access the property siswa.findMany.loading = true;
siswa.findMany.page = page; siswa.findMany.page = page;
siswa.findMany.search = search; siswa.findMany.search = search;
siswa.findMany.jenjangPendidikan = jenjangPendidikan;
try { try {
const query: any = { page, limit }; const query: any = { page, limit };
if (search) query.search = search; if (search) query.search = search;
if (jenjangPendidikan) query.jenjangPendidikanName = jenjangPendidikan;
const res = await ApiFetch.api.pendidikan.infosekolahpaud.siswa[ const res = await ApiFetch.api.pendidikan.infosekolahpaud.siswa[
"find-many" "find-many"
].get({ ].get({
@@ -894,7 +913,11 @@ const pengajar = proxy({
data: null as Array< data: null as Array<
Prisma.PengajarGetPayload<{ Prisma.PengajarGetPayload<{
include: { include: {
lembaga: true; lembaga: {
include: {
jenjangPendidikan: true
}
}
}; };
}> }>
> | null, > | null,
@@ -903,14 +926,17 @@ const pengajar = proxy({
total: 0, total: 0,
loading: false, loading: false,
search: "", search: "",
load: async (page = 1, limit = 10, search = "") => { jenjangPendidikan: "",
load: async (page = 1, limit = 10, search = "", jenjangPendidikan = "") => {
// Change to arrow function // Change to arrow function
pengajar.findMany.loading = true; // Use the full path to access the property pengajar.findMany.loading = true; // Use the full path to access the property
pengajar.findMany.page = page; pengajar.findMany.page = page;
pengajar.findMany.search = search; pengajar.findMany.search = search;
pengajar.findMany.jenjangPendidikan = jenjangPendidikan;
try { try {
const query: any = { page, limit }; const query: any = { page, limit };
if (search) query.search = search; if (search) query.search = search;
if (jenjangPendidikan) query.jenjangPendidikanId = jenjangPendidikan;
const res = await ApiFetch.api.pendidikan.infosekolahpaud.pengajar[ const res = await ApiFetch.api.pendidikan.infosekolahpaud.pengajar[
"find-many" "find-many"
].get({ ].get({

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch"; import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
@@ -54,23 +55,46 @@ const dataPerpustakaan = proxy({
}, },
}, },
findMany: { findMany: {
data: [] as Prisma.DataPerpustakaanGetPayload<{ data: null as
include: { | Prisma.DataPerpustakaanGetPayload<{
kategori: true; include: {
image: true; image: true;
}; kategori: true;
}>[], };
loading: false, }>[]
async load() { | null,
const res = page: 1,
await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan[ totalPages: 1,
"findMany" loading: false,
].get(); search: "",
if (res.status === 200) { load: async (page = 1, limit = 10, search = "", kategori = "") => {
dataPerpustakaan.findMany.data = res.data?.data ?? []; dataPerpustakaan.findMany.loading = true; // ✅ Akses langsung via nama path
} dataPerpustakaan.findMany.page = page;
dataPerpustakaan.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
if (kategori) query.kategori = kategori;
const res = await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan["findMany"].get({ query });
if (res.status === 200 && res.data?.success) {
dataPerpustakaan.findMany.data = res.data.data ?? [];
dataPerpustakaan.findMany.totalPages = res.data.totalPages ?? 1;
} else {
dataPerpustakaan.findMany.data = [];
dataPerpustakaan.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch data perpustakaan paginated:", err);
dataPerpustakaan.findMany.data = [];
dataPerpustakaan.findMany.totalPages = 1;
} finally {
dataPerpustakaan.findMany.loading = false;
}
},
}, },
},
findUnique: { findUnique: {
data: null as Prisma.DataPerpustakaanGetPayload<{ data: null as Prisma.DataPerpustakaanGetPayload<{
include: { include: {

View File

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

View File

@@ -2,14 +2,14 @@
'use client' 'use client'
import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi'; import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Paper, Stack, TextInput, Title, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function EditKategoriDesaAntiKorupsi() { export default function EditKategoriDesaAntiKorupsi() {
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
const id = params?.id as string; const id = params?.id as string;
@@ -18,16 +18,17 @@ function EditKategoriDesaAntiKorupsi() {
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: "", name: "",
}); });
const [isLoading, setIsLoading] = useState(false);
useEffect(() => { useEffect(() => {
const loadKategorikegiatan = async () => { const loadKategori = async () => {
if (!id) return; if (!id) return;
setIsLoading(true);
try { try {
const data = await stateKategori.edit.load(id); const data = await stateKategori.edit.load(id);
if (data) { if (data) {
// pastikan id-nya masuk ke state edit
stateKategori.edit.id = id; stateKategori.edit.id = id;
setFormData({ setFormData({
name: data.name || '', name: data.name || '',
@@ -36,63 +37,88 @@ function EditKategoriDesaAntiKorupsi() {
} catch (error) { } catch (error) {
console.error("Error loading kategori desa anti korupsi:", error); console.error("Error loading kategori desa anti korupsi:", error);
toast.error("Gagal memuat data kategori desa anti korupsi"); toast.error("Gagal memuat data kategori desa anti korupsi");
} finally {
setIsLoading(false);
} }
}; };
loadKategorikegiatan(); loadKategori();
}, [id]); }, [id]);
const handleSubmit = async () => { const handleSubmit = async () => {
try { if (!formData.name.trim()) {
if (!formData.name.trim()) { return toast.error('Nama kategori tidak boleh kosong');
toast.error('Nama kategori desa anti korupsi tidak boleh kosong'); }
return;
}
try {
setIsLoading(true);
stateKategori.edit.form = { stateKategori.edit.form = {
name: formData.name.trim(), name: formData.name.trim(),
}; };
// Safety check tambahan: pastikan ID tidak kosong
if (!stateKategori.edit.id) { if (!stateKategori.edit.id) {
stateKategori.edit.id = id; // fallback stateKategori.edit.id = id;
} }
const success = await stateKategori.edit.update(); await stateKategori.edit.update();
toast.success('Kategori berhasil diperbarui');
if (success) { router.push("/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi");
router.push("/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi");
}
} catch (error) { } catch (error) {
console.error("Error updating kategori desa anti korupsi:", error); console.error("Error updating kategori desa anti korupsi:", error);
// toast akan ditampilkan dari fungsi update toast.error(error instanceof Error ? error.message : 'Gagal memperbarui kategori');
} finally {
setIsLoading(false);
} }
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> <Group mb="md">
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Tooltip label="Kembali ke halaman sebelumnya" color="blue" withArrow>
<IconArrowBack color={colors['blue-button']} size={25} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Button> <IconArrowBack color={colors['blue-button']} size={24} />
</Box> </Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Kategori Desa Anti Korupsi
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Paper
<Stack gap={"xs"}> w={{ base: '100%', md: '50%' }}
<Title order={4}>Edit Kategori Desa Anti Korupsi</Title> bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label="Nama Kategori"
placeholder="Masukkan nama kategori"
value={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Desa Anti Korupsi</Text>} required
placeholder='Masukkan nama kategori desa anti korupsi' disabled={isLoading}
/> />
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button> <Group justify="right" mt="md">
<Button
onClick={handleSubmit}
radius="md"
size="md"
loading={isLoading}
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>
); );
} }
export default EditKategoriDesaAntiKorupsi;

View File

@@ -1,16 +1,16 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Paper, Stack, TextInput, Title, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import korupsiState from '../../../../_state/landing-page/desa-anti-korupsi'; import korupsiState from '../../../../_state/landing-page/desa-anti-korupsi';
function CreateKategoriDesaAntiKorupsi() { export default function CreateKategoriDesaAntiKorupsi() {
const router = useRouter(); const router = useRouter();
const stateKategori = useProxy(korupsiState.kategoriDesaAntiKorupsi) const stateKategori = useProxy(korupsiState.kategoriDesaAntiKorupsi);
useEffect(() => { useEffect(() => {
stateKategori.findMany.load(); stateKategori.findMany.load();
@@ -20,42 +20,64 @@ function CreateKategoriDesaAntiKorupsi() {
stateKategori.create.form = { stateKategori.create.form = {
name: "", name: "",
}; };
} };
const handleSubmit = async () => { const handleSubmit = async () => {
if (!stateKategori.create.form.name) {
return alert('Nama kategori harus diisi');
}
await stateKategori.create.create(); await stateKategori.create.create();
resetForm(); resetForm();
router.push("/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi") router.push("/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi");
} };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box> <Group mb="md">
<Box mb={10}> <Tooltip label="Kembali ke halaman sebelumnya" color="blue" withArrow>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
</Box> </Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Kategori Desa Anti Korupsi
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Paper
<Stack gap={"xs"}> w={{ base: '100%', md: '50%' }}
<Title order={4}>Create Kategori Desa Anti Korupsi</Title> bg={colors['white-1']}
<TextInput p="lg"
value={stateKategori.create.form.name} radius="md"
onChange={(val) => { shadow="sm"
stateKategori.create.form.name = val.target.value; style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label="Nama Kategori"
placeholder="Masukkan nama kategori"
value={stateKategori.create.form.name || ''}
onChange={(e) => (stateKategori.create.form.name = e.target.value)}
required
/>
<Group justify="right" mt="md">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Desa Anti Korupsi</Text>} >
placeholder='Masukkan nama kategori desa anti korupsi' Simpan
/> </Button>
<Group> </Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button> </Stack>
</Group> </Paper>
</Stack>
</Paper>
</Box>
</Box> </Box>
); );
} }
export default CreateKategoriDesaAntiKorupsi;

View File

@@ -1,13 +1,12 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core'; import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconX } from '@tabler/icons-react'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import korupsiState from '../../../_state/landing-page/desa-anti-korupsi'; import korupsiState from '../../../_state/landing-page/desa-anti-korupsi';
@@ -56,74 +55,84 @@ function ListKategoriKegiatan({ search }: { search: string }) {
const filteredData = data || [] const filteredData = data || []
// Handle loading state
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton height={550} /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
); );
} }
if (data.length === 0) {
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'
/>
<Box style={{ overflowX: "auto" }}>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
</Table>
</Box>
</Paper>
</Box>
);
}
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<JudulList <Group justify="space-between" mb="md">
title='List Kategori Kegiatan' <Title order={4}>Daftar Kategori Kegiatan</Title>
href='/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/create' <Tooltip label="Tambah Kategori" withArrow>
/> <Button
<Box style={{ overflowY: "auto" }}> leftSection={<IconPlus size={18} />}
<Table striped withTableBorder withRowBorders> color="blue"
variant="light"
onClick={() => router.push('/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Nama Kategori</TableTh> <TableTh>Nama Kategori</TableTh>
<TableTh>Edit</TableTh> <TableTh>Edit</TableTh>
<TableTh>Delete</TableTh> <TableTh>Hapus</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item) => ( {filteredData.length > 0 ? (
<TableTr key={item.id}> filteredData.map((item) => (
<TableTd>{item.name}</TableTd> <TableTr key={item.id}>
<TableTd> <TableTd>
<Button color="green" onClick={() => router.push(`/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/${item.id}`)}> <Text fw={500}>{item.name}</Text>
<IconEdit size={20} /> </TableTd>
</Button> <TableTd>
</TableTd> <Tooltip label="Edit" withArrow>
<TableTd> <Button
<Button color="red" onClick={() => { variant="light"
setSelectedId(item.id) color="blue"
setModalHapus(true) size="sm"
}}> onClick={() => router.push(`/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/${item.id}`)}
<IconX size={20} /> >
</Button> <IconEdit size={18} />
</Button>
</Tooltip>
</TableTd>
<TableTd>
<Tooltip label="Hapus" withArrow>
<Button
variant="light"
color="red"
size="sm"
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={18} />
</Button>
</Tooltip>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={2}>
<Center py={20}>
<Text c="dimmed">Tidak ada data kategori yang ditemukan</Text>
</Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} )}
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
@@ -133,11 +142,13 @@ function ListKategoriKegiatan({ search }: { search: string }) {
value={page} value={page}
onChange={(newPage) => { onChange={(newPage) => {
load(newPage, 10); load(newPage, 10);
window.scrollTo(0, 0); window.scrollTo({ top: 0, behavior: 'smooth' });
}} }}
total={totalPages} total={totalPages}
mt="md" mt="md"
mb="md" mb="md"
color="blue"
radius="md"
/> />
</Center> </Center>
{/* Modal Konfirmasi Hapus */} {/* Modal Konfirmasi Hapus */}

View File

@@ -1,18 +1,16 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client';
import { useProxy } from 'valtio/utils'; import { Box, Button, Group, Paper, Select, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { Box, Button, Group, Paper, Select, Stack, Text, TextInput } from '@mantine/core';
import { IconArrowBack, IconFile, IconUpload, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconFile, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import colors from '@/con/colors'; import colors from '@/con/colors';
import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi'; import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import { toast } from 'react-toastify';
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
interface FormDesaAntiKorupsi { interface FormDesaAntiKorupsi {
@@ -22,18 +20,20 @@ interface FormDesaAntiKorupsi {
fileId: string; fileId: string;
} }
function EditDesaAntiKorupsi() { export default function EditDesaAntiKorupsi() {
const desaAntiKorupsiState = useProxy(korupsiState.desaAntikorupsi) const desaAntiKorupsiState = useProxy(korupsiState.desaAntikorupsi);
const [previewFile, setPreviewFile] = useState<string | null>(null); const [previewFile, setPreviewFile] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const params = useParams() const [isLoading, setIsLoading] = useState(false);
const router = useRouter() const params = useParams();
const router = useRouter();
const [formData, setFormData] = useState<FormDesaAntiKorupsi>({ const [formData, setFormData] = useState<FormDesaAntiKorupsi>({
name: '', name: '',
deskripsi: '', deskripsi: '',
kategoriId: '', kategoriId: '',
fileId: '', fileId: '',
}) });
useEffect(() => { useEffect(() => {
const loadDesaAntiKorupsi = async () => { const loadDesaAntiKorupsi = async () => {
@@ -43,7 +43,6 @@ function EditDesaAntiKorupsi() {
try { try {
const data = await desaAntiKorupsiState.edit.load(id); const data = await desaAntiKorupsiState.edit.load(id);
if (data) { if (data) {
// ⬇️ FIX PENTING: tambahkan ini
desaAntiKorupsiState.edit.id = id; desaAntiKorupsiState.edit.id = id;
desaAntiKorupsiState.edit.form = { desaAntiKorupsiState.edit.form = {
@@ -61,169 +60,198 @@ function EditDesaAntiKorupsi() {
}); });
if (data?.file?.link) { if (data?.file?.link) {
setPreviewFile(data.file.link) setPreviewFile(data.file.link);
} }
} }
} catch (error) { } catch (error) {
console.error("Error loading program penghijauan:", error); console.error('Error loading data:', error);
toast.error("Gagal memuat data program penghijauan"); toast.error('Gagal memuat data Desa Anti Korupsi');
} }
} };
loadDesaAntiKorupsi(); loadDesaAntiKorupsi();
}, [params?.id]); }, [params?.id]);
const handleSubmit = async () => { const handleSubmit = async () => {
if (!formData.name) {
return toast.warn('Masukkan judul dokumen');
}
if (!formData.kategoriId) {
return toast.warn('Pilih kategori dokumen');
}
setIsLoading(true);
try { try {
// Update global state with form data // Update global state with form data
desaAntiKorupsiState.edit.form = { desaAntiKorupsiState.edit.form = {
...desaAntiKorupsiState.edit.form, ...desaAntiKorupsiState.edit.form,
name: formData.name, ...formData,
deskripsi: formData.deskripsi,
kategoriId: formData.kategoriId || '', kategoriId: formData.kategoriId || '',
fileId: formData.fileId // Keep existing imageId if not changed
}; };
// Jika ada file baru, upload // Upload new file if exists
if (file) { if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name }); const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name
});
const uploaded = res.data?.data; const uploaded = res.data?.data;
if (!uploaded?.id) { if (!uploaded?.id) {
return toast.error("Gagal upload gambar"); throw new Error('Gagal mengunggah dokumen');
} }
// Update imageId in global state
desaAntiKorupsiState.edit.form.fileId = uploaded.id; desaAntiKorupsiState.edit.form.fileId = uploaded.id;
} }
await desaAntiKorupsiState.edit.update(); await desaAntiKorupsiState.edit.update();
toast.success("desa anti korupsi berhasil diperbarui!"); toast.success('Data berhasil diperbarui');
router.push("/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi"); router.push('/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi');
} catch (error) { } catch (error) {
console.error("Error updating desa anti korupsi:", error); console.error('Error updating data:', error);
toast.error("Terjadi kesalahan saat memperbarui desa anti korupsi"); toast.error('Terjadi kesalahan saat memperbarui data');
} finally {
setIsLoading(false);
} }
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()}> <Tooltip label="Kembali ke halaman sebelumnya" color="blue" withArrow>
<IconArrowBack color={colors['blue-button']} size={25} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Button> <IconArrowBack color={colors['blue-button']} size={24} />
</Box> </Button>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> </Tooltip>
<Stack> <Title order={4} ml="sm" c="dark">
<Text fz={"xl"} fw={"bold"}>Edit List Desa Anti Korupsi</Text> Edit Desa Anti Korupsi
{desaAntiKorupsiState.findUnique.data ? ( </Title>
<Paper key={desaAntiKorupsiState.findUnique.data.id}> </Group>
<Stack gap={"xs"}>
<TextInput <Paper
value={formData.name} w={{ base: '100%', md: '50%' }}
onChange={(val) => { bg={colors['white-1']}
setFormData({ p="lg"
...formData, radius="md"
name: val.target.value shadow="sm"
}) style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label="Judul Dokumen"
placeholder="Masukkan judul dokumen"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required
/>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(val) => setFormData({ ...formData, deskripsi: val })}
/>
</Box>
<Select
label="Kategori"
placeholder="Pilih kategori"
value={formData.kategoriId}
onChange={(val) => setFormData({ ...formData, kategoriId: val || '' })}
data={
korupsiState.kategoriDesaAntiKorupsi.findMany.data?.map((v) => ({
value: v.id,
label: v.name,
})) || []
}
required
searchable
clearable
/>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Dokumen
</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewFile(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error('File tidak valid, gunakan format dokumen')}
maxSize={5 * 1024 ** 2}
accept={{
'application/*': ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx'],
}}
radius="md"
>
<Group justify="center" gap="xl" mih={180} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconFile size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="lg" inline>
Seret dokumen ke sini atau klik untuk memilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB (PDF, DOC, DOCX, XLS, XLSX, PPT, PPTX)
</Text>
</div>
</Group>
</Dropzone>
{previewFile && (
<Box mt="md">
<Text fw="bold" fz="sm" mb={6}>
Pratinjau Dokumen
</Text>
<Box
style={{
border: '1px solid #e0e0e0',
borderRadius: '8px',
overflow: 'hidden',
height: '500px',
width: '100%',
}} }}
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>} >
placeholder='Masukkan judul' <iframe
/> src={previewFile}
<Box> width="100%"
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text> height="100%"
<EditEditor style={{ border: 'none' }}
value={formData.deskripsi}
onChange={(val) => {
setFormData({
...formData,
deskripsi: val
})
}}
/> />
</Box> </Box>
<Select </Box>
value={formData.kategoriId} )}
onChange={(val) => { </Box>
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> <Group justify="right" mt="xl">
<Text size="xl" inline> <Button
Drag file ke sini atau klik untuk pilih file onClick={handleSubmit}
</Text> radius="md"
<Text size="sm" c="dimmed" inline mt={7}> size="md"
Maksimal 5MB dan harus format document loading={isLoading}
</Text> style={{
</div> background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
</Group> color: '#fff',
</Dropzone> boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
<Box> }}
<Text fw={"bold"} fz={"lg"}>Dokumen</Text> >
{previewFile ? ( Simpan
<iframe </Button>
src={previewFile} </Group>
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> </Stack>
</Paper> </Paper>
</Box> </Box>
); );
} }
export default EditDesaAntiKorupsi;

View File

@@ -2,15 +2,15 @@
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi'; import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function DetailKegiatanDesa() { export default function DetailKegiatanDesa() {
const detailState = useProxy(korupsiState.desaAntikorupsi) const detailState = useProxy(korupsiState.desaAntikorupsi)
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null)
@@ -34,89 +34,122 @@ function DetailKegiatanDesa() {
if (!detailState.findUnique.data) { if (!detailState.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={40} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) );
} }
const data = detailState.findUnique.data;
return ( return (
<Box> <Box py={10}>
<Box mb={10}> <Button
<Button variant="subtle" onClick={() => router.back()}> variant="subtle"
<IconArrowBack color={colors['blue-button']} size={25} /> onClick={() => router.back()}
</Button> leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
</Box> mb={15}
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}> >
<Stack> Kembali
<Text fz={"xl"} fw={"bold"}>Detail List Desa Anti Korupsi</Text> </Button>
{detailState.findUnique.data ? (
<Paper key={detailState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}> <Paper
<Stack gap={"xs"}> w={{ base: "100%", md: "50%" }}
<Box> bg={colors['white-1']}
<Text fw={"bold"} fz={"lg"}>Judul</Text> p="lg"
<Text fz={"lg"}>{detailState.findUnique.data?.name}</Text> radius="md"
</Box> shadow="sm"
<Box> >
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text> <Stack gap="md">
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: detailState.findUnique.data?.deskripsi }} /> <Text fz="2xl" fw="bold" c={colors['blue-button']}>
</Box> Detail Desa Anti Korupsi
<Box> </Text>
<Text fw={"bold"} fz={"lg"}>Kategori</Text>
<Text fz={"lg"}>{detailState.findUnique.data?.kategori?.name}</Text> <Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
</Box> <Stack gap="md">
<Box> <Box>
<Text fw={"bold"} fz={"lg"}>Dokumen</Text> <Text fz="lg" fw="bold">Judul</Text>
{detailState.findUnique.data?.file?.link ? ( <Text fz="md" c="dimmed">{data.name || '-'}</Text>
<iframe </Box>
src={detailState.findUnique.data.file.link}
width="100%" <Box>
height="500px" <Text fz="lg" fw="bold">Kategori</Text>
style={{ border: "1px solid #ccc", borderRadius: "8px" }} <Text fz="md" c="dimmed">{data.kategori?.name || '-'}</Text>
/> </Box>
) : (
<Text>Tidak ada dokumen tersedia</Text> <Box>
)} <Text fz="lg" fw="bold" mb="xs">Deskripsi</Text>
</Box> <Box
<Flex gap={"xs"} mt={10}> fz="md"
<Button c="dimmed"
onClick={() => { dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
if (detailState.findUnique.data) { style={{ lineHeight: 1.6 }}
setSelectedId(detailState.findUnique.data.id); />
setModalHapus(true); </Box>
}
<Box>
<Text fz="lg" fw="bold" mb="xs">Dokumen</Text>
{data.file?.link ? (
<Box
style={{
border: '1px solid #e0e0e0',
borderRadius: '8px',
overflow: 'hidden',
height: '500px',
width: '100%'
}} }}
disabled={detailState.delete.loading || !detailState.findUnique.data}
color={"red"}
> >
<IconX size={20} /> <iframe
</Button> src={data.file.link}
width="100%"
height="100%"
style={{ border: 'none' }}
/>
</Box>
) : (
<Text fz="sm" c="dimmed">Tidak ada dokumen tersedia</Text>
)}
</Box>
<Group gap="sm" mt="md">
<Tooltip label="Hapus Data" withArrow position="top">
<Button <Button
color="red"
onClick={() => { onClick={() => {
if (detailState.findUnique.data) { setSelectedId(data.id);
router.push(`/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/${detailState.findUnique.data.id}/edit`); setModalHapus(true);
}
}} }}
disabled={!detailState.findUnique.data} variant="light"
color={"green"} radius="md"
size="md"
disabled={detailState.delete.loading}
>
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Data" withArrow position="top">
<Button
color="green"
onClick={() => router.push(`/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/${data.id}/edit`)}
variant="light"
radius="md"
size="md"
> >
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Stack> </Group>
</Paper> </Stack>
) : null} </Paper>
</Stack> </Stack>
</Paper> </Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus desa anti korupsi ini?' text="Apakah Anda yakin ingin menghapus data Desa Anti Korupsi ini?"
/> />
</Box> </Box>
); );
} }
export default DetailKegiatanDesa;

View File

@@ -1,10 +1,21 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client';
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi'; import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Select,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconFile, IconUpload, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconFile, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@@ -13,12 +24,12 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function CreateDesaAntiKorupsi() { export default function CreateDesaAntiKorupsi() {
const router = useRouter(); const router = useRouter();
const stateKorupsi = useProxy(korupsiState.desaAntikorupsi) const stateKorupsi = useProxy(korupsiState.desaAntikorupsi);
const [previewFile, setPreviewFile] = useState<string | null>(null); const [previewFile, setPreviewFile] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => { useEffect(() => {
stateKorupsi.findMany.load(); stateKorupsi.findMany.load();
@@ -27,140 +38,181 @@ function CreateDesaAntiKorupsi() {
const resetForm = () => { const resetForm = () => {
stateKorupsi.create.form = { stateKorupsi.create.form = {
name: "", name: '',
deskripsi: "", deskripsi: '',
kategoriId: "", kategoriId: '',
fileId: "", fileId: '',
}; };
setFile(null); setFile(null);
setPreviewFile(null); setPreviewFile(null);
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
if (!file) { if (!file) {
return toast.warn("Pilih file pdf terlebih dahulu"); return toast.warn('Pilih file dokumen terlebih dahulu');
}
if (!stateKorupsi.create.form.name) {
return toast.warn('Masukkan judul dokumen');
}
if (!stateKorupsi.create.form.kategoriId) {
return toast.warn('Pilih kategori dokumen');
} }
const res = await ApiFetch.api.fileStorage.create.post({ setIsLoading(true);
file, try {
name: file.name, const res = await ApiFetch.api.fileStorage.create.post({
}) file,
name: file.name,
});
const uploaded = res.data?.data; const uploaded = res.data?.data;
if (!uploaded?.id) { if (!uploaded?.id) {
return toast.error("Gagal mengupload file"); throw new Error('Gagal mengunggah dokumen');
}
stateKorupsi.create.form.fileId = uploaded.id;
await stateKorupsi.create.create();
toast.success('Data berhasil disimpan');
resetForm();
router.push('/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi');
} catch (error) {
console.error('Error:', error);
toast.error('Terjadi kesalahan saat menyimpan data');
} finally {
setIsLoading(false);
} }
};
stateKorupsi.create.form.fileId = uploaded.id;
await stateKorupsi.create.create();
resetForm();
router.push("/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi")
}
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> <Group mb="md">
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Tooltip label="Kembali ke halaman sebelumnya" color="blue" withArrow>
<IconArrowBack color={colors['blue-button']} size={25} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Button> <IconArrowBack color={colors['blue-button']} size={24} />
</Box> </Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Dokumen Desa Anti Korupsi
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Paper
<Stack gap={"xs"}> w={{ base: '100%', md: '50%' }}
<Title order={4}>Create Kegiatan Desa</Title> bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<Box> <Box>
<Text fz={"md"} fw={"bold"}>File Document</Text> <Text fw="bold" fz="sm" mb={6}>
<Box> Dokumen
<Dropzone </Text>
onDrop={(files) => { <Dropzone
const selectedFile = files[0]; // Ambil file pertama onDrop={(files) => {
if (selectedFile) { const selectedFile = files[0];
setFile(selectedFile); if (selectedFile) {
setPreviewFile(URL.createObjectURL(selectedFile)); // Buat preview setFile(selectedFile);
} setPreviewFile(URL.createObjectURL(selectedFile));
}} }
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;
}} }}
onReject={() => toast.error('File tidak valid, gunakan format dokumen')}
maxSize={5 * 1024 ** 2}
accept={{
'application/*': ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx'],
}}
radius="md"
>
<Group justify="center" gap="xl" mih={180} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconFile size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="lg" inline>
Seret dokumen ke sini atau klik untuk memilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB (PDF, DOC, DOCX, XLS, XLSX, PPT, PPTX)
</Text>
</div>
</Group>
</Dropzone>
{previewFile && (
<Box mt="md" style={{ textAlign: 'center' }}>
<iframe
src={previewFile}
width="100%"
height="500px"
style={{
border: '1px solid #e0e0e0',
borderRadius: '8px',
maxWidth: '100%',
}}
/>
</Box>
)}
</Box>
<TextInput
label="Judul Dokumen"
placeholder="Masukkan judul dokumen"
value={stateKorupsi.create.form.name || ''}
onChange={(e) => (stateKorupsi.create.form.name = e.target.value)}
required
/>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<CreateEditor
value={stateKorupsi.create.form.deskripsi || ''}
onChange={(val) => (stateKorupsi.create.form.deskripsi = val)}
/> />
</Box> </Box>
<Select <Select
value={stateKorupsi.create.form.kategoriId} label="Kategori"
onChange={(val) => {
stateKorupsi.create.form.kategoriId = val ?? "";
}}
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>}
placeholder="Pilih kategori" placeholder="Pilih kategori"
value={stateKorupsi.create.form.kategoriId || ''}
onChange={(val) => (stateKorupsi.create.form.kategoriId = val || '')}
data={ data={
korupsiState.kategoriDesaAntiKorupsi.findMany.data?.map((v) => ({ korupsiState.kategoriDesaAntiKorupsi.findMany.data?.map((v) => ({
value: v.id, value: v.id,
label: v.name, label: v.name,
})) || [] })) || []
} }
required
searchable
clearable
/> />
<Group> <Group justify="right" mt="xl">
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button> <Button
onClick={handleSubmit}
radius="md"
size="md"
loading={isLoading}
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>
); );
} }
export default CreateDesaAntiKorupsi;

View File

@@ -1,13 +1,12 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import korupsiState from '../../../_state/landing-page/desa-anti-korupsi'; import korupsiState from '../../../_state/landing-page/desa-anti-korupsi';
function DesaAntiKorupsi() { function DesaAntiKorupsi() {
@@ -16,7 +15,7 @@ function DesaAntiKorupsi() {
<Box> <Box>
<HeaderSearch <HeaderSearch
title='List Desa Anti Korupsi' title='List Desa Anti Korupsi'
placeholder='pencarian' placeholder='Cari nama program atau kategori...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -27,8 +26,8 @@ function DesaAntiKorupsi() {
} }
function ListDesaAntiKorupsi({ search }: { search: string }) { function ListDesaAntiKorupsi({ search }: { search: string }) {
const listState = useProxy(korupsiState.desaAntikorupsi)
const router = useRouter(); const router = useRouter();
const listState = useProxy(korupsiState.desaAntikorupsi);
const { const {
data, data,
@@ -42,99 +41,96 @@ function ListDesaAntiKorupsi({ search }: { search: string }) {
load(page, 10, search); load(page, 10, search);
}, [page, search]); }, [page, search]);
const filteredData = data || [] const filteredData = data || [];
// Handle loading state
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton height={550} /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
); );
} }
if (data.length === 0) {
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Desa Anti Korupsi'
href='/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/create'
/>
<Box style={{ overflowX: "auto" }}>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Kategori</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
</Table>
</Box>
</Paper>
</Box>
);
}
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<Stack> <Group justify="space-between" mb="md">
<JudulList <Title order={4}>Daftar Program Desa Anti Korupsi</Title>
title='List Desa Anti Korupsi' <Tooltip label="Tambah Program Desa Anti Korupsi" withArrow>
href='/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/create' <Button
/> leftSection={<IconPlus size={18} />}
<Box style={{ overflowX: 'auto' }}> color="blue"
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}> variant="light"
<TableThead> onClick={() => router.push('/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/create')}
<TableTr> >
<TableTh>Nama Desa Anti Korupsi</TableTh> Tambah Baru
<TableTh>Deskripsi Desa Anti Korupsi</TableTh> </Button>
<TableTh>Kategori Desa Anti Korupsi</TableTh> </Tooltip>
<TableTh>Detail</TableTh> </Group>
</TableTr> <Box style={{ overflowX: "auto" }}>
</TableThead> <Table highlightOnHover>
<TableTbody> <TableThead>
{filteredData.map((item) => ( <TableTr>
<TableTh>Nama Program</TableTh>
<TableTh>Kategori</TableTh>
<TableTh>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Box w={100}> <Box w={350}>
<Text truncate="end" fz={"sm"}>{item.name}</Text> <Text lineClamp={1} fw={500}>{item.name || '-'}</Text>
</Box> </Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={200}> <Text fz="sm" c="dimmed">
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} /> {item.kategori?.name || '-'}
</Box> </Text>
</TableTd> </TableTd>
<TableTd>{item.kategori?.name}</TableTd>
<TableTd> <TableTd>
<Button onClick={() => router.push(`/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/${item.id}`)}> <Button
<IconDeviceImacCog size={25} /> variant="light"
color="blue"
onClick={() => router.push(`/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/${item.id}`)}
>
<IconDeviceImacCog size={20} />
<Text ml={5}>Detail</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} ))
</TableTbody> ) : (
</Table> <TableTr>
</Box> <TableTd colSpan={4}>
</Stack> <Center py={20}>
<Text c="dimmed">Tidak ada data program yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper> </Paper>
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => { onChange={(newPage) => {
load(newPage, 10); load(newPage, 10);
window.scrollTo(0, 0); window.scrollTo({ top: 0, behavior: 'smooth' });
}} }}
total={totalPages} total={totalPages}
mt="md" mt="md"
mb="md" mb="md"
color="blue"
radius="md"
/> />
</Center> </Center>
</Box> </Box>
) );
} }
export default DesaAntiKorupsi; export default DesaAntiKorupsi;

View File

@@ -1,67 +1,110 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { IconBulb, IconUsers, IconBrandFacebook } from '@tabler/icons-react';
function LayoutTabs({ children }: { children: React.ReactNode }) { function LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter() const router = useRouter();
const pathname = usePathname() const pathname = usePathname();
const tabs = [ const tabs = [
{ {
label: "Program Inovasi", label: "Program Inovasi",
value: "program-inovasi", value: "program-inovasi",
href: "/admin/landing-page/profile/program-inovasi" href: "/admin/landing-page/profile/program-inovasi",
icon: <IconBulb size={18} stroke={1.8} />,
tooltip: "Lihat dan kelola program inovasi desa",
}, },
{ {
label: "Pejabat Desa", label: "Pejabat Desa",
value: "pejabat-desa", value: "pejabat-desa",
href: "/admin/landing-page/profile/pejabat-desa" href: "/admin/landing-page/profile/pejabat-desa",
icon: <IconUsers size={18} stroke={1.8} />,
tooltip: "Kelola data pejabat desa",
}, },
{ {
label: "Media Sosial", label: "Media Sosial",
value: "media-sosial", value: "media-sosial",
href: "/admin/landing-page/profile/media-sosial" href: "/admin/landing-page/profile/media-sosial",
icon: <IconBrandFacebook size={18} stroke={1.8} />,
tooltip: "Atur tautan media sosial desa",
}, },
]; ];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value); const currentTab = tabs.find(tab => tab.href === pathname);
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => { const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value) const tab = tabs.find(t => t.value === value);
if (tab) { if (tab) {
router.push(tab.href) router.push(tab.href);
} }
setActiveTab(value) setActiveTab(value);
} };
useEffect(() => { useEffect(() => {
const match = tabs.find(tab => tab.href === pathname) const match = tabs.find(tab => tab.href === pathname);
if (match) { if (match) {
setActiveTab(match.value) setActiveTab(match.value);
} }
}, [pathname]) }, [pathname]);
return ( return (
<Stack> <Stack gap="lg">
<Title order={3}>Profile</Title> <Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}> Profil Desa
<TabsList p={"xs"} bg={"#BBC8E7FF"}> </Title>
{tabs.map((e, i) => ( <Tabs
<TabsTab key={i} value={e.value}>{e.label}</TabsTab> variant="pills"
value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
}}
>
{tabs.map((tab, i) => (
<Tooltip key={i} label={tab.tooltip} position="bottom" withArrow>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))} ))}
</TabsList> </TabsList>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value}> {tabs.map((tab, i) => (
{/* Konten dummy, bisa diganti tergantung routing */} <TabsPanel
<></> key={i}
value={tab.value}
style={{
padding: "1.5rem",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
borderRadius: "1rem",
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
}}
>
{children}
</TabsPanel> </TabsPanel>
))} ))}
</Tabs> </Tabs>
{children}
</Stack> </Stack>
); );
} }
export default LayoutTabs; export default LayoutTabs;

View File

@@ -1,9 +1,20 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client';
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile'; import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Image,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
@@ -12,17 +23,17 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function EditMediaSosial() { function EditMediaSosial() {
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial) const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial);
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
const [previewImage, setPreviewImage] = useState<string | null>(null); const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: stateMediaSosial.update.form.name || "", name: stateMediaSosial.update.form.name || '',
iconUrl: stateMediaSosial.update.form.iconUrl || "", iconUrl: stateMediaSosial.update.form.iconUrl || '',
imageId: stateMediaSosial.update.form.imageId || "" imageId: stateMediaSosial.update.form.imageId || '',
}) });
useEffect(() => { useEffect(() => {
const id = params?.id as string; const id = params?.id as string;
@@ -34,136 +45,147 @@ function EditMediaSosial() {
if (data) { if (data) {
setFormData({ setFormData({
name: data.name || "", name: data.name || '',
iconUrl: data.iconUrl || "", iconUrl: data.iconUrl || '',
imageId: data.imageId || "", imageId: data.imageId || '',
}); });
// Tampilkan preview gambar if (data.image?.link) setPreviewImage(data.image.link);
if (data.image?.link) {
setPreviewImage(data.image.link);
}
} }
} catch (error) { } catch (error) {
console.error("Error loading program inovasi:", error); console.error('Error loading media sosial:', error);
toast.error( toast.error(
error instanceof Error ? error.message : "Gagal mengambil data program inovasi" error instanceof Error ? error.message : 'Gagal mengambil data media sosial'
); );
} }
} };
loadMediaSosial(); loadMediaSosial();
}, [params?.id]); }, [params?.id]);
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
stateMediaSosial.update.form = { stateMediaSosial.update.form = { ...stateMediaSosial.update.form, ...formData };
...stateMediaSosial.update.form,
name: formData.name,
iconUrl: formData.iconUrl,
imageId: formData.imageId ?? "",
}
if (file) { if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name }); const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data; const uploaded = res.data?.data;
if (!uploaded?.id) { if (!uploaded?.id) return toast.error('Gagal upload gambar');
return toast.error("Gagal upload gambar");
}
// Update imageId in global state
stateMediaSosial.update.form.imageId = uploaded.id; stateMediaSosial.update.form.imageId = uploaded.id;
} }
await stateMediaSosial.update.update(); await stateMediaSosial.update.update();
toast.success("Media Sosial berhasil diperbarui!"); toast.success('Media sosial berhasil diperbarui!');
router.push("/admin/landing-page/profile/media-sosial"); router.push('/admin/landing-page/profile/media-sosial');
} catch (error) { } catch (error) {
console.error("Error updating media sosial:", error); console.error('Error updating media sosial:', error);
toast.error("Terjadi kesalahan saat memperbarui media sosial"); toast.error('Terjadi kesalahan saat memperbarui media sosial');
} }
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> <Group mb="md">
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Tooltip label="Kembali ke halaman sebelumnya" color="blue" withArrow>
<IconArrowBack color={colors['blue-button']} size={25} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Button> <IconArrowBack color={colors['blue-button']} size={24} />
</Box> </Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Media Sosial
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Paper
<Stack gap={"xs"}> w={{ base: '100%', md: '50%' }}
<Title order={4}>Edit Media Sosial</Title> bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text> <Text fw="bold" fz="sm" mb={6}>
<Box> Gambar Media Sosial
<Dropzone </Text>
onDrop={(files) => { <Dropzone
const selectedFile = files[0]; // Ambil file pertama onDrop={(files) => {
if (selectedFile) { const selectedFile = files[0];
setFile(selectedFile); if (selectedFile) {
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview setFile(selectedFile);
} setPreviewImage(URL.createObjectURL(selectedFile));
}} }
onReject={() => toast.error('File tidak valid.')} }}
maxSize={5 * 1024 ** 2} // Maks 5MB onReject={() => toast.error('File tidak valid, gunakan format gambar')}
accept={{ 'image/*': [] }} maxSize={5 * 1024 ** 2}
> accept={{ 'image/*': [] }}
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}> radius="md"
<Dropzone.Accept> p="xl"
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /> >
</Dropzone.Accept> <Group justify="center" gap="xl" mih={180}>
<Dropzone.Reject> <Dropzone.Accept>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} /> <IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
</Dropzone.Reject> </Dropzone.Accept>
<Dropzone.Idle> <Dropzone.Reject>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} /> <IconX size={48} color="red" stroke={1.5} />
</Dropzone.Idle> </Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={48} color="#868e96" stroke={1.5} />
</Dropzone.Idle>
<Stack gap="xs" align="center">
<Text size="md" fw={500}>
Seret gambar atau klik untuk memilih file
</Text>
<Text size="sm" c="dimmed">
Maksimal 5MB, format gambar wajib
</Text>
</Stack>
</Group>
</Dropzone>
<div> {previewImage && (
<Text size="xl" inline> <Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
Drag gambar ke sini atau klik untuk pilih file <Image
</Text> src={previewImage}
<Text size="sm" c="dimmed" inline mt={7}> alt="Preview Gambar"
Maksimal 5MB dan harus format gambar radius="md"
</Text> style={{ maxHeight: 220, objectFit: 'contain', border: `1px solid ${colors['blue-button']}` }}
</div> />
</Group> </Box>
</Dropzone> )}
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box> </Box>
<TextInput <TextInput
label="Nama Media Sosial / Kontak"
placeholder="Masukkan nama media sosial atau kontak"
value={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Nama Media Sosial / Nama Kontak</Text>} required
placeholder='Masukkan nama media sosial'
/> />
<TextInput <TextInput
label="Link Media Sosial / Nomor Telepon"
placeholder="Masukkan link media sosial atau nomor telepon"
value={formData.iconUrl} value={formData.iconUrl}
onChange={(e) => setFormData({ ...formData, iconUrl: e.target.value })} onChange={(e) => setFormData({ ...formData, iconUrl: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Icon URL / No Telephone</Text>} required
placeholder='Masukkan icon url'
/> />
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button> <Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -2,103 +2,132 @@
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile'; import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function DetailMediaSosial() { function DetailMediaSosial() {
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial) const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial);
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null);
const params = useParams() const params = useParams();
const router = useRouter(); const router = useRouter();
useShallowEffect(() => { useShallowEffect(() => {
stateMediaSosial.findUnique.load(params?.id as string) stateMediaSosial.findUnique.load(params?.id as string);
}, []) }, []);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
stateMediaSosial.delete.byId(selectedId) stateMediaSosial.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/landing-page/profile/media-sosial") router.push("/admin/landing-page/profile/media-sosial");
} }
} };
if (!stateMediaSosial.findUnique.data) { if (!stateMediaSosial.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) );
} }
const data = stateMediaSosial.findUnique.data;
return ( return (
<Box> <Box py={10}>
<Box mb={10}> <Button
<Button variant="subtle" onClick={() => router.back()}> variant="subtle"
<IconArrowBack color={colors['blue-button']} size={25} /> onClick={() => router.back()}
</Button> leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
</Box> mb={15}
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}> >
<Stack> Kembali
<Text fz={"xl"} fw={"bold"}>Detail Media Sosial</Text> </Button>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}> <Paper
w={{ base: "100%", md: "60%" }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Media Sosial
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Nama Media Sosial / Nama Kontak</Text> <Text fz="lg" fw="bold">Nama Media Sosial / Kontak</Text>
<Text fz={"lg"}>{stateMediaSosial.findUnique.data?.name}</Text> <Text fz="md" c="dimmed">{data.name || '-'}</Text>
</Box> </Box>
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Icon URL / No Telephone</Text> <Text fz="lg" fw="bold">Icon / Nomor Telepon</Text>
<Text fz={"lg"}>{stateMediaSosial.findUnique.data?.iconUrl}</Text> <Text fz="md" c="dimmed">{data.iconUrl || '-'}</Text>
</Box> </Box>
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text> <Text fz="lg" fw="bold">Gambar</Text>
<Box w={100} h={100}> {data.image?.link ? (
<Image src={stateMediaSosial.findUnique.data?.image?.link} alt="gambar" /> <Image
</Box> src={data.image.link}
alt={data.name || 'Gambar Media Sosial'}
w={120}
h={120}
radius="md"
fit="cover"
/>
) : (
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
)}
</Box> </Box>
<Box>
<Flex gap={"xs"}> <Group gap="sm">
<Tooltip label="Hapus Media Sosial" withArrow position="top">
<Button <Button
color="red"
onClick={() => { onClick={() => {
if (stateMediaSosial.findUnique.data) { setSelectedId(data.id);
setSelectedId(stateMediaSosial.findUnique.data.id); setModalHapus(true);
setModalHapus(true);
}
}} }}
disabled={!stateMediaSosial.findUnique.data} variant="light"
color="red"> radius="md"
<IconX size={20} /> size="md"
>
<IconTrash size={20} />
</Button> </Button>
</Tooltip>
<Tooltip label="Edit Media Sosial" withArrow position="top">
<Button <Button
onClick={() => { color="green"
if (stateMediaSosial.findUnique.data) { onClick={() => router.push(`/admin/landing-page/profile/media-sosial/${data.id}/edit`)}
router.push(`/admin/landing-page/profile/media-sosial/${stateMediaSosial.findUnique.data.id}/edit`); variant="light"
} radius="md"
}} size="md"
disabled={!stateMediaSosial.findUnique.data} >
color="green">
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Box> </Group>
</Stack> </Stack>
</Paper> </Paper>
</Stack> </Stack>
</Paper> </Paper>
{/* Modal Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus media sosial ini?" text="Apakah Anda yakin ingin menghapus media sosial ini?"
/> />
</Box> </Box>
); );

View File

@@ -1,8 +1,19 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Image,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@@ -11,9 +22,9 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import profileLandingPageState from '../../../../_state/landing-page/profile'; import profileLandingPageState from '../../../../_state/landing-page/profile';
function CreateMediaSosial() { export default function CreateMediaSosial() {
const router = useRouter(); const router = useRouter();
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial) const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial);
const [previewImage, setPreviewImage] = useState<string | null>(null); const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
@@ -23,27 +34,28 @@ function CreateMediaSosial() {
const resetForm = () => { const resetForm = () => {
stateMediaSosial.create.form = { stateMediaSosial.create.form = {
name: "", name: '',
imageId: "", imageId: '',
iconUrl: "", iconUrl: '',
}; };
setPreviewImage(null); setPreviewImage(null);
setFile(null); setFile(null);
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
if (!file) { if (!file) {
return toast.warn("Pilih file gambar terlebih dahulu"); return toast.warn('Silakan pilih file gambar terlebih dahulu');
} }
const res = await ApiFetch.api.fileStorage.create.post({ const res = await ApiFetch.api.fileStorage.create.post({
file, file,
name: file.name, name: file.name,
}) });
const uploaded = res.data?.data; const uploaded = res.data?.data;
if (!uploaded?.id) { if (!uploaded?.id) {
return toast.error("Gagal mengupload file"); return toast.error('Gagal mengunggah gambar, silakan coba lagi');
} }
stateMediaSosial.create.form.imageId = uploaded.id; stateMediaSosial.create.form.imageId = uploaded.id;
@@ -51,98 +63,108 @@ function CreateMediaSosial() {
await stateMediaSosial.create.create(); await stateMediaSosial.create.create();
resetForm(); resetForm();
router.push("/admin/landing-page/profile/media-sosial") router.push('/admin/landing-page/profile/media-sosial');
} };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> <Group mb="md">
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Tooltip label="Kembali ke halaman sebelumnya" color="blue" withArrow>
<IconArrowBack color={colors['blue-button']} size={25} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Button> <IconArrowBack color={colors['blue-button']} size={24} />
</Box> </Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Media Sosial
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Paper
<Stack gap={"xs"}> w={{ base: '100%', md: '50%' }}
<Title order={4}>Create Media Sosial</Title> bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text> <Text fw="bold" fz="sm" mb={6}>
<Box> Gambar Media Sosial
<Dropzone </Text>
onDrop={(files) => { <Dropzone
const selectedFile = files[0]; // Ambil file pertama onDrop={(files) => {
if (selectedFile) { const selectedFile = files[0];
setFile(selectedFile); if (selectedFile) {
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview setFile(selectedFile);
} setPreviewImage(URL.createObjectURL(selectedFile));
}} }
onReject={() => toast.error('File tidak valid.')} }}
maxSize={5 * 1024 ** 2} // Maks 5MB onReject={() => toast.error('File tidak valid, gunakan format gambar')}
accept={{ 'image/*': [] }} maxSize={5 * 1024 ** 2}
> accept={{ 'image/*': [] }}
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}> radius="md"
<Dropzone.Accept> p="xl"
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /> >
</Dropzone.Accept> <Group justify="center" gap="xl" mih={180}>
<Dropzone.Reject> <Dropzone.Accept>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} /> <IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Reject> </Dropzone.Accept>
<Dropzone.Idle> <Dropzone.Reject>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} /> <IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Idle> </Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
</Group>
<Text ta="center" mt="sm" size="sm" color="dimmed">
Seret gambar atau klik untuk memilih file (maks 5MB)
</Text>
</Dropzone>
<div> {previewImage && (
<Text size="xl" inline> <Box mt="sm" style={{ textAlign: 'center' }}>
Drag gambar ke sini atau klik untuk pilih file <Image
</Text> src={previewImage}
<Text size="sm" c="dimmed" inline mt={7}> alt="Preview Gambar"
Maksimal 5MB dan harus format gambar radius="md"
</Text> style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
</div> />
</Group> </Box>
</Dropzone> )}
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box> </Box>
<TextInput <TextInput
label="Nama Media Sosial / Kontak"
placeholder="Masukkan nama media sosial atau kontak"
value={stateMediaSosial.create.form.name || ''} value={stateMediaSosial.create.form.name || ''}
onChange={(val) => { onChange={(e) => (stateMediaSosial.create.form.name = e.target.value)}
stateMediaSosial.create.form.name = val.target.value; required
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Media Sosial / Nama Kontak</Text>}
placeholder='Masukkan nama media sosial / nama kontak'
/> />
<TextInput <TextInput
label="Link Media Sosial / Nomor Telepon"
placeholder="Masukkan link media sosial atau nomor telepon"
value={stateMediaSosial.create.form.iconUrl || ''} value={stateMediaSosial.create.form.iconUrl || ''}
onChange={(val) => { onChange={(e) => (stateMediaSosial.create.form.iconUrl = e.target.value)}
stateMediaSosial.create.form.iconUrl = val.target.value; required
}}
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 justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>
); );
} }
export default CreateMediaSosial;

View File

@@ -1,13 +1,12 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Image, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import { Box, Button, Center, Group, Image, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import profileLandingPageState from '../../../_state/landing-page/profile'; import profileLandingPageState from '../../../_state/landing-page/profile';
function MediaSosial() { function MediaSosial() {
@@ -16,7 +15,7 @@ function MediaSosial() {
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Media Sosial' title='Media Sosial'
placeholder='pencarian' placeholder='Cari nama media sosial atau kontak...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -44,80 +43,77 @@ function ListMediaSosial({ search }: { search: string }) {
const filteredData = data || [] const filteredData = data || []
// Handle loading state
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton height={550} /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
); );
} }
if (data.length === 0) {
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Media Sosial'
href='/admin/landing-page/profile/media-sosial/create'
/>
<Box style={{ overflowX: "auto" }}>
<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>
</Table>
</Box>
</Paper>
</Box>
);
}
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<JudulList <Group justify="space-between" mb="md">
title='List Media Sosial' <Title order={4}>Daftar Media Sosial</Title>
href='/admin/landing-page/profile/media-sosial/create' <Tooltip label="Tambah Media Sosial" withArrow>
/> <Button leftSection={<IconPlus size={18} />} color="blue" variant="light" onClick={() => router.push('/admin/landing-page/profile/media-sosial/create')}>
<Box style={{ overflowY: "auto" }}> Tambah Baru
<Table striped withTableBorder withRowBorders> </Button>
</Tooltip>
</Group>
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Nama Media Sosial / Nama Kontak</TableTh> <TableTh>Nama Media Sosial / Kontak</TableTh>
<TableTh>Image</TableTh> <TableTh>Gambar</TableTh>
<TableTh>Icon URL / No Telephone</TableTh> <TableTh>Icon / No. Telepon</TableTh>
<TableTh>Detail</TableTh> <TableTh>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item) => ( {filteredData.length > 0 ? (
<TableTr key={item.id}> filteredData.map((item) => (
<TableTd>{item.name}</TableTd> <TableTr key={item.id}>
<TableTd> <TableTd>
<Box w={50} h={50}> <Text fw={500}>{item.name}</Text>
<Image src={item.image?.link} alt={item.name} /> </TableTd>
</Box> <TableTd>
</TableTd> <Box w={50} h={50} style={{ borderRadius: 8, overflow: 'hidden' }}>
<TableTd> {item.image?.link ? (
<Box w={250}> <Image src={item.image.link} alt={item.name} fit="cover" />
<a style={{color: "black"}} href={item.iconUrl} target="_blank" rel="noopener noreferrer"> ) : (
<Text truncate fz={'sm'}>{item.iconUrl}</Text> <Box bg={colors['blue-button']} w="100%" h="100%" />
</a> )}
</Box> </Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button onClick={() => router.push(`/admin/landing-page/profile/media-sosial/${item.id}`)}> <Text truncate fz="sm" color="dimmed">
<IconDeviceImac size={20} /> {item.iconUrl || item.noTelp || '-'}
</Button> </Text>
</TableTd>
<TableTd>
<Button
variant="light"
color="blue"
onClick={() => router.push(`/admin/landing-page/profile/media-sosial/${item.id}`)}
>
<IconDeviceImac size={20} />
<Text ml={5}>Detail</Text>
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">Tidak ada data media sosial yang cocok</Text>
</Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} )}
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
@@ -127,11 +123,13 @@ function ListMediaSosial({ search }: { search: string }) {
value={page} value={page}
onChange={(newPage) => { onChange={(newPage) => {
load(newPage, 10); load(newPage, 10);
window.scrollTo(0, 0); window.scrollTo({ top: 0, behavior: 'smooth' });
}} }}
total={totalPages} total={totalPages}
mt="md" mt="md"
mb="md" mb="md"
color="blue"
radius="md"
/> />
</Center> </Center>
</Box> </Box>

View File

@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Alert, Box, Button, Center, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Alert, Box, Button, Center, Group, Image, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
@@ -144,124 +144,134 @@ function EditPejabatDesa() {
return ( return (
<Box> <Box>
<Stack gap="xs"> <Stack gap="xs">
<Box> <Group mb="md">
<Button variant="subtle" onClick={handleBack}> <Tooltip label="Kembali ke halaman sebelumnya" color="blue" withArrow>
<IconArrowBack color={colors['blue-button']} size={20} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Button> <IconArrowBack color={colors['blue-button']} size={24} />
</Box> </Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Pejabat Desa
</Title>
</Group>
<Box> <Paper
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p="md" radius={10}> w={{ base: "100%", md: "50%" }}
<Stack gap="xs"> bg={colors['white-1']}
<Title order={3}>Edit Profile Pejabat Desa</Title> p="md"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="xs">
<Title order={3}>Edit Profile Pejabat Desa</Title>
{/* Nama Field */} {/* Nama Field */}
<TextInput <TextInput
label={<Text fw="bold">Nama Perbekel</Text>} label={<Text fw="bold">Nama Perbekel</Text>}
placeholder="Masukkan nama perbekel" placeholder="Masukkan nama perbekel"
value={allState.edit.form.name} value={allState.edit.form.name}
onChange={(e) => handleFieldChange('name', e.currentTarget.value)} onChange={(e) => handleFieldChange('name', e.currentTarget.value)}
error={!allState.edit.form.name && "Nama wajib diisi"} error={!allState.edit.form.name && "Nama wajib diisi"}
/> />
{/* Posisi Field */} {/* Posisi Field */}
<TextInput <TextInput
label={<Text fw="bold">Posisi</Text>} label={<Text fw="bold">Posisi</Text>}
placeholder="Masukkan posisi" placeholder="Masukkan posisi"
value={allState.edit.form.position} value={allState.edit.form.position}
onChange={(e) => handleFieldChange('position', e.currentTarget.value)} onChange={(e) => handleFieldChange('position', e.currentTarget.value)}
error={!allState.edit.form.position && "Posisi wajib diisi"} error={!allState.edit.form.position && "Posisi wajib diisi"}
/> />
{/* File Upload */} {/* File Upload */}
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text> <Dropzone
<Box> onDrop={(files) => handleFileChange(files[0])}
<Dropzone onReject={() => toast.error('File tidak valid.')}
onDrop={(files) => handleFileChange(files[0])} maxSize={5 * 1024 ** 2} // Maks 5MB
onReject={() => toast.error('File tidak valid.')} accept={{ 'image/*': [] }}
maxSize={5 * 1024 ** 2} // Maks 5MB >
accept={{ 'image/*': [] }} <Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
> <Dropzone.Accept>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}> <IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
<Dropzone.Accept> </Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /> <Dropzone.Reject>
</Dropzone.Accept> <IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
<Dropzone.Reject> </Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} /> <Dropzone.Idle>
</Dropzone.Reject> <IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
<Dropzone.Idle> </Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div> <div>
<Text size="xl" inline> <Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file Drag gambar ke sini atau klik untuk pilih file
</Text> </Text>
<Text size="sm" c="dimmed" inline mt={7}> <Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar Maksimal 5MB dan harus format gambar
</Text> </Text>
</div> </div>
</Group> </Group>
</Dropzone> </Dropzone>
{/* Tampilkan preview kalau ada */} {/* Tampilkan preview kalau ada */}
{previewImage && ( {previewImage && (
<Box mt="sm"> <Box mt="sm">
<Image <Image
src={previewImage} src={previewImage}
alt="Preview" alt="Preview"
style={{ style={{
maxWidth: '100%', maxWidth: '100%',
maxHeight: '200px', maxHeight: '200px',
objectFit: 'contain', objectFit: 'contain',
borderRadius: '8px', borderRadius: '8px',
border: '1px solid #ddd', border: '1px solid #ddd',
}} }}
/> />
</Box> </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> </Box>
</Box>
{/* Submit Button */} {/* Preview Gambar */}
<Group> <Box>
<Button <Text fz="sm" fw="bold" mb="xs">Preview Gambar</Text>
bg={colors['blue-button']} {previewImage ? (
onClick={handleSubmit} <Image alt="Profile preview" src={previewImage} w={200} h={200} fit="cover" radius="md" />
loading={isSubmitting || allState.edit.loading} ) : (
disabled={!allState.edit.form.name} <Center w={200} h={200} bg="gray.2">
> <Stack align="center" gap="xs">
{isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'} <IconImageInPicture size={48} color="gray" />
</Button> <Text size="sm" c="gray">Tidak ada gambar</Text>
</Stack>
</Center>
)}
</Box>
<Button {/* Submit Button */}
variant="outline" <Group>
onClick={handleBack} <Button
disabled={isSubmitting || allState.edit.loading} bg={colors['blue-button']}
> onClick={handleSubmit}
Batal loading={isSubmitting || allState.edit.loading}
</Button> disabled={!allState.edit.form.name}
</Group> >
</Stack> {isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
</Paper> </Button>
</Box>
<Button
variant="outline"
onClick={handleBack}
disabled={isSubmitting || allState.edit.loading}
>
Batal
</Button>
</Group>
</Stack>
</Paper>
</Stack> </Stack>
</Box> </Box>
); );

View File

@@ -1,24 +1,26 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Divider, Grid, GridCol, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core'; import { Box, Button, Center, Divider, Grid, GridCol, Image, Paper, Skeleton, Stack, Text, Title, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconEdit } from '@tabler/icons-react'; import { IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile'; import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
function Page() { function Page() {
const router = useRouter() const router = useRouter();
const allList = useProxy(profileLandingPageState.pejabatDesa) const allList = useProxy(profileLandingPageState.pejabatDesa);
useShallowEffect(() => { useShallowEffect(() => {
allList.findUnique.load("edit") // Assuming "1" is your default ID, adjust as needed allList.findUnique.load("edit");
}, []) }, []);
if (!allList.findUnique.data) { if (!allList.findUnique.data) {
return <Stack> return (
<Skeleton radius={10} h={800} /> <Stack align="center" justify="center" py="xl">
</Stack> <Skeleton radius="md" height={800} />
</Stack>
);
} }
const dataArray = Array.isArray(allList.findUnique.data) const dataArray = Array.isArray(allList.findUnique.data)
@@ -26,79 +28,82 @@ function Page() {
: [allList.findUnique.data]; : [allList.findUnique.data];
return ( return (
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
<Stack gap={"xs"}> <Stack gap="md">
<Grid> <Grid align="center">
<GridCol span={{ base: 12, md: 11 }}> <GridCol span={{ base: 12, md: 11 }}>
<Title order={3}>Preview Pejabat Desa</Title> <Title order={3} c={colors['blue-button']}>Preview Pejabat Desa</Title>
</GridCol> </GridCol>
<GridCol span={{ base: 12, md: 1 }}> <GridCol span={{ base: 12, md: 1 }}>
<Button bg={colors['blue-button']} onClick={() => router.push(`/admin/landing-page/profile/pejabat-desa/${allList.findUnique.data?.id}`)}> <Tooltip label="Edit Profil Pejabat" withArrow>
<IconEdit size={16} /> <Button
</Button> c="blue"
variant="light"
leftSection={<IconEdit size={18} stroke={2} />}
radius="md"
onClick={() => router.push(`/admin/landing-page/profile/pejabat-desa/${allList.findUnique.data?.id}`)}
>
Edit
</Button>
</Tooltip>
</GridCol> </GridCol>
</Grid> </Grid>
{dataArray.map((item) => ( {dataArray.map((item) => (
<Box key={item.id} > <Paper key={item.id} p="xl" bg={colors['BG-trans']} radius="md" shadow="xs">
<Paper p={"xl"} bg={colors['BG-trans']}> <Box px={{ base: "sm", md: 100 }}>
<Box px={{ base: "md", md: 100 }}> <Grid>
<Grid> <GridCol span={12}>
<GridCol span={{ base: 12, md: 12 }}> <Center>
<Center> <Image src="/darmasaba-icon.png" w={{ base: 100, md: 150 }} alt="Logo Desa" />
<Image src={"/darmasaba-icon.png"} w={{ base: 100, md: 150 }} alt='' /> </Center>
</Center> </GridCol>
</GridCol> <GridCol span={12}>
<GridCol span={{ base: 12, md: 12 }}> <Text ta="center" fz={{ base: "1.2rem", md: "1.8rem" }} fw="bold" c={colors['blue-button']}>
<Text ta={"center"} fz={{ base: "1.2rem", md: "1.8rem" }} fw={'bold'}>PROFIL PIMPINAN BADAN PUBLIK DESA DARMASABA </Text> Profil Pimpinan Badan Publik Desa Darmasaba
</GridCol> </Text>
</Grid> </GridCol>
</Box> </Grid>
<Divider my={"md"} color={colors['blue-button']} /> </Box>
{/* biodata perbekel */} <Divider my="md" color={colors['blue-button']} />
<Box px={{ base: 0, md: 50 }} pb={30}> <Box px={{ base: 0, md: 50 }} pb="xl">
<Box pb={20} px={{ base: 0, md: 50 }}> <Paper bg={colors['BG-trans']} radius="md" shadow="xs" p="lg">
<Paper bg={colors['BG-trans']} w={{ base: "100%", md: "100%" }}> <Stack gap={0}>
<Stack gap={0}> <Center>
<Center> <Image
<Image pt={{ base: 0, md: 60 }}
pt={{ base: 0, md: 90 }} src={item.image?.link || "/perbekel.png"}
src={item.image?.link || "/perbekel.png"} w={{ base: 250, md: 350 }}
w={{ base: 250, md: 350 }} alt="Foto Profil Pejabat"
alt='Foto Profil PPID' radius="md"
onError={(e) => { onError={(e) => { e.currentTarget.src = "/perbekel.png"; }}
e.currentTarget.src = "/perbekel.png"; />
}} </Center>
/> <Paper
</Center> bg={colors['blue-button']}
<Paper py="md"
bg={colors['blue-button']} px="sm"
py={20} radius="md"
className="glass3" className="glass3"
px={{ base: 10, md: 10 }} style={{ mt: -30, boxShadow: '0 4px 20px rgba(0,0,0,0.15)' }}
>
> <Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "1.2rem", md: "1.6rem" }}>
<Text ta={"center"} c={colors['white-1']} fw={"bolder"} fz={{ base: "1.2rem", md: "1.6rem" }}> {item.name}
{item.name} </Text>
</Text>
</Paper>
</Stack>
</Paper> </Paper>
</Box> </Stack>
<Box pt={10}> </Paper>
<Box> <Box mt="lg">
<Text fz={{ base: "1.125rem", md: "1.6rem" }} fw={'bold'}>Position</Text> <Text fz={{ base: "1.125rem", md: "1.5rem" }} fw="bold" mb={4}>Jabatan</Text>
<Text fz={{ base: "1rem", md: "1.5rem" }} ta={"justify"}>{item.position}</Text> <Text fz={{ base: "1rem", md: "1.4rem" }} ta="justify" c={colors['blue-button']}>
</Box> {item.position}
</Box> </Text>
</Box> </Box>
</Paper> </Box>
</Box> </Paper>
))} ))}
</Stack> </Stack>
</Paper> </Paper>
) );
} }
export default Page;
export default Page;

View File

@@ -3,7 +3,18 @@
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile'; import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Image,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
@@ -86,92 +97,113 @@ function EditProgramInovasi() {
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> <Group mb="md">
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Tooltip label="Kembali ke halaman sebelumnya" color="blue" withArrow>
<IconArrowBack color={colors['blue-button']} size={25} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Button> <IconArrowBack color={colors['blue-button']} size={24} />
</Box> </Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Program Inovasi
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Paper
<Stack gap={"xs"}> w={{ base: '100%', md: '50%' }}
<Title order={4}>Edit Program Inovasi</Title> bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text> <Text fw="bold" fz="sm" mb={6}>
<Box> Gambar Program Inovasi
<Dropzone </Text>
onDrop={(files) => { <Dropzone
const selectedFile = files[0]; // Ambil file pertama onDrop={(files) => {
if (selectedFile) { const selectedFile = files[0];
setFile(selectedFile); if (selectedFile) {
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview setFile(selectedFile);
} setPreviewImage(URL.createObjectURL(selectedFile));
}} }
onReject={() => toast.error('File tidak valid.')} }}
maxSize={5 * 1024 ** 2} // Maks 5MB onReject={() => toast.error('File tidak valid, gunakan format gambar')}
accept={{ 'image/*': [] }} maxSize={5 * 1024 ** 2}
> accept={{ 'image/*': [] }}
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}> radius="md"
<Dropzone.Accept> p="xl"
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /> >
</Dropzone.Accept> <Group justify="center" gap="xl" mih={180}>
<Dropzone.Reject> <Dropzone.Accept>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} /> <IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
</Dropzone.Reject> </Dropzone.Accept>
<Dropzone.Idle> <Dropzone.Reject>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} /> <IconX size={48} color="red" stroke={1.5} />
</Dropzone.Idle> </Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={48} color="#868e96" stroke={1.5} />
</Dropzone.Idle>
<Stack gap="xs" align="center">
<Text size="md" fw={500}>
Seret gambar atau klik untuk memilih file
</Text>
<Text size="sm" c="dimmed">
Maksimal 5MB, format gambar wajib
</Text>
</Stack>
</Group>
</Dropzone>
<div> {previewImage && (
<Text size="xl" inline> <Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
Drag gambar ke sini atau klik untuk pilih file <Image
</Text> src={previewImage}
<Text size="sm" c="dimmed" inline mt={7}> alt="Preview Gambar"
Maksimal 5MB dan harus format gambar radius="md"
</Text> style={{ maxHeight: 220, objectFit: 'contain', border: `1px solid ${colors['blue-button']}` }}
</div> />
</Group> </Box>
</Dropzone> )}
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box> </Box>
<TextInput <TextInput
label="Nama Program Inovasi"
placeholder="Masukkan nama program inovasi"
value={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Nama Produk</Text>} required
placeholder='Masukkan nama produk'
/> />
<TextInput <TextInput
label="Deskripsi"
placeholder="Masukkan deskripsi program inovasi"
value={formData.description} value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })} onChange={(e) => setFormData({ ...formData, description: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Deskripsi</Text>} required
placeholder='Masukkan deskripsi'
/> />
<TextInput <TextInput
label="Link Program Inovasi"
placeholder="Masukkan link program inovasi (opsional)"
value={formData.link} value={formData.link}
onChange={(e) => setFormData({ ...formData, link: e.target.value })} onChange={(e) => setFormData({ ...formData, link: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Link</Text>}
placeholder='Masukkan link'
/> />
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button> <Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -2,9 +2,9 @@
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile'; import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
@@ -31,91 +31,116 @@ function DetailProgramInovasi() {
if (!stateProgramInovasi.findUnique.data) { if (!stateProgramInovasi.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={12}>
<Skeleton h={500} /> <Skeleton height={520} radius="md" />
</Stack> </Stack>
) )
} }
const data = stateProgramInovasi.findUnique.data
return ( return (
<Box> <Box px={{ base: 'md', md: 'xl' }} py="lg">
<Box mb={10}> <Button variant="subtle" onClick={() => router.back()} leftSection={<IconArrowBack size={22} color={colors['blue-button']} />}>
<Button variant="subtle" onClick={() => router.back()}> Kembali
<IconArrowBack color={colors['blue-button']} size={25} /> </Button>
</Button>
</Box> <Paper
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}> w={{ base: "100%", md: "60%" }}
<Stack> bg={colors['white-1']}
<Text fz={"xl"} fw={"bold"}>Detail Program Inovasi</Text> p="lg"
<Paper bg={colors['BG-trans']} p={'md'}> radius="md"
<Stack gap={"xs"}> shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Program Inovasi
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Nama Program Inovasi</Text> <Text fz="lg" fw="bold">Nama Program Inovasi</Text>
<Text fz={"lg"}>{stateProgramInovasi.findUnique.data?.name}</Text> <Text fz="md" c="dimmed">{data.name || '-'}</Text>
</Box> </Box>
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text> <Text fz="lg" fw="bold">Deskripsi</Text>
<Text <Text fz="md" c="dimmed" style={{ whiteSpace: 'pre-wrap' }}>{data.description || '-'}</Text>
fz={"lg"}
>{stateProgramInovasi.findUnique.data?.description}</Text>
</Box> </Box>
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Link</Text> <Text fz="lg" fw="bold">Link</Text>
<a {data.link ? (
href={stateProgramInovasi.findUnique.data?.link || "#"} <a
target="_blank" href={data.link}
rel="noopener noreferrer" target="_blank"
style={{ rel="noopener noreferrer"
wordWrap: 'break-word', style={{
whiteSpace: 'pre-wrap', color: colors['blue-button'],
overflowWrap: 'break-word', textDecoration: 'underline',
width: '100%' wordBreak: 'break-word',
}}
>
{stateProgramInovasi.findUnique.data?.link || "Tidak ada link"}
</a>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
<Image src={stateProgramInovasi.findUnique.data?.image?.link} alt="gambar" />
</Box>
<Box>
<Flex gap={"xs"}>
<Button
onClick={() => {
if (stateProgramInovasi.findUnique.data) {
setSelectedId(stateProgramInovasi.findUnique.data.id);
setModalHapus(true);
}
}} }}
disabled={!stateProgramInovasi.findUnique.data} >
color="red"> {data.link}
<IconX size={20} /> </a>
) : (
<Text fz="md" c="dimmed">-</Text>
)}
</Box>
<Box>
<Text fz="lg" fw="bold">Gambar</Text>
{data.image?.link ? (
<Image
src={data.image.link}
alt="Gambar Program"
radius="md"
style={{ maxWidth: '100%', maxHeight: 300, objectFit: 'contain' }}
/>
) : (
<Text fz="md" c="dimmed">-</Text>
)}
</Box>
<Group gap="sm">
<Tooltip label="Hapus Program Inovasi" withArrow position="top">
<Button
color="red"
onClick={() => {
setSelectedId(data.id);
setModalHapus(true);
}}
variant="light"
radius="md"
size="md"
>
<IconTrash size={20} />
</Button> </Button>
</Tooltip>
<Tooltip label="Edit Program Inovasi" withArrow position="top">
<Button <Button
onClick={() => { color="green"
if (stateProgramInovasi.findUnique.data) { onClick={() => router.push(`/admin/landing-page/profile/program-inovasi/${data.id}/edit`)}
router.push(`/admin/landing-page/profile/program-inovasi/${stateProgramInovasi.findUnique.data.id}/edit`); variant="light"
} radius="md"
}} size="md"
disabled={!stateProgramInovasi.findUnique.data} >
color="green">
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Box> </Group>
</Stack> </Stack>
</Paper> </Paper>
</Stack> </Stack>
</Paper> </Paper>
{/* Modal Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus program inovasi ini?" text="Apakah Anda yakin ingin menghapus program inovasi ini?"
/> />
</Box> </Box>
); );

View File

@@ -1,8 +1,20 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client';
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Image,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@@ -13,7 +25,7 @@ import profileLandingPageState from '../../../../_state/landing-page/profile';
function CreateProgramInovasi() { function CreateProgramInovasi() {
const router = useRouter(); const router = useRouter();
const stateProgramInovasi = useProxy(profileLandingPageState.programInovasi) const stateProgramInovasi = useProxy(profileLandingPageState.programInovasi);
const [previewImage, setPreviewImage] = useState<string | null>(null); const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
@@ -31,20 +43,21 @@ function CreateProgramInovasi() {
setPreviewImage(null); setPreviewImage(null);
setFile(null); setFile(null);
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
if (!file) { if (!file) {
return toast.warn("Pilih file gambar terlebih dahulu"); return toast.warn("Silakan pilih file gambar terlebih dahulu");
} }
const res = await ApiFetch.api.fileStorage.create.post({ const res = await ApiFetch.api.fileStorage.create.post({
file, file,
name: file.name, name: file.name,
}) });
const uploaded = res.data?.data; const uploaded = res.data?.data;
if (!uploaded?.id) { if (!uploaded?.id) {
return toast.error("Gagal mengupload file"); return toast.error("Gagal mengunggah gambar, silakan coba lagi");
} }
stateProgramInovasi.create.form.imageId = uploaded.id; stateProgramInovasi.create.form.imageId = uploaded.id;
@@ -55,99 +68,116 @@ function CreateProgramInovasi() {
router.push("/admin/landing-page/profile/program-inovasi") router.push("/admin/landing-page/profile/program-inovasi")
} }
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> <Group mb="md">
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Tooltip label="Kembali ke halaman sebelumnya" color="blue" withArrow>
<IconArrowBack color={colors['blue-button']} size={25} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Button> <IconArrowBack color={colors['blue-button']} size={24} />
</Box> </Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Program Inovasi
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Paper
<Stack gap={"xs"}> w={{ base: '100%', md: '50%' }}
<Title order={4}>Create Program Inovasi</Title> bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text> <Text fw="bold" fz="sm" mb={6}>
<Box> Gambar Program Inovasi
<Dropzone </Text>
onDrop={(files) => { <Dropzone
const selectedFile = files[0]; // Ambil file pertama onDrop={(files) => {
if (selectedFile) { const selectedFile = files[0];
setFile(selectedFile); if (selectedFile) {
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview setFile(selectedFile);
} setPreviewImage(URL.createObjectURL(selectedFile));
}} }
onReject={() => toast.error('File tidak valid.')} }}
maxSize={5 * 1024 ** 2} // Maks 5MB onReject={() => toast.error('File tidak valid, gunakan format gambar')}
accept={{ 'image/*': [] }} maxSize={5 * 1024 ** 2}
> accept={{ 'image/*': [] }}
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}> radius="md"
<Dropzone.Accept> p="xl"
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /> >
</Dropzone.Accept> <Group justify="center" gap="xl" mih={180}>
<Dropzone.Reject> <Dropzone.Accept>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} /> <IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
</Dropzone.Reject> </Dropzone.Accept>
<Dropzone.Idle> <Dropzone.Reject>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} /> <IconX size={48} color="red" stroke={1.5} />
</Dropzone.Idle> </Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={48} color="#868e96" stroke={1.5} />
</Dropzone.Idle>
<Stack gap="xs" align="center">
<Text size="md" fw={500}>
Seret gambar atau klik untuk memilih file
</Text>
<Text size="sm" c="dimmed">
Maksimal 5MB, format gambar wajib
</Text>
</Stack>
</Group>
</Dropzone>
<div> {previewImage && (
<Text size="xl" inline> <Box mt="sm" style={{ textAlign: 'center' }}>
Drag gambar ke sini atau klik untuk pilih file <Image
</Text> src={previewImage}
<Text size="sm" c="dimmed" inline mt={7}> alt="Preview Gambar"
Maksimal 5MB dan harus format gambar radius="md"
</Text> style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
</div> />
</Group> </Box>
</Dropzone> )}
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box> </Box>
<TextInput
value={stateProgramInovasi.create.form.name || ''}
onChange={(val) => {
stateProgramInovasi.create.form.name = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Program Inovasi</Text>}
placeholder='Masukkan nama program inovasi'
/>
<TextInput
value={stateProgramInovasi.create.form.description || ''}
onChange={(val) => {
stateProgramInovasi.create.form.description = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Deskripsi</Text>}
placeholder='Masukkan deskripsi'
/>
<TextInput <TextInput
value={stateProgramInovasi.create.form.link || ''} label="Nama Program Inovasi"
onChange={(val) => { placeholder="Masukkan nama program inovasi"
stateProgramInovasi.create.form.link = val.target.value; value={stateProgramInovasi.create.form.name}
}} onChange={(e) => (stateProgramInovasi.create.form.name = e.target.value)}
label={<Text fw={"bold"} fz={"sm"}>Link</Text>} required
placeholder='Masukkan link'
/> />
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button> <Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<CreateEditor
value={stateProgramInovasi.create.form.description || ''}
onChange={(htmlContent: string) => {
stateProgramInovasi.create.form.description = htmlContent;
}}
/>
</Box>
<TextInput
label="Link Program Inovasi"
placeholder="Masukkan link program inovasi (opsional)"
value={stateProgramInovasi.create.form.link || ''}
onChange={(e) => (stateProgramInovasi.create.form.link = e.target.value)}
/>
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -1,22 +1,22 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import profileLandingPageState from '../../../_state/landing-page/profile'; import profileLandingPageState from '../../../_state/landing-page/profile';
function ProgramInovasi() { function ProgramInovasi() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
return ( return (
<Box> <Box px="md" py="lg">
<HeaderSearch <HeaderSearch
title='Program Inovasi' title="Program Inovasi"
placeholder='pencarian' placeholder="Cari program inovasi..."
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -27,107 +27,118 @@ function ProgramInovasi() {
} }
function ListProgramInovasi({ search }: { search: string }) { function ListProgramInovasi({ search }: { search: string }) {
const stateProgramInovasi = useProxy(profileLandingPageState.programInovasi) const stateProgramInovasi = useProxy(profileLandingPageState.programInovasi);
const router = useRouter(); const router = useRouter();
const { const { data, page, totalPages, loading, load } = stateProgramInovasi.findMany;
data,
page,
totalPages,
loading,
load,
} = stateProgramInovasi.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search); load(page, 10, search);
}, [page, search]); }, [page, search]);
const filteredData = data || [] const filteredData = data || [];
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={20}>
<Skeleton height={550} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
); );
} }
if (data.length === 0) {
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Program Inovasi'
href='/admin/landing-page/profile/program-inovasi/create'
/>
<Box style={{ overflowX: "auto" }}>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Program</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Link</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
</Table>
</Box>
</Paper>
</Box>
);
}
return ( return (
<Box py={10}> <Box py={15}>
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
<JudulList <Box mb="md" display="flex"
title='List Program Inovasi' style={{ justifyContent: 'space-between', alignItems: 'center' }}
href='/admin/landing-page/profile/program-inovasi/create' >
/> <Title order={4}>Daftar Program Inovasi</Title>
<Box style={{ overflowY: "auto" }}> <Tooltip label="Tambah Program Inovasi" withArrow>
<Table striped withTableBorder withRowBorders> <Button
color="blue"
leftSection={<IconPlus size={18} />}
variant="light"
radius="md"
onClick={() => router.push('/admin/landing-page/profile/program-inovasi/create')}
>
Tambah Program
</Button>
</Tooltip>
</Box>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover striped verticalSpacing="sm">
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Nama Program</TableTh> <TableTh>Nama Program</TableTh>
<TableTh>Deskripsi</TableTh> <TableTh>Deskripsi</TableTh>
<TableTh>Link</TableTh> <TableTh>Link</TableTh>
<TableTh>Detail</TableTh> <TableTh>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item) => ( {filteredData.length === 0 ? (
<TableTr key={item.id}> <TableTr>
<TableTd>{item.name}</TableTd> <TableTd colSpan={4}>
<TableTd w={200}>{item.description}</TableTd> <Center py={20}>
<TableTd> <Text color="dimmed">Belum ada data program inovasi</Text>
<Box w={250}> </Center>
<a style={{ color: "black" }} href={item.link} target="_blank" rel="noopener noreferrer">
<Text truncate fz={'sm'}>{item.link}</Text>
</a>
</Box>
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/landing-page/profile/program-inovasi/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} ) : (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text fw={500}>{item.name}</Text>
</TableTd>
<TableTd style={{ maxWidth: 250 }}>
<Text fz="sm" lineClamp={2}>
{item.description}
</Text>
</TableTd>
<TableTd style={{ maxWidth: 250 }}>
<Tooltip label="Buka tautan program" position="top" withArrow>
<a
href={item.link}
target="_blank"
rel="noopener noreferrer"
style={{ color: colors['blue-button'], textDecoration: 'underline' }}
>
<Text truncate fz="sm">{item.link}</Text>
</a>
</Tooltip>
</TableTd>
<TableTd>
<Button
variant="light"
color="blue"
radius="md"
onClick={() =>
router.push(`/admin/landing-page/profile/program-inovasi/${item.id}`)
}
>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))
)}
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{filteredData.length > 0 && (
<Center mt="md">
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
color="blue"
/>
</Center>
)}
</Paper> </Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={totalPages}
mt="md"
mb="md"
/>
</Center>
</Box> </Box>
); );
} }

View File

@@ -1,63 +1,93 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { IconTrash, IconRecycle } from '@tabler/icons-react';
function LayoutTabsPengelolaanSampahBankSampah({ children }: { children: React.ReactNode }) { function LayoutTabsPengelolaanSampahBankSampah({ children }: { children: React.ReactNode }) {
const router = useRouter() const router = useRouter();
const pathname = usePathname() const pathname = usePathname();
const tabs = [
{
label: "List Pengelolaan Sampah Bank Sampah",
value: "listpengelolaansampahbanksampah",
href: "/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah"
},
{
label: "Keterangan Bank Sampah Terdekat",
value: "keteranganbanksampahterdekat",
href: "/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat"
},
];
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 tabs = [
const tab = tabs.find(t => t.value === value) {
if (tab) { label: "List Pengelolaan Sampah Bank Sampah",
router.push(tab.href) value: "listpengelolaansampahbanksampah",
} href: "/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah",
setActiveTab(value) icon: <IconTrash size={18} stroke={1.8} />,
tooltip: "Kelola data pengelolaan sampah bank sampah",
},
{
label: "Keterangan Bank Sampah Terdekat",
value: "keteranganbanksampahterdekat",
href: "/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat",
icon: <IconRecycle size={18} stroke={1.8} />,
tooltip: "Kelola data bank sampah terdekat",
},
];
const currentTab = tabs.find(tab => tab.href === pathname);
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value);
if (tab) {
router.push(tab.href);
} }
setActiveTab(value);
};
useEffect(() => { useEffect(() => {
const match = tabs.find(tab => tab.href === pathname) const match = tabs.find(tab => tab.href === pathname);
if (match) { if (match) {
setActiveTab(match.value) setActiveTab(match.value);
} }
}, [pathname]) }, [pathname]);
return ( return (
<Stack> <Stack gap="md">
<Title order={3}>Layanan Online Desa</Title> <Title order={3} mb="sm">Pengelolaan Sampah Bank Sampah</Title>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}> <Tabs
<TabsList p={"xs"} bg={"#BBC8E7FF"}> value={activeTab}
{tabs.map((e, i) => ( onChange={handleTabChange}
<TabsTab key={i} value={e.value}>{e.label}</TabsTab> variant="pills"
))} radius="md"
</TabsList> >
{tabs.map((e, i) => ( <TabsList>
<TabsPanel key={i} value={e.value}> {tabs.map((tab) => (
{/* Konten dummy, bisa diganti tergantung routing */} <Tooltip
<></> key={tab.value}
</TabsPanel> label={tab.tooltip}
))} position="top"
</Tabs> withArrow
{children} transitionProps={{ transition: 'pop', duration: 300 }}
</Stack> >
); <TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
padding: '10px 20px',
height: 'auto',
minHeight: 44,
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
<TabsPanel
value={activeTab || ''}
pt="lg"
style={{
minHeight: '60vh',
}}
>
{children}
</TabsPanel>
</Tabs>
</Stack>
);
} }
export default LayoutTabsPengelolaanSampahBankSampah; export default LayoutTabsPengelolaanSampahBankSampah;

View File

@@ -1,8 +1,8 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client';
import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah'; import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
@@ -64,63 +64,97 @@ function EditKeteranganBankSampahTerdekat() {
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
if (!formData.name.trim()) {
return toast.error('Nama bank sampah harus diisi');
}
if (!formData.alamat.trim()) {
return toast.error('Alamat harus diisi');
}
if (!formData.namaTempatMaps.trim()) {
return toast.error('Nama tempat di Maps harus diisi');
}
if (!markerPosition) {
return toast.error('Silakan pilih lokasi di peta');
}
keteranganState.edit.form = { keteranganState.edit.form = {
...keteranganState.edit.form, ...keteranganState.edit.form,
name: formData.name.trim(), name: formData.name.trim(),
alamat: formData.alamat.trim(), alamat: formData.alamat.trim(),
namaTempatMaps: formData.namaTempatMaps.trim(), namaTempatMaps: formData.namaTempatMaps.trim(),
lat: formData.lat, lat: markerPosition.lat,
lng: formData.lng, lng: markerPosition.lng,
} };
await keteranganState.edit.update(); await keteranganState.edit.update();
toast.success('Data bank sampah berhasil diperbarui');
keteranganState.findUnique.data = null; keteranganState.findUnique.data = null;
router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat"); router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat");
} catch (error) { } catch (error) {
console.error("Error updating pengelolaan sampah:", error); console.error("Error updating pengelolaan sampah:", error);
toast.error("Gagal memuat data pengelolaan sampah"); toast.error(error instanceof Error ? error.message : 'Gagal memperbarui data bank sampah');
} }
} }
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> <Group mb="md">
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Tooltip label="Kembali ke halaman sebelumnya" color="blue" withArrow>
<IconArrowBack color={colors['blue-button']} size={25} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Button> <IconArrowBack color={colors['blue-button']} size={24} />
</Box> </Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Bank Sampah Terdekat
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Paper
<Stack gap={"xs"}> w={{ base: '100%', md: '50%' }}
<Title order={4}>Edit Keterangan Bank Sampah Terdekat</Title> bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label="Nama Bank Sampah"
placeholder="Masukkan nama bank sampah"
value={formData.name} value={formData.name}
onChange={(val) => setFormData({ ...formData, name: val.target.value })} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fw="bold" fz="sm">Nama Bank Sampah Terdekat</Text>} required
placeholder='Masukkan nama Bank Sampah Terdekat'
/> />
<TextInput <TextInput
label="Alamat"
placeholder="Masukkan alamat lengkap"
value={formData.alamat} value={formData.alamat}
onChange={(val) => setFormData({ ...formData, alamat: val.target.value })} onChange={(e) => setFormData({ ...formData, alamat: e.target.value })}
label={<Text fw="bold" fz="sm">Alamat</Text>} required
placeholder='Masukkan alamat Bank Sampah'
/> />
<TextInput <TextInput
label="Nama Tempat di Maps"
placeholder="Masukkan nama tempat yang terdaftar di Google Maps"
value={formData.namaTempatMaps} value={formData.namaTempatMaps}
onChange={(val) => setFormData({ ...formData, namaTempatMaps: val.target.value })} onChange={(e) => setFormData({ ...formData, namaTempatMaps: e.target.value })}
label={<Text fw="bold" fz="sm">Nama Tempat Maps</Text>} required
placeholder='Masukkan nama tempat maps Bank Sampah'
/> />
<Box> <Box>
<Text fw="bold" fz="sm">Pilih Lokasi di Peta</Text> <Text fw="bold" fz="sm" mb={6}>
<Box style={{ height: 300, width: '100%' }}> Pilih Lokasi di Peta
</Text>
<Text fz="xs" c="dimmed" mb={4}>
Klik pada peta untuk menandai lokasi
</Text>
<Box style={{ height: 300, width: '100%', borderRadius: '8px', overflow: 'hidden' }}>
<LeafletMapEdit <LeafletMapEdit
key={markerPosition?.lat ?? 'default'} key={markerPosition?.lat ?? 'default'}
initialPosition={markerPosition || { lat: -8.65, lng: 115.2 }} initialPosition={markerPosition || { lat: -8.65, lng: 115.2 }}
onChange={(pos) => { onChange={(pos) => {
setMarkerPosition(pos); setMarkerPosition(pos);
setFormData((prev) => ({ setFormData(prev => ({
...prev, ...prev,
lat: pos.lat, lat: pos.lat,
lng: pos.lng, lng: pos.lng,
@@ -128,9 +162,26 @@ function EditKeteranganBankSampahTerdekat() {
}} }}
/> />
</Box> </Box>
{markerPosition && (
<Text fz="xs" mt={4} c="green">
Lokasi dipilih: {markerPosition.lat.toFixed(6)}, {markerPosition.lng.toFixed(6)}
</Text>
)}
</Box> </Box>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button> <Group justify="right" mt="md">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -3,132 +3,148 @@
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah'; import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { Anchor, Box, Button, Flex, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; import { IconArrowLeft, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import dynamic from 'next/dynamic';
import dynamic from 'next/dynamic'
const LeafletMap = dynamic(() => import('@/app/admin/(dashboard)/_com/leafletMapCreate'), { const LeafletMap = dynamic(() => import('@/app/admin/(dashboard)/_com/leafletMapCreate'), {
ssr: false ssr: false
}) });
function DetailKeteranganBankSampahTerdekat() { function DetailKeteranganBankSampahTerdekat() {
const router = useRouter(); const router = useRouter();
const keteranganState = useProxy(pengelolaanSampahState.keteranganSampah) const keteranganState = useProxy(pengelolaanSampahState.keteranganSampah);
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null);
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false);
const params = useParams() const params = useParams();
useEffect(() => { useEffect(() => {
keteranganState.findUnique.load(params?.id as string) keteranganState.findUnique.load(params?.id as string);
}, []) }, []);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
keteranganState.delete.byId(selectedId) keteranganState.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat") router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat");
} }
} };
if (!keteranganState.findUnique.data) { if (!keteranganState.findUnique.data) {
return ( return (
<Stack py={10}> <Stack p="md">
<Skeleton h={500} /> <Skeleton h={500} radius="md" />
</Stack> </Stack>
) );
} }
return ( return (
<Box> <Box>
<Box mb={10}> <Box mb="md">
<Button variant="subtle" onClick={() => router.back()}> <Button
<IconArrowBack color={colors['blue-button']} size={25} /> variant="light"
leftSection={<IconArrowLeft size={20} />}
onClick={() => router.back()}
radius="xl"
color="blue"
>
Kembali
</Button> </Button>
</Box> </Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}> <Paper
<Stack> w={{ base: "100%", md: "60%" }}
<Text fz={"xl"} fw={"bold"}>Detail Keterangan Bank Sampah Terdekat</Text> p="xl"
radius="lg"
withBorder
shadow="md"
style={{ background: colors['white-1'] }}
>
<Stack gap="lg">
<Title order={2} c="dark">
Detail Bank Sampah Terdekat
</Title>
<Paper bg={colors['BG-trans']} p={'md'}> <Paper p="lg" radius="md" withBorder >
<Stack gap={"xs"}> <Stack gap="md">
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Nama Bank Sampah Terdekat</Text> <Text fz="sm" c="dimmed">Nama Bank Sampah</Text>
<Text fz={"lg"}>{keteranganState.findUnique.data?.name}</Text> <Text fz="lg" fw={600}>{keteranganState.findUnique.data?.name}</Text>
</Box> </Box>
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Alamat</Text> <Text fz="sm" c="dimmed">Alamat</Text>
<Text fz={"lg"}>{keteranganState.findUnique.data?.alamat}</Text> <Text fz="lg">{keteranganState.findUnique.data?.alamat}</Text>
</Box> </Box>
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Nama Tempat Maps</Text> <Text fz="sm" c="dimmed">Nama Tempat di Maps</Text>
<Text fz={"lg"}>{keteranganState.findUnique.data?.namaTempatMaps}</Text> <Text fz="lg">{keteranganState.findUnique.data?.namaTempatMaps}</Text>
</Box> </Box>
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Peta Lokasi</Text> <Text fz="sm" c="dimmed" mb={6}>Peta Lokasi</Text>
{keteranganState.findUnique.data?.lat && keteranganState.findUnique.data?.lng ? ( {keteranganState.findUnique.data?.lat && keteranganState.findUnique.data?.lng ? (
<Box <Box style={{ height: "300px", borderRadius: "12px", overflow: "hidden" }}>
style={{
height: "300px",
}}
>
<LeafletMap <LeafletMap
defaultCenter={{ lat: keteranganState.findUnique.data.lat, lng: keteranganState.findUnique.data.lng }} defaultCenter={{ lat: keteranganState.findUnique.data.lat, lng: keteranganState.findUnique.data.lng }}
readOnly readOnly
/> />
</Box> </Box>
) : ( ) : (
<Text c="dimmed" fz="sm">Koordinat belum tersedia</Text> <Text c="dimmed" fz="sm">Belum ada koordinat</Text>
)} )}
</Box> </Box>
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Link Petunjuk Arah</Text> <Text fz="sm" c="dimmed" mb={6}>Petunjuk Arah</Text>
{keteranganState.findUnique.data?.lat && keteranganState.findUnique.data?.lng ? ( {keteranganState.findUnique.data?.lat && keteranganState.findUnique.data?.lng ? (
<a <Anchor
href={`https://www.google.com/maps/dir/?api=1&destination=${keteranganState.findUnique.data.lat},${keteranganState.findUnique.data.lng}`} href={`https://www.google.com/maps/dir/?api=1&destination=${keteranganState.findUnique.data.lat},${keteranganState.findUnique.data.lng}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
style={{ color: 'black', textDecoration: 'underline' }} underline="always"
c="blue"
> >
Buka Petunjuk Arah di Google Maps Buka di Google Maps
</a> </Anchor>
) : ( ) : (
<Text c="dimmed" fz="sm">Koordinat belum tersedia</Text> <Text c="dimmed" fz="sm">Belum ada koordinat</Text>
)} )}
</Box> </Box>
<Box> <Flex gap="sm" mt="md">
<Flex gap={"xs"}> <Button
<Button onClick={() => {
onClick={() => { if (keteranganState.findUnique.data) {
if (keteranganState.findUnique.data) { setSelectedId(keteranganState.findUnique.data.id);
setSelectedId(keteranganState.findUnique.data.id); setModalHapus(true);
setModalHapus(true); }
} }}
}} disabled={!keteranganState.findUnique.data}
disabled={!keteranganState.findUnique.data} leftSection={<IconTrash size={18} />}
color="red" color="red"
> radius="md"
<IconX size={20} /> variant='light'
</Button> >
<Button Hapus
onClick={() => router.push(`/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/${keteranganState.findUnique.data?.id}/edit`)} </Button>
color="green" <Button
> onClick={() =>
<IconEdit size={20} /> router.push(`/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/${keteranganState.findUnique.data?.id}/edit`)
</Button> }
</Flex> leftSection={<IconEdit size={18} />}
</Box> color="green"
radius="md"
variant='light'
>
Edit
</Button>
</Flex>
</Stack> </Stack>
</Paper> </Paper>
</Stack> </Stack>
@@ -138,10 +154,9 @@ function DetailKeteranganBankSampahTerdekat() {
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus keterangan bank sampah terdekat ini?" text="Apakah Anda yakin ingin menghapus data bank sampah ini?"
/> />
</Box> </Box>
); );
} }

View File

@@ -1,9 +1,10 @@
'use client' 'use client';
import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah'; import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { toast } from 'react-toastify';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -28,58 +29,107 @@ function CreateKeteranganBankSampahTerdekat() {
setMarkerPosition(null) setMarkerPosition(null)
} }
const handleSubmit = async () => { const handleSubmit = async () => {
if (markerPosition) { try {
keteranganState.create.form.lat = markerPosition.lat if (!keteranganState.create.form.name) {
keteranganState.create.form.lng = markerPosition.lng return toast.error('Nama bank sampah harus diisi');
}
if (markerPosition) {
keteranganState.create.form.lat = markerPosition.lat;
keteranganState.create.form.lng = markerPosition.lng;
} else {
return toast.error('Silakan pilih lokasi di peta');
}
await keteranganState.create.create();
toast.success('Data bank sampah berhasil ditambahkan');
resetForm();
router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat");
} catch (error) {
console.error('Error creating bank sampah:', error);
toast.error('Gagal menambahkan data bank sampah');
} }
await keteranganState.create.create()
resetForm()
router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat")
} }
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> <Group mb="md">
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Tooltip label="Kembali ke halaman sebelumnya" color="blue" withArrow>
<IconArrowBack color={colors['blue-button']} size={25} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Button> <IconArrowBack color={colors['blue-button']} size={24} />
</Box> </Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Bank Sampah Terdekat
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Paper
<Stack gap={"xs"}> w={{ base: '100%', md: '50%' }}
<Title order={4}>Create Keterangan Bank Sampah Terdekat</Title> bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label="Nama Bank Sampah"
placeholder="Masukkan nama bank sampah"
value={keteranganState.create.form.name} value={keteranganState.create.form.name}
onChange={(val) => keteranganState.create.form.name = val.target.value} onChange={(e) => (keteranganState.create.form.name = e.target.value)}
label={<Text fw="bold" fz="sm">Nama Bank Sampah Terdekat</Text>} required
placeholder='Masukkan nama Bank Sampah Terdekat'
/> />
<TextInput <TextInput
label="Alamat"
placeholder="Masukkan alamat lengkap"
value={keteranganState.create.form.alamat} value={keteranganState.create.form.alamat}
onChange={(val) => keteranganState.create.form.alamat = val.target.value} onChange={(e) => (keteranganState.create.form.alamat = e.target.value)}
label={<Text fw="bold" fz="sm">Alamat</Text>} required
placeholder='Masukkan alamat Bank Sampah'
/> />
<TextInput <TextInput
label="Nama Tempat di Maps"
placeholder="Masukkan nama tempat yang terdaftar di Google Maps"
value={keteranganState.create.form.namaTempatMaps} value={keteranganState.create.form.namaTempatMaps}
onChange={(val) => keteranganState.create.form.namaTempatMaps = val.target.value} onChange={(e) => (keteranganState.create.form.namaTempatMaps = e.target.value)}
label={<Text fw="bold" fz="sm">Nama Tempat Maps</Text>} required
placeholder='Masukkan nama tempat maps Bank Sampah'
/> />
<Box> <Box>
<Text fw="bold" fz="sm">Pilih Lokasi di Peta</Text> <Text fw="bold" fz="sm" mb={6}>
<Box style={{ height: 300, width: '100%' }}> Pilih Lokasi di Peta
</Text>
<Text fz="xs" c="dimmed" mb={4}>
Klik pada peta untuk menandai lokasi
</Text>
<Box style={{ height: 300, width: '100%', borderRadius: '8px', overflow: 'hidden' }}>
<LeafletMap <LeafletMap
onSelect={(pos) => setMarkerPosition(pos)} onSelect={(pos) => setMarkerPosition(pos)}
defaultCenter={{ lat: -8.65, lng: 115.2 }} defaultCenter={{ lat: -8.65, lng: 115.2 }}
/> />
</Box> </Box>
{markerPosition && (
<Text fz="xs" mt={4} c="green">
Lokasi dipilih: {markerPosition.lat.toFixed(6)}, {markerPosition.lng.toFixed(6)}
</Text>
)}
</Box> </Box>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button> <Group justify="right" mt="md">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -1,14 +1,12 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core'; import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import pengelolaanSampahState from '../../../_state/lingkungan/pengelolaan-sampah'; import pengelolaanSampahState from '../../../_state/lingkungan/pengelolaan-sampah';
function KeteranganBankSampahTerdekat() { function KeteranganBankSampahTerdekat() {
@@ -17,71 +15,124 @@ function KeteranganBankSampahTerdekat() {
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Keterangan Bank Sampah Terdekat' title='Keterangan Bank Sampah Terdekat'
placeholder='pencarian' placeholder='Cari nama bank sampah...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
/> />
<ListKeteranganBankSampahTerdekat search={search}/> <ListKeteranganBankSampahTerdekat search={search} />
</Box> </Box>
); );
} }
function ListKeteranganBankSampahTerdekat({ search }: { search: string }) { function ListKeteranganBankSampahTerdekat({ search }: { search: string }) {
const keteranganState = useProxy(pengelolaanSampahState.keteranganSampah) const keteranganState = useProxy(pengelolaanSampahState.keteranganSampah);
const router = useRouter(); const router = useRouter();
useEffect(() => { const {
keteranganState.findMany.load() data,
}, []) page,
totalPages,
loading,
load,
} = keteranganState.findMany;
const filteredData = (keteranganState.findMany.data || []).filter(item => { useShallowEffect(() => {
const keyword = search.toLowerCase(); load(page, 10, search);
return ( }, [page, search]);
item.name.toLowerCase().includes(keyword) ||
item.alamat.toLowerCase().includes(keyword) ||
item.namaTempatMaps.toLowerCase().includes(keyword)
);
});
if (!keteranganState.findMany.data) { const filteredData = data || [];
if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
) );
} }
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p="lg" shadow="md" radius="md">
<JudulList <Group justify="space-between" mb="md">
title='List Keterangan Bank Sampah Terdekat' <Title order={4}>Daftar Bank Sampah Terdekat</Title>
href='/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/create' <Tooltip label="Tambah Bank Sampah" withArrow>
/> <Button
<Table striped withTableBorder withRowBorders> leftSection={<IconPlus size={18} />}
<TableThead> color="blue"
<TableTr> variant="light"
<TableTh>Nama Bank Sampah Terdekat</TableTh> onClick={() => router.push('/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/create')}
<TableTh>Alamat</TableTh> >
<TableTh>Nama Tempat Maps</TableTh> Tambah Baru
<TableTh>Detail</TableTh> </Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh>Nama Bank Sampah</TableTh>
<TableTh>Alamat</TableTh>
<TableTh>Nama Tempat di Maps</TableTh>
<TableTh>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item) => ( {filteredData.length > 0 ? (
<TableTr key={item.id}> filteredData.map((item) => (
<TableTd>{item.name}</TableTd> <TableTr key={item.id}>
<TableTd>{item.alamat}</TableTd> <TableTd>
<TableTd>{item.namaTempatMaps}</TableTd> <Text fw={500}>{item.name}</Text>
<TableTd> </TableTd>
<Button onClick={() => router.push(`/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/${item.id}`)}> <TableTd>
<IconDeviceImac size={20} /> <Box w={200}>
</Button> <Text lineClamp={2} truncate="end" fz="sm">
</TableTd> {item.alamat || '-'}
</TableTr> </Text>
))} </Box>
</TableTbody> </TableTd>
</Table> <TableTd>
<Text fz="sm">
{item.namaTempatMaps || '-'}
</Text>
</TableTd>
<TableTd>
<Tooltip label="Lihat Detail" withArrow>
<Button
variant="light"
color="blue"
onClick={() => router.push(`/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/${item.id}`)}
>
<IconDeviceImac size={20} />
</Button>
</Tooltip>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4} align="center" py="xl">
<Text c="dimmed">Tidak ada data bank sampah terdekat</Text>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
{totalPages > 1 && (
<Center mt="xl">
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
total={totalPages}
siblings={1}
boundaries={1}
withEdges
/>
</Center>
)}
</Paper> </Paper>
</Box> </Box>
); );

View File

@@ -3,7 +3,7 @@
import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah'; import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah';
import SelectIconProgramEdit from '@/app/admin/(dashboard)/_com/selectIconEdit'; import SelectIconProgramEdit from '@/app/admin/(dashboard)/_com/selectIconEdit';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -65,47 +65,85 @@ function EditProgramKreatifDesa() {
...stateSampah.update.form, ...stateSampah.update.form,
name: formData.name.trim(), name: formData.name.trim(),
icon: formData.icon.trim(), icon: formData.icon.trim(),
} };
await stateSampah.update.submit(); await stateSampah.update.submit();
toast.success('Data pengelolaan sampah berhasil diperbarui!');
router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah"); router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah");
} catch (error) { } catch (error) {
console.error("Error updating pengelolaan sampah:", error); console.error("Error updating pengelolaan sampah:", error);
toast.error("Gagal memuat data pengelolaan sampah"); toast.error(error instanceof Error ? error.message : "Gagal memperbarui data pengelolaan sampah");
} }
} }
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> <Group mb="md">
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Tooltip label="Kembali ke halaman sebelumnya" color="blue" withArrow>
<IconArrowBack color={colors['blue-button']} size={25} /> <Button
</Button> variant="subtle"
</Box> onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Pengelolaan Sampah Bank Sampah
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Paper
<Stack gap={"xs"}> w={{ base: '100%', md: '50%' }}
<Title order={3}>Edit List Pengelolaan Sampah Bank Sampah</Title> bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label="Nama Pengelolaan Sampah"
placeholder="Masukkan nama pengelolaan sampah"
value={formData.name} value={formData.name}
label={<Text fz={"sm"} fw={"bold"}>Nama List Pengelolaan Sampah Bank Sampah</Text>} onChange={(e) => {
placeholder="masukkan nama list pengelolaan sampah bank sampah" const value = e.target.value;
onChange={(val) => { setFormData(prev => ({
setFormData({ ...prev,
...formData, name: value
name: val.target.value }));
}) stateSampah.update.form.name = value;
}} }}
required
/> />
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Ikon List Pengelolaan Sampah Bank Sampah</Text> <Text fw="bold" fz="sm" mb={6}>
Pilih Ikon
</Text>
<SelectIconProgramEdit <SelectIconProgramEdit
value={formData.icon as IconKey} value={formData.icon as IconKey}
onChange={(value) => { onChange={(value) => {
setFormData((prev) => ({ ...prev, icon: value })); setFormData(prev => ({ ...prev, icon: value }));
stateSampah.update.form.icon = value; stateSampah.update.form.icon = value;
}} }}
/> />
</Box> </Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
<Group justify="right" mt="md">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

@@ -1,8 +1,8 @@
'use client' 'use client';
import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah'; import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah';
import SelectIconProgram from '@/app/admin/(dashboard)/_com/selectIcon'; import SelectIconProgram from '@/app/admin/(dashboard)/_com/selectIcon';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
@@ -11,43 +11,83 @@ import { useProxy } from 'valtio/utils';
function CreatePengelolaanSampahBankSampah() { function CreatePengelolaanSampahBankSampah() {
const stateCreate = useProxy(pengelolaanSampahState.pengelolaanSampah) const stateCreate = useProxy(pengelolaanSampahState.pengelolaanSampah);
const router = useRouter(); const router = useRouter();
const resetForm = () => { const resetForm = () => {
stateCreate.create.form = { stateCreate.create.form = {
name: "", name: "",
icon: "", icon: "",
} };
} };
const handleSubmit = async () => { const handleSubmit = async () => {
await stateCreate.create.create(); try {
resetForm(); await stateCreate.create.create();
router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah") resetForm();
} router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah");
return ( } catch (error) {
<Box> console.error('Error creating pengelolaan sampah:', error);
<Box mb={10}> }
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> };
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> return (
<Stack gap={"xs"}> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Title order={3}>Create List Pengelolaan Sampah Bank Sampah</Title> <Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" color="blue" position="bottom">
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Pengelolaan Sampah Bank Sampah
</Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label={<Text fz={"sm"} fw={"bold"}>Nama Pengelolaan Sampah Bank Sampah</Text>} label="Nama Pengelolaan Sampah"
placeholder="masukkan nama pengelolaan sampah bank sampah" placeholder="Masukkan nama pengelolaan sampah"
onChange={(val) => stateCreate.create.form.name = val.target.value} value={stateCreate.create.form.name || ''}
onChange={(e) => (stateCreate.create.form.name = e.target.value)}
required
/> />
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Ikon Pengelolaan Sampah Bank Sampah</Text> <Text fw="bold" fz="sm" mb={6}>
<SelectIconProgram onChange={(value) => stateCreate.create.form.icon = value} /> Pilih Ikon
</Text>
<SelectIconProgram
onChange={(value) => (stateCreate.create.form.icon = value)}
/>
</Box> </Box>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button> <Group justify="right" mt="md">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -1,26 +1,24 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core'; import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconChartLine, IconClipboardTextFilled, IconEdit, IconLeaf, IconRecycle, IconScale, IconSearch, IconTent, IconTrashFilled, IconTrophy, IconTruckFilled, IconX } from '@tabler/icons-react'; import { IconChartLine, IconClipboardTextFilled, IconEdit, IconLeaf, IconPlus, IconRecycle, IconScale, IconSearch, IconTent, IconTrashFilled, IconTrophy, IconTruckFilled } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import pengelolaanSampahState from '../../../_state/lingkungan/pengelolaan-sampah'; import pengelolaanSampahState from '../../../_state/lingkungan/pengelolaan-sampah';
import React from 'react'; import React from 'react';
function PengelolaanSampahBankSampah() { function PengelolaanSampahBankSampah() {
const [search, setSearch] = useState("") const [search, setSearch] = useState("");
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title='List Pengelolaan Sampah Bank Sampah' title='List Pengelolaan Sampah Bank Sampah'
placeholder='pencarian' placeholder='Cari pengelolaan sampah...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -36,9 +34,18 @@ function ListPengelolaanSampahBankSampah({ search }: { search: string }) {
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter() const router = useRouter()
const {
data,
page,
totalPages,
loading,
load,
} = stateList.findMany
useShallowEffect(() => { useShallowEffect(() => {
stateList.findMany.load() load(page, 10, search)
}, []) }, [page, search])
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
@@ -48,15 +55,9 @@ function ListPengelolaanSampahBankSampah({ search }: { search: string }) {
} }
} }
const filteredData = (stateList.findMany.data || []).filter(item => { const filteredData = data || []
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword)
|| item.icon.toLowerCase().includes(keyword)
);
});
const iconMap: Record<string, React.FC<any>> = { const iconMap: Record<string, React.FC<{ size: number; style?: React.CSSProperties }>> = {
ekowisata: IconLeaf, ekowisata: IconLeaf,
kompetisi: IconTrophy, kompetisi: IconTrophy,
wisata: IconTent, wisata: IconTent,
@@ -68,7 +69,7 @@ function ListPengelolaanSampahBankSampah({ search }: { search: string }) {
trash: IconTrashFilled, trash: IconTrashFilled,
}; };
if (!stateList.findMany.data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton h={500} />
@@ -78,49 +79,107 @@ function ListPengelolaanSampahBankSampah({ search }: { search: string }) {
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p="lg" shadow="md" radius="md">
<JudulList <Group justify="space-between" mb="md">
title='List Pengelolaan Sampah Bank Sampah' <Title order={4}>Daftar Pengelolaan Sampah Bank Sampah</Title>
href='/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/create' <Tooltip label="Tambah Pengelolaan Sampah" withArrow>
/> <Button
<Table striped withTableBorder withRowBorders> leftSection={<IconPlus size={18} />}
<TableThead> color="blue"
<TableTr> variant="light"
<TableTh>Nama Pengelolaan Sampah</TableTh> onClick={() => router.push('/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/create')}
<TableTh>Icon</TableTh> >
<TableTh>Edit</TableTh> Tambah Baru
<TableTh>Delete</TableTh> </Button>
</TableTr> </Tooltip>
</TableThead> </Group>
<TableTbody>
{filteredData.map((item) => ( <Box style={{ overflowX: 'auto' }}>
<TableTr key={item.id}> <Table highlightOnHover>
<TableTd>{item.name}</TableTd> <TableThead>
<TableTd style={{ width: '10%' }}> <TableTr>
{iconMap[item.icon] && ( <TableTh>Nama Pengelolaan Sampah</TableTh>
<Box title={item.icon}> <TableTh>Icon</TableTh>
{React.createElement(iconMap[item.icon], { size: 24 })} <TableTh>Edit</TableTh>
</Box> <TableTh>Delete</TableTh>
)}
</TableTd>
<TableTd>
<Button color="green" onClick={() => router.push(`/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button color="red" onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconX size={20} />
</Button>
</TableTd>
</TableTr> </TableTr>
))} </TableThead>
</TableTbody> <TableTbody>
</Table> {filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text lineClamp={1} truncate="end" fw={500}>{item.name}</Text>
</TableTd>
<TableTd>
{iconMap[item.icon] ? (
<Tooltip label={item.icon} withArrow>
<Box>
{React.createElement(iconMap[item.icon], {
size: 24,
style: { color: colors['blue-button'] }
})}
</Box>
</Tooltip>
) : (
<Text c="dimmed" fz="sm">-</Text>
)}
</TableTd>
<TableTd>
<Tooltip label="Edit" withArrow>
<Button
variant="light"
color="blue"
size="sm"
onClick={() => router.push(`/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/${item.id}`)}
>
<IconEdit size={18} />
</Button>
</Tooltip>
</TableTd>
<TableTd>
<Tooltip label="Hapus" withArrow>
<Button
variant="light"
color="red"
size="sm"
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrashFilled size={18} />
</Button>
</Tooltip>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={3} align="center" py="xl">
<Text c="dimmed">Tidak ada data pengelolaan sampah</Text>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
{totalPages > 1 && (
<Center mt="xl">
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
total={totalPages}
siblings={1}
boundaries={1}
withEdges
/>
</Center>
)}
</Paper> </Paper>
{/* Modal Konfirmasi Hapus */} {/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}

View File

@@ -0,0 +1,144 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import beasiswaDesaState from '@/app/admin/(dashboard)/_state/pendidikan/beasiswa-desa';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } 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 EditProgramKreatifDesa() {
const state = useProxy(beasiswaDesaState.keunggulanProgram)
const params = useParams()
const router = useRouter();
const [formData, setFormData] = useState({
judul: '',
deskripsi: '',
})
useEffect(() => {
const loadProgramKreatif = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await state.update.load(id);
if (data) {
// ⬇️ FIX PENTING: tambahkan ini
state.update.id = id;
state.update.form = {
judul: data.judul,
deskripsi: data.deskripsi,
};
setFormData({
judul: data.judul,
deskripsi: data.deskripsi,
});
}
} catch (error) {
console.error("Error loading pengelolaan sampah:", error);
toast.error("Gagal memuat data pengelolaan sampah");
}
}
loadProgramKreatif();
}, [params?.id]);
const handleSubmit = async () => {
try {
state.update.form = {
...state.update.form,
judul: formData.judul.trim(),
deskripsi: formData.deskripsi.trim(),
};
await state.update.update();
toast.success('Data keunggulan program berhasil diperbarui!');
router.push("/admin/pendidikan/beasiswa-desa/keunggulan-program");
} catch (error) {
console.error("Error updating keunggulan program:", error);
toast.error(error instanceof Error ? error.message : "Gagal memperbarui data keunggulan program");
}
}
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" color="blue" withArrow>
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Keunggulan Program
</Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label="Judul"
placeholder="Masukkan judul"
value={formData.judul}
onChange={(e) => {
const value = e.target.value;
setFormData(prev => ({
...prev,
judul: value
}));
state.update.form.judul = value;
}}
required
/>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<EditEditor
value={state.update.form.deskripsi}
onChange={(htmlContent) => {
state.update.form.deskripsi = htmlContent;
}}
/>
</Box>
<Group justify="right" mt="md">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditProgramKreatifDesa;

View File

@@ -0,0 +1,101 @@
'use client';
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import beasiswaDesaState from '@/app/admin/(dashboard)/_state/pendidikan/beasiswa-desa';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
function CreateKeunggulanProgram() {
const stateCreate = useProxy(beasiswaDesaState.keunggulanProgram);
const router = useRouter();
const resetForm = () => {
stateCreate.create.form = {
judul: "",
deskripsi: "",
};
};
const handleSubmit = async () => {
try {
await stateCreate.create.create();
resetForm();
router.push("/admin/pendidikan/beasiswa-desa/keunggulan-program");
} catch (error) {
console.error('Error creating keunggulan program:', error);
}
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" color="blue" position="bottom">
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Keunggulan Program
</Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label="Judul"
placeholder="Masukkan judul"
value={stateCreate.create.form.judul || ''}
onChange={(e) => (stateCreate.create.form.judul = e.target.value)}
required
/>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<CreateEditor
value={stateCreate.create.form.deskripsi}
onChange={(htmlContent) => {
stateCreate.create.form.deskripsi = htmlContent;
}}
/>
</Box>
<Group justify="right" mt="md">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateKeunggulanProgram;

View File

@@ -1,71 +1,168 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrashFilled } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import beasiswaDesaState from '../../../_state/pendidikan/beasiswa-desa';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function BeasiswaDesa() { function KeunggulanProgram() {
const [search, setSearch] = useState("");
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Keunggulan Program' title='List Keunggulan Program'
placeholder='pencarian' placeholder='Cari keunggulan program...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/> />
<ListBeasiswaDesa/> <ListKeunggulanProgram search={search} />
</Box> </Box>
); );
} }
function ListBeasiswaDesa() { function ListKeunggulanProgram({ search }: { search: string }) {
const router = useRouter(); const stateList = useProxy(beasiswaDesaState.keunggulanProgram)
const router = useRouter()
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const {
data,
page,
totalPages,
loading,
load,
} = stateList.findMany
useShallowEffect(() => {
load(page, 10, search)
}, [page, search])
const handleHapus = () => {
if (selectedId) {
stateList.delete.delete(selectedId)
setModalHapus(false)
setSelectedId(null)
}
}
const filteredData = data || []
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p="md"> <Paper bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Stack> <Group justify="space-between" mb="md">
<Title order={4}>List Beasiswa Desa</Title> <Title order={4}>Daftar Keunggulan Program</Title>
<Box style={{overflowX: 'auto'}}> <Tooltip label="Tambah Keunggulan Program" withArrow>
<Table striped withRowBorders withTableBorder style={{minWidth: '700px'}}> <Button
<TableThead> leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/pendidikan/beasiswa-desa/keunggulan-program/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh>Nama Keunggulan Program</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text lineClamp={1} truncate="end" fw={500}>{item.judul}</Text>
</TableTd>
<TableTd>
<Text lineClamp={1} truncate="end" fw={500} dangerouslySetInnerHTML={{ __html: item.deskripsi }}></Text>
</TableTd>
<TableTd>
<Tooltip label="Edit" withArrow>
<Button
variant="light"
color="blue"
size="sm"
onClick={() => router.push(`/admin/pendidikan/beasiswa-desa/keunggulan-program/${item.id}`)}
>
<IconEdit size={18} />
</Button>
</Tooltip>
</TableTd>
<TableTd>
<Tooltip label="Hapus" withArrow>
<Button
variant="light"
color="red"
size="sm"
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrashFilled size={18} />
</Button>
</Tooltip>
</TableTd>
</TableTr>
))
) : (
<TableTr> <TableTr>
<TableTh>Nomor</TableTh> <TableTd colSpan={3} align="center" py="xl">
<TableTh>Nama Lengkap</TableTh> <Text c="dimmed">Tidak ada data pengelolaan sampah</Text>
<TableTh>Nomor Telepon</TableTh>
<TableTh>Email</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>1</Text>
</Box>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>Nama Lengkap</Text>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>Nomor Telepon</Text>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>Email</Text>
</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/pendidikan/beasiswa-desa/detail')}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
</TableTbody> )}
</Table> </TableTbody>
</Box> </Table>
</Stack> </Box>
{totalPages > 1 && (
<Center mt="xl">
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
total={totalPages}
siblings={1}
boundaries={1}
withEdges
/>
</Center>
)}
</Paper> </Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus pengelolaan sampah bank sampah ini?'
/>
</Box> </Box>
) );
} }
export default BeasiswaDesa; export default KeunggulanProgram;

View File

@@ -12,22 +12,22 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
{ {
label: "Jenjang Pendidikan", label: "Jenjang Pendidikan",
value: "jenjangPendidikan", value: "jenjangPendidikan",
href: "/admin/pendidikan/info-sekolah-paud/jenjang-pendidikan" href: "/admin/pendidikan/info-sekolah/jenjang-pendidikan"
}, },
{ {
label: "Lembaga", label: "Lembaga",
value: "lembaga", value: "lembaga",
href: "/admin/pendidikan/info-sekolah-paud/lembaga" href: "/admin/pendidikan/info-sekolah/lembaga"
}, },
{ {
label: "Siswa", label: "Siswa",
value: "siswa", value: "siswa",
href: "/admin/pendidikan/info-sekolah-paud/siswa" href: "/admin/pendidikan/info-sekolah/siswa"
}, },
{ {
label: "Pengajar", label: "Pengajar",
value: "pengajar", value: "pengajar",
href: "/admin/pendidikan/info-sekolah-paud/pengajar" href: "/admin/pendidikan/info-sekolah/pengajar"
}, },
]; ];
const curentTab = tabs.find(tab => tab.href === pathname) const curentTab = tabs.find(tab => tab.href === pathname)

View File

@@ -61,7 +61,7 @@ function EditJenjangPendidikan() {
const success = await stateJenjang.edit.update(); const success = await stateJenjang.edit.update();
if (success) { if (success) {
router.push("/admin/pendidikan/info-sekolah-paud/jenjang-pendidikan"); router.push("/admin/pendidikan/info-sekolah/jenjang-pendidikan");
} }
} catch (error) { } catch (error) {
console.error("Error updating jenjang pendidikan:", error); console.error("Error updating jenjang pendidikan:", error);

View File

@@ -25,7 +25,7 @@ function CreateJenjangPendidikan() {
const handleSubmit = async () => { const handleSubmit = async () => {
await stateJenjang.create.create(); await stateJenjang.create.create();
resetForm(); resetForm();
router.push("/admin/pendidikan/info-sekolah-paud/jenjang-pendidikan") router.push("/admin/pendidikan/info-sekolah/jenjang-pendidikan")
} }
return ( return (

View File

@@ -66,7 +66,7 @@ function ListJenjangPendidikan({ search }: { search: string }) {
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p={'md'}>
<JudulList <JudulList
title='List Jenjang Pendidikan' title='List Jenjang Pendidikan'
href='/admin/pendidikan/info-sekolah-paud/jenjang-pendidikan/create' href='/admin/pendidikan/info-sekolah/jenjang-pendidikan/create'
/> />
<Table striped withTableBorder withRowBorders> <Table striped withTableBorder withRowBorders>
<TableThead> <TableThead>
@@ -81,7 +81,7 @@ function ListJenjangPendidikan({ search }: { search: string }) {
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd>{item.nama}</TableTd> <TableTd>{item.nama}</TableTd>
<TableTd> <TableTd>
<Button color="green" onClick={() => router.push(`/admin/pendidikan/info-sekolah-paud/jenjang-pendidikan/${item.id}`)}> <Button color="green" onClick={() => router.push(`/admin/pendidikan/info-sekolah/jenjang-pendidikan/${item.id}`)}>
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</TableTd> </TableTd>

View File

@@ -50,7 +50,7 @@ export default function EditLembaga() {
if (result) { if (result) {
toast.success("Data berhasil diperbarui"); toast.success("Data berhasil diperbarui");
router.push('/admin/pendidikan/info-sekolah-paud/lembaga'); router.push('/admin/pendidikan/info-sekolah/lembaga');
} }
}; };

View File

@@ -29,7 +29,7 @@ function DetailLembaga() {
detailState.delete.byId(selectedId) detailState.delete.byId(selectedId)
setModalHapus(false) setModalHapus(false)
setSelectedId(null) setSelectedId(null)
router.push("/admin/pendidikan/info-sekolah-paud/lembaga") router.push("/admin/pendidikan/info-sekolah/lembaga")
} }
} }
@@ -86,7 +86,7 @@ function DetailLembaga() {
<Button <Button
onClick={() => { onClick={() => {
if (detailState.findUnique.data) { if (detailState.findUnique.data) {
router.push(`/admin/pendidikan/info-sekolah-paud/lembaga/${detailState.findUnique.data.id}/edit`); router.push(`/admin/pendidikan/info-sekolah/lembaga/${detailState.findUnique.data.id}/edit`);
} }
}} }}
disabled={!detailState.findUnique.data} disabled={!detailState.findUnique.data}

View File

@@ -27,7 +27,7 @@ function CreateLembaga() {
const handleSubmit = async () => { const handleSubmit = async () => {
await stateLembaga.create.create(); await stateLembaga.create.create();
resetForm(); resetForm();
router.push("/admin/pendidikan/info-sekolah-paud/lembaga") router.push("/admin/pendidikan/info-sekolah/lembaga")
} }
return ( return (

View File

@@ -57,7 +57,7 @@ function ListLembaga({ search }: { search: string }) {
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p={'md'}>
<JudulList <JudulList
title='List Lembaga' title='List Lembaga'
href='/admin/pendidikan/info-sekolah-paud/lembaga/create' href='/admin/pendidikan/info-sekolah/lembaga/create'
/> />
<Table striped withTableBorder withRowBorders> <Table striped withTableBorder withRowBorders>
<TableThead> <TableThead>
@@ -73,7 +73,7 @@ function ListLembaga({ search }: { search: string }) {
<TableTd>{item.nama}</TableTd> <TableTd>{item.nama}</TableTd>
<TableTd>{item.jenjangPendidikan?.nama}</TableTd> <TableTd>{item.jenjangPendidikan?.nama}</TableTd>
<TableTd> <TableTd>
<Button color="blue" onClick={() => router.push(`/admin/pendidikan/info-sekolah-paud/lembaga/${item.id}`)}> <Button color="blue" onClick={() => router.push(`/admin/pendidikan/info-sekolah/lembaga/${item.id}`)}>
<IconDeviceImac size={20} /> <IconDeviceImac size={20} />
</Button> </Button>
</TableTd> </TableTd>

View File

@@ -55,7 +55,7 @@ function EditPengajar() {
lembagaId: formData.lembagaId.trim(), lembagaId: formData.lembagaId.trim(),
} }
await pengajarState.edit.update() await pengajarState.edit.update()
router.push("/admin/pendidikan/info-sekolah-paud/pengajar"); router.push("/admin/pendidikan/info-sekolah/pengajar");
} catch (error) { } catch (error) {
console.error("Error updating pengajar:", error); console.error("Error updating pengajar:", error);
toast.error("Terjadi kesalahan saat memperbarui pengajar"); toast.error("Terjadi kesalahan saat memperbarui pengajar");

View File

@@ -29,7 +29,7 @@ function DetailPengajar() {
detailState.delete.byId(selectedId) detailState.delete.byId(selectedId)
setModalHapus(false) setModalHapus(false)
setSelectedId(null) setSelectedId(null)
router.push("/admin/pendidikan/info-sekolah-paud/pengajar") router.push("/admin/pendidikan/info-sekolah/pengajar")
} }
} }
@@ -78,7 +78,7 @@ function DetailPengajar() {
<Button <Button
onClick={() => { onClick={() => {
if (detailState.findUnique.data) { if (detailState.findUnique.data) {
router.push(`/admin/pendidikan/info-sekolah-paud/pengajar/${detailState.findUnique.data.id}/edit`); router.push(`/admin/pendidikan/info-sekolah/pengajar/${detailState.findUnique.data.id}/edit`);
} }
}} }}
disabled={!detailState.findUnique.data} disabled={!detailState.findUnique.data}

View File

@@ -28,7 +28,7 @@ function CreatePengajar() {
await stateCreate.create.create(); await stateCreate.create.create();
resetForm(); resetForm();
router.push("/admin/pendidikan/info-sekolah-paud/pengajar") router.push("/admin/pendidikan/info-sekolah/pengajar")
} }
return ( return (
<Box> <Box>

View File

@@ -55,7 +55,7 @@ function ListPengajar({ search }: { search: string }) {
<Stack> <Stack>
<JudulList <JudulList
title='List Pengajar' title='List Pengajar'
href='/admin/pendidikan/info-sekolah-paud/pengajar/create' href='/admin/pendidikan/info-sekolah/pengajar/create'
/> />
<Box style={{ overflowX: 'auto' }}> <Box style={{ overflowX: 'auto' }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}> <Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
@@ -72,7 +72,7 @@ function ListPengajar({ search }: { search: string }) {
<TableTd>{item.nama}</TableTd> <TableTd>{item.nama}</TableTd>
<TableTd>{item.lembaga.nama}</TableTd> <TableTd>{item.lembaga.nama}</TableTd>
<TableTd> <TableTd>
<Button onClick={() => router.push(`/admin/pendidikan/info-sekolah-paud/pengajar/${item.id}`)}> <Button onClick={() => router.push(`/admin/pendidikan/info-sekolah/pengajar/${item.id}`)}>
<IconDeviceImacCog size={25} /> <IconDeviceImacCog size={25} />
</Button> </Button>
</TableTd> </TableTd>

View File

@@ -55,7 +55,7 @@ function EditSiswa() {
lembagaId: formData.lembagaId.trim(), lembagaId: formData.lembagaId.trim(),
} }
await siswaState.edit.update() await siswaState.edit.update()
router.push("/admin/pendidikan/info-sekolah-paud/siswa"); router.push("/admin/pendidikan/info-sekolah/siswa");
} catch (error) { } catch (error) {
console.error("Error updating siswa:", error); console.error("Error updating siswa:", error);
toast.error("Terjadi kesalahan saat memperbarui siswa"); toast.error("Terjadi kesalahan saat memperbarui siswa");

View File

@@ -29,7 +29,7 @@ function DetailSiswa() {
detailState.delete.byId(selectedId) detailState.delete.byId(selectedId)
setModalHapus(false) setModalHapus(false)
setSelectedId(null) setSelectedId(null)
router.push("/admin/pendidikan/info-sekolah-paud/siswa") router.push("/admin/pendidikan/info-sekolah/siswa")
} }
} }
@@ -78,7 +78,7 @@ function DetailSiswa() {
<Button <Button
onClick={() => { onClick={() => {
if (detailState.findUnique.data) { if (detailState.findUnique.data) {
router.push(`/admin/pendidikan/info-sekolah-paud/siswa/${detailState.findUnique.data.id}/edit`); router.push(`/admin/pendidikan/info-sekolah/siswa/${detailState.findUnique.data.id}/edit`);
} }
}} }}
disabled={!detailState.findUnique.data} disabled={!detailState.findUnique.data}

View File

@@ -28,7 +28,7 @@ function CreateSiswa() {
await stateCreate.create.create(); await stateCreate.create.create();
resetForm(); resetForm();
router.push("/admin/pendidikan/info-sekolah-paud/siswa") router.push("/admin/pendidikan/info-sekolah/siswa")
} }
return ( return (
<Box> <Box>

View File

@@ -55,7 +55,7 @@ function ListSiswa({ search }: { search: string }) {
<Stack> <Stack>
<JudulList <JudulList
title='List Siswa' title='List Siswa'
href='/admin/pendidikan/info-sekolah-paud/siswa/create' href='/admin/pendidikan/info-sekolah/siswa/create'
/> />
<Box style={{ overflowX: 'auto' }}> <Box style={{ overflowX: 'auto' }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}> <Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
@@ -72,7 +72,7 @@ function ListSiswa({ search }: { search: string }) {
<TableTd>{item.nama}</TableTd> <TableTd>{item.nama}</TableTd>
<TableTd>{item.lembaga.nama}</TableTd> <TableTd>{item.lembaga.nama}</TableTd>
<TableTd> <TableTd>
<Button onClick={() => router.push(`/admin/pendidikan/info-sekolah-paud/siswa/${item.id}`)}> <Button onClick={() => router.push(`/admin/pendidikan/info-sekolah/siswa/${item.id}`)}>
<IconDeviceImacCog size={25} /> <IconDeviceImacCog size={25} />
</Button> </Button>
</TableTd> </TableTd>

View File

@@ -343,8 +343,8 @@ export const navBar = [
children: [ children: [
{ {
id: "Pendidikan_1", id: "Pendidikan_1",
name: "Info Sekolah & PAUD", name: "Info Sekolah",
path: "/admin/pendidikan/info-sekolah-paud/jenjang-pendidikan" path: "/admin/pendidikan/info-sekolah/jenjang-pendidikan"
}, },
{ {
id: "Pendidikan_2", id: "Pendidikan_2",

View File

@@ -7,18 +7,23 @@ import {
AppShellHeader, AppShellHeader,
AppShellMain, AppShellMain,
AppShellNavbar, AppShellNavbar,
Box,
Burger, Burger,
Flex, Flex,
Group, Group,
Image, Image,
NavLink, NavLink,
ScrollArea, ScrollArea,
Text Text,
Tooltip,
rem
} from "@mantine/core"; } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks"; import { useDisclosure } from "@mantine/hooks";
import { IconChevronLeft, IconChevronRight, IconDoorExit } from "@tabler/icons-react"; import {
import _ from 'lodash'; IconChevronLeft,
IconChevronRight,
IconDoorExit,
} from "@tabler/icons-react";
import _ from "lodash";
import Link from "next/link"; import Link from "next/link";
import { useRouter, useSelectedLayoutSegments } from "next/navigation"; import { useRouter, useSelectedLayoutSegments } from "next/navigation";
import { navBar } from "./_com/list_PageAdmin"; import { navBar } from "./_com/list_PageAdmin";
@@ -26,69 +31,97 @@ import { navBar } from "./_com/list_PageAdmin";
export default function Layout({ children }: { children: React.ReactNode }) { export default function Layout({ children }: { children: React.ReactNode }) {
const [opened, { toggle }] = useDisclosure(); const [opened, { toggle }] = useDisclosure();
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true); const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
const router = useRouter() const router = useRouter();
// Normalisasi semua segmen jadi lowercase const segments = useSelectedLayoutSegments().map((s) => _.lowerCase(s));
const segments = useSelectedLayoutSegments().map(s => _.lowerCase(s));
return ( return (
<AppShell <AppShell
suppressHydrationWarning suppressHydrationWarning
header={{ height: 60 }} header={{ height: 64 }}
navbar={{ navbar={{
width: 300, width: 300,
breakpoint: 'sm', breakpoint: "sm",
collapsed: { collapsed: {
mobile: !opened, mobile: !opened,
desktop: !desktopOpened, desktop: !desktopOpened,
}, },
}} }}
padding={'md'} padding="md"
> >
<AppShellHeader bg={colors["white-1"]}> <AppShellHeader
<Group px={10} align="center"> style={{
<Flex align="center" gap={'xs'}> background: "linear-gradient(90deg, #ffffff, #f9fbff)",
borderBottom: `1px solid ${colors["blue-button"]}20`,
}}
>
<Group px="md" h="100%" justify="space-between">
<Flex align="center" gap="sm">
<Image <Image
py={5} src="/assets/images/darmasaba-icon.png"
src={'/assets/images/darmasaba-icon.png'} alt="Logo Darmasaba"
alt="" width={46}
width={50} height={46}
height={50} radius="md"
/> />
<Text fw={'bold'} c={colors["blue-button"]} fz={'lg'}> <Text
Dashboard Admin fw={700}
c={colors["blue-button"]}
fz="lg"
style={{ letterSpacing: rem(0.3) }}
>
Admin Darmasaba
</Text> </Text>
</Flex> </Flex>
{!desktopOpened && (
<ActionIcon variant="light" onClick={toggleDesktop}> <Group gap="xs">
<IconChevronRight /> {!desktopOpened && (
</ActionIcon> <Tooltip label="Buka Navigasi" position="bottom" withArrow>
)} <ActionIcon
<Burger variant="light"
opened={opened} radius="xl"
onClick={toggle} size="lg"
hiddenFrom="sm" onClick={toggleDesktop}
size={'sm'} color={colors["blue-button"]}
/> >
<Box> <IconChevronRight />
<ActionIcon onClick={() => { </ActionIcon>
router.push("/darmasaba") </Tooltip>
}} color={colors["blue-button"]} radius={'xl'}> )}
<IconDoorExit size={24} />
</ActionIcon> <Burger
</Box> opened={opened}
<ActionIcon onClick={toggle}
w={50} hiddenFrom="sm"
h={50} size="sm"
variant="transparent" color={colors["blue-button"]}
component={Link} />
href="/admin"
> <Tooltip label="Kembali ke Website Desa" position="bottom" withArrow>
</ActionIcon> <ActionIcon
onClick={() => {
router.push("/darmasaba");
}}
color={colors["blue-button"]}
radius="xl"
size="lg"
variant="gradient"
gradient={{ from: colors["blue-button"], to: "#228be6" }}
>
<IconDoorExit size={22} />
</ActionIcon>
</Tooltip>
</Group>
</Group> </Group>
</AppShellHeader> </AppShellHeader>
<AppShellNavbar c={colors["blue-button"]} component={ScrollArea}> <AppShellNavbar
<AppShell.Section> component={ScrollArea}
style={{
background: "#ffffff",
borderRight: `1px solid ${colors["blue-button"]}20`,
}}
>
<AppShell.Section p="sm">
{navBar.map((v, k) => { {navBar.map((v, k) => {
const isParentActive = segments.includes(_.lowerCase(v.name)); const isParentActive = segments.includes(_.lowerCase(v.name));
@@ -96,26 +129,42 @@ export default function Layout({ children }: { children: React.ReactNode }) {
<NavLink <NavLink
key={k} key={k}
defaultOpened={isParentActive} defaultOpened={isParentActive}
c={isParentActive ? colors["blue-button"] : "grey"} c={isParentActive ? colors["blue-button"] : "gray"}
label={ label={
<Text style={{ fontWeight: isParentActive ? "bold" : "normal" }}> <Text fw={isParentActive ? 600 : 400} fz="sm">
{v.name} {v.name}
</Text> </Text>
} }
style={{
borderRadius: rem(10),
marginBottom: rem(4),
transition: "background 150ms ease",
}}
variant="light"
active={isParentActive}
> >
{v.children.map((child, key) => { {v.children.map((child, key) => {
const isChildActive = segments.includes(_.lowerCase(child.name)); const isChildActive = segments.includes(
_.lowerCase(child.name)
);
return ( return (
<NavLink <NavLink
key={key} key={key}
href={child.path} href={child.path}
c={isChildActive ? colors["blue-button"] : "grey"} c={isChildActive ? colors["blue-button"] : "gray"}
label={ label={
<Text style={{ fontWeight: isChildActive ? "bold" : "normal" }}> <Text fw={isChildActive ? 600 : 400} fz="sm">
{child.name} {child.name}
</Text> </Text>
} }
style={{
borderRadius: rem(8),
marginBottom: rem(2),
transition: "background 150ms ease",
}}
active={isChildActive}
component={Link}
/> />
); );
})} })}
@@ -124,16 +173,35 @@ export default function Layout({ children }: { children: React.ReactNode }) {
})} })}
</AppShell.Section> </AppShell.Section>
<AppShell.Section py={20}> <AppShell.Section py="md">
<Group justify="end"> <Group justify="end" pr="sm">
<ActionIcon variant="light" onClick={toggleDesktop}> <Tooltip
<IconChevronLeft /> label={desktopOpened ? "Tutup Navigasi" : "Buka Navigasi"}
</ActionIcon> position="top"
withArrow
>
<ActionIcon
variant="light"
radius="xl"
size="lg"
onClick={toggleDesktop}
color={colors["blue-button"]}
>
<IconChevronLeft />
</ActionIcon>
</Tooltip>
</Group> </Group>
</AppShell.Section> </AppShell.Section>
</AppShellNavbar> </AppShellNavbar>
<AppShellMain bg={colors.Bg}>{children}</AppShellMain> <AppShellMain
style={{
background: "linear-gradient(180deg, #fdfdfd, #f6f9fc)",
minHeight: "100vh",
}}
>
{children}
</AppShellMain>
</AppShell> </AppShell>
); );
} }

View File

@@ -6,6 +6,7 @@ import EdukasiLingkungan from "./edukasi-lingkungan";
import KonservasiAdatBali from "./konservasi-adat-bali"; import KonservasiAdatBali from "./konservasi-adat-bali";
import KegiatanDesa from "./gotong-royong"; import KegiatanDesa from "./gotong-royong";
import KategoriKegiatan from "./gotong-royong/kategori-kegiatan"; import KategoriKegiatan from "./gotong-royong/kategori-kegiatan";
import KeteranganBankSampahTerdekat from "./pengelolaan-sampah/keterangan-bank-sampah";
const Lingkungan = new Elysia({ const Lingkungan = new Elysia({
prefix: "/api/lingkungan", prefix: "/api/lingkungan",
@@ -19,6 +20,7 @@ const Lingkungan = new Elysia({
.use(KonservasiAdatBali) .use(KonservasiAdatBali)
.use(KegiatanDesa) .use(KegiatanDesa)
.use(KategoriKegiatan) .use(KategoriKegiatan)
.use(KeteranganBankSampahTerdekat);
export default Lingkungan; export default Lingkungan;

View File

@@ -4,7 +4,6 @@ import pengelolaanSampahDelete from "./del";
import pengelolaanSampahFindMany from "./findMany"; import pengelolaanSampahFindMany from "./findMany";
import pengelolaanSampahFindUnique from "./findUnique"; import pengelolaanSampahFindUnique from "./findUnique";
import pengelolaanSampahUpdate from "./updt"; import pengelolaanSampahUpdate from "./updt";
import KeteranganBankSampahTerdekat from "./keterangan-bank-sampah";
const PengelolaanSampah = new Elysia({ const PengelolaanSampah = new Elysia({
prefix: "/pengelolaansampah", prefix: "/pengelolaansampah",
@@ -35,5 +34,4 @@ const PengelolaanSampah = new Elysia({
} }
) )
.delete("/del/:id", pengelolaanSampahDelete) .delete("/del/:id", pengelolaanSampahDelete)
.use(KeteranganBankSampahTerdekat);
export default PengelolaanSampah; export default PengelolaanSampah;

View File

@@ -1,21 +1,55 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function keteranganBankSampahTerdekatFindMany() { // Di findMany.ts
try { export default async function keteranganBankSampahTerdekatFindMany(context: Context) {
const data = await prisma.keteranganBankSampahTerdekat.findMany({ const page = Number(context.query.page) || 1;
where: { isActive: true }, const limit = Number(context.query.limit) || 10;
}); const search = (context.query.search as string) || '';
const skip = (page - 1) * limit;
return { const where: any = { isActive: true };
success: true,
message: "Success fetch keterangan bank sampah terdekat", // Tambahkan pencarian (jika ada)
data, if (search) {
}; where.OR = [
} catch (e) { { name: { contains: search, mode: 'insensitive' } },
console.error("Find many error:", e); ];
return { }
success: false,
message: "Failed fetch keterangan bank sampah terdekat", try {
}; const [data, total] = await Promise.all([
} prisma.keteranganBankSampahTerdekat.findMany({
where,
skip,
take: limit,
orderBy: { createdAt: 'desc' },
}),
prisma.keteranganBankSampahTerdekat.count({
where,
})
]);
const totalPages = Math.ceil(total / limit);
return {
success: true,
message: "Success fetch keterangan bank sampah terdekat with pagination",
data,
page,
totalPages,
total,
};
} catch (e) {
console.error("Find many paginated error:", e);
return {
success: false,
message: "Failed fetch keterangan bank sampah terdekat with pagination",
data: [],
page: 1,
totalPages: 1,
total: 0,
};
}
} }

View File

@@ -1,11 +1,56 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// /api/berita/findManyPaginated.ts
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function beasiswaPendaftarFindMany() { async function beasiswaPendaftarFindMany(context: Context) {
const data = await prisma.beasiswaPendaftar.findMany(); // Ambil parameter dari query
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || '';
const skip = (page - 1) * limit;
return { // Buat where clause
success: true, const where: any = { isActive: true };
message: "Success get all beasiswa pendaftar",
data, // Tambahkan pencarian (jika ada)
}; if (search) {
where.OR = [
{ namaLengkap: { contains: search, mode: 'insensitive' } },
{ tempatLahir: { contains: search, mode: 'insensitive' } },
{ alamatKTP: { contains: search, mode: 'insensitive' } },
{ alamatDomisili: { contains: search, mode: 'insensitive' } },
];
}
try {
// Ambil data dan total count secara paralel
const [data, total] = await Promise.all([
prisma.beasiswaPendaftar.findMany({
where,
skip,
take: limit,
orderBy: { createdAt: 'desc' },
}),
prisma.beasiswaPendaftar.count({ where }),
]);
return {
success: true,
message: "Berhasil ambil beasiswa pendaftar dengan pagination",
data,
page,
limit,
total,
totalPages: Math.ceil(total / limit),
};
} catch (e) {
console.error("Error di findMany paginated:", e);
return {
success: false,
message: "Gagal mengambil data beasiswa pendaftar",
};
}
} }
export default beasiswaPendaftarFindMany;

View File

@@ -1,10 +1,12 @@
import Elysia from "elysia"; import Elysia from "elysia";
import BeasiswaPendaftar from "./beasiswa-pendaftar"; import BeasiswaPendaftar from "./beasiswa-pendaftar";
import KeunggulanProgram from "./keunggulan-program";
const Beasiswa = new Elysia({ const Beasiswa = new Elysia({
prefix: "/beasiswa", prefix: "/beasiswa",
tags: ["Pendidikan/Beasiswa Desa"] tags: ["Pendidikan/Beasiswa Desa"]
}) })
.use(BeasiswaPendaftar) .use(BeasiswaPendaftar)
.use(KeunggulanProgram)
export default Beasiswa export default Beasiswa

View File

@@ -0,0 +1,31 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormCreate = {
judul: string;
deskripsi: string;
}
export default async function keunggulanProgramCreate(context: Context) {
const body = context.body as FormCreate;
try {
const result = await prisma.keunggulanProgram.create({
data: {
judul: body.judul,
deskripsi: body.deskripsi,
},
});
return {
success: true,
message: "Berhasil membuat data keunggulan program",
data: result,
};
} catch (error) {
console.error("Gagal membuat data keunggulan program:", error);
throw new Error("Gagal membuat data keunggulan program: " + (error as Error).message);
}
}

View File

@@ -0,0 +1,16 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function keunggulanProgramDelete(context: Context) {
const id = context.params.id as string;
await prisma.keunggulanProgram.delete({
where: { id },
});
return {
status: 200,
success: true,
message: "Success delete keunggulan program",
};
}

View File

@@ -0,0 +1,54 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// /api/berita/findManyPaginated.ts
import prisma from "@/lib/prisma";
import { Context } from "elysia";
async function keunggulanProgramFindMany(context: Context) {
// Ambil parameter dari query
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || '';
const skip = (page - 1) * limit;
// Buat where clause
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ judul: { contains: search, mode: 'insensitive' } },
{ deskripsi: { contains: search, mode: 'insensitive' } },
];
}
try {
// Ambil data dan total count secara paralel
const [data, total] = await Promise.all([
prisma.keunggulanProgram.findMany({
where,
skip,
take: limit,
orderBy: { createdAt: 'desc' },
}),
prisma.keunggulanProgram.count({ where }),
]);
return {
success: true,
message: "Berhasil ambil keunggulan program dengan pagination",
data,
page,
limit,
total,
totalPages: Math.ceil(total / limit),
};
} catch (e) {
console.error("Error di findMany paginated:", e);
return {
success: false,
message: "Gagal mengambil data keunggulan program",
};
}
}
export default keunggulanProgramFindMany;

View File

@@ -0,0 +1,46 @@
import prisma from "@/lib/prisma";
export default async function keunggulanProgramFindUnique(request: Request) {
const url = new URL(request.url);
const pathSegments = url.pathname.split('/');
const id = pathSegments[pathSegments.length - 1];
if (!id) {
return {
success: false,
message: "ID is required",
}
}
try {
if (typeof id !== 'string') {
return {
success: false,
message: "ID is required",
}
}
const data = await prisma.keunggulanProgram.findUnique({
where: { id },
});
if (!data) {
return {
success: false,
message: "Data not found",
}
}
return {
success: true,
message: "Success get keunggulan program",
data,
}
} catch (error) {
console.error("Find by ID error:", error);
return {
success: false,
message: "Gagal mengambil data: " + (error instanceof Error ? error.message : 'Unknown error'),
}
}
}

View File

@@ -0,0 +1,41 @@
import Elysia, { t } from "elysia";
import keunggulanProgramCreate from "./create";
import keunggulanProgramFindMany from "./findMany";
import keunggulanProgramFindUnique from "./findUnique";
import keunggulanProgramUpdate from "./updt";
import keunggulanProgramDelete from "./del";
const KeunggulanProgram = new Elysia({
prefix: "/keunggulanprogram",
tags: ["Pendidikan / Beasiswa Desa / Keunggulan Program"],
})
.post("/create", keunggulanProgramCreate, {
body: t.Object({
judul: t.String(),
deskripsi: t.String(),
}),
})
.get("/findMany", keunggulanProgramFindMany)
.get("/:id", async (context) => {
const response = await keunggulanProgramFindUnique(
new Request(context.request)
);
return response;
})
.put(
"/:id",
async (context) => {
const response = await keunggulanProgramUpdate(context);
return response;
},
{
body: t.Object({
judul: t.String(),
deskripsi: t.String(),
}),
}
)
.delete("/del/:id", keunggulanProgramDelete);
export default KeunggulanProgram;

View File

@@ -0,0 +1,45 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormUpdate = {
judul: string;
deskripsi: string;
}
export default async function keunggulanProramUpdate(context: Context){
const id = context.params.id as string;
const body = context.body as FormUpdate;
try {
if (typeof id !== "string") {
return {
success: false,
message: "ID is required",
};
}
const data = await prisma.keunggulanProgram.update({
where: { id },
data: {
judul: body.judul,
deskripsi: body.deskripsi,
},
});
if (!data) {
return {
success: false,
message: "Data not found",
};
}
return {
success: true,
message: "Success update keunggulan program",
data,
};
} catch (error) {
console.error("Gagal update data keunggulan program:", error);
throw new Error("Gagal update data keunggulan program: " + (error as Error).message);
}
}

View File

@@ -4,42 +4,74 @@ import prisma from "@/lib/prisma";
import { Context } from "elysia"; import { Context } from "elysia";
async function lembagaPendidikanFindMany(context: Context) { async function lembagaPendidikanFindMany(context: Context) {
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || "";
const skip = (page - 1) * limit;
// Buat where clause
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ nama: { contains: search, mode: "insensitive" } },
{ siswa: { contains: search, mode: "insensitive" } },
{ pengajar: { contains: search, mode: "insensitive" } },
{ jenjangPendidikan: { contains: search, mode: "insensitive" } },
];
}
try { try {
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || "";
const skip = (page - 1) * limit;
const jenjangPendidikanName = (context.query.jenjangPendidikanId as string) || "";
console.log('Lembaga API Query Params:', { page, limit, search, jenjangPendidikanName });
// Buat where clause
const where: any = { isActive: true };
// Filter berdasarkan jenjang pendidikan (jika ada)
if (jenjangPendidikanName) {
// Cari jenjang pendidikan berdasarkan nama
const jenjangPendidikan = await prisma.jenjangPendidikan.findFirst({
where: {
nama: {
equals: jenjangPendidikanName,
mode: 'insensitive',
},
isActive: true,
},
orderBy: { nama: 'desc' },
});
if (jenjangPendidikan) {
where.jenjangId = jenjangPendidikan.id;
} else {
// Jika tidak ditemukan, return data kosong
return {
success: true,
message: "Jenjang pendidikan tidak ditemukan",
data: [],
page,
limit,
totalPages: 0,
total: 0,
};
}
}
// Add search functionality
if (search) {
where.OR = [
{ nama: { contains: search, mode: 'insensitive' } },
{ jenjangPendidikan: { nama: { contains: search, mode: 'insensitive' } } },
];
}
const [data, total] = await Promise.all([ const [data, total] = await Promise.all([
prisma.lembaga.findMany({ prisma.lembaga.findMany({
where, where,
include: { include: {
jenjangPendidikan: true, jenjangPendidikan: true,
siswa: true,
pengajar: true,
}, },
skip, skip,
take: limit, take: limit,
orderBy: { createdAt: 'desc' }, // opsional, kalau mau urut berdasarkan waktu orderBy: { nama: 'asc' },
}), }),
prisma.lembaga.count({ prisma.lembaga.count({
where, where,
}) })
]); ]);
console.log('Fetched data count:', data.length);
console.log('Total count:', total);
return { return {
success: true, success: true,
message: "Success fetch lembaga pendidikan with pagination", message: "Success fetch lembaga pendidikan with pagination",
@@ -53,7 +85,7 @@ async function lembagaPendidikanFindMany(context: Context) {
console.error("Find many paginated error:", e); console.error("Find many paginated error:", e);
return { return {
success: false, success: false,
message: "Failed fetch lembaga pendidikan with pagination", message: `Failed fetch lembaga pendidikan: ${e instanceof Error ? e.message : 'Unknown error'}`,
}; };
} }
} }

View File

@@ -1,41 +1,82 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
// /api/berita/findManyPaginated.ts
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { Context } from "elysia"; import { Context } from "elysia";
async function pengajarFindMany(context: Context) { async function pengajarFindMany(context: Context) {
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const skip = (page - 1) * limit;
const search = (context.query.search as string) || "";
// Buat where clause
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ nama: { contains: search, mode: "insensitive" } },
{ lembaga: { contains: search, mode: "insensitive" } },
];
}
try { try {
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const skip = (page - 1) * limit;
const search = (context.query.search as string) || "";
const jenjangPendidikanName = (context.query.jenjangPendidikanId as string) || "";
console.log('Pengajar API Query Params:', { page, limit, search, jenjangPendidikanId: jenjangPendidikanName });
const where: any = { isActive: true };
// Filter berdasarkan jenjang pendidikan (jika ada)
if (jenjangPendidikanName) {
// Cari jenjang pendidikan berdasarkan nama
const jenjangPendidikan = await prisma.jenjangPendidikan.findFirst({
where: {
nama: {
equals: jenjangPendidikanName,
mode: 'insensitive'
},
isActive: true
},
orderBy: { nama: 'desc' },
});
if (jenjangPendidikan) {
where.lembaga = {
...where.lembaga,
jenjangId: jenjangPendidikan.id
};
} else {
// Jika tidak ditemukan, return data kosong
return {
success: true,
message: "Jenjang pendidikan tidak ditemukan",
data: [],
page,
limit,
totalPages: 0,
total: 0,
};
}
}
// Add search condition if search term exists
if (search) {
where.OR = [
{ nama: { contains: search, mode: 'insensitive' } },
{ lembaga: { nama: { contains: search, mode: 'insensitive' } } }
];
}
const [data, total] = await Promise.all([ const [data, total] = await Promise.all([
prisma.pengajar.findMany({ prisma.pengajar.findMany({
where, where,
include: { include: {
lembaga: true, lembaga: {
include: {
jenjangPendidikan: true
}
}
}, },
skip, skip,
take: limit, take: limit,
orderBy: { createdAt: 'desc' }, // opsional, kalau mau urut berdasarkan waktu orderBy: { nama: 'asc' },
}), }),
prisma.pengajar.count({ prisma.pengajar.count({
where, where,
}) })
]); ]);
console.log('Fetched pengajar data count:', data.length);
console.log('Total pengajar count:', total);
return { return {
success: true, success: true,
message: "Success fetch pengajar with pagination", message: "Success fetch pengajar with pagination",
@@ -45,13 +86,12 @@ async function pengajarFindMany(context: Context) {
totalPages: Math.ceil(total / limit), totalPages: Math.ceil(total / limit),
total, total,
}; };
} catch (e) { } catch (error) {
console.error("Find many paginated error:", e); console.error("Error in pengajarFindMany:", error);
return { return {
success: false, success: false,
message: "Failed fetch pengajar with pagination", message: `Failed fetch pengajar: ${error instanceof Error ? error.message : 'Unknown error'}`,
}; };
} }
} }
export default pengajarFindMany;
export default pengajarFindMany;

View File

@@ -1,41 +1,82 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
// /api/berita/findManyPaginated.ts
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { Context } from "elysia"; import { Context } from "elysia";
async function siswaFindMany(context: Context) { async function siswaFindMany(context: Context) {
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const skip = (page - 1) * limit;
const search = (context.query.search as string) || "";
// Buat where clause
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ nama: { contains: search, mode: "insensitive" } },
{ lembaga: { contains: search, mode: "insensitive" } },
];
}
try { try {
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const skip = (page - 1) * limit;
const search = (context.query.search as string) || "";
const jenjangPendidikanName = (context.query.jenjangPendidikanName as string) || "";
console.log('Siswa API Query Params:', { page, limit, search, jenjangPendidikanId: jenjangPendidikanName });
// Buat where clause
const where: any = { isActive: true };
// Filter berdasarkan jenjang pendidikan (jika ada)
if (jenjangPendidikanName) {
// Cari jenjang pendidikan berdasarkan nama
const jenjangPendidikan = await prisma.jenjangPendidikan.findFirst({
where: {
nama: {
equals: jenjangPendidikanName,
mode: 'insensitive'
},
isActive: true,
}
});
if (jenjangPendidikan) {
where.lembaga = {
...where.lembaga,
jenjangId: jenjangPendidikan.id
};
} else {
// Jika tidak ditemukan, return data kosong
return {
success: true,
message: "Jenjang pendidikan tidak ditemukan",
data: [],
page,
limit,
totalPages: 0,
total: 0,
};
}
}
// Add search functionality
if (search) {
where.OR = [
{ nama: { contains: search, mode: 'insensitive' } },
{ lembaga: { nama: { contains: search, mode: 'insensitive' } } }
];
}
const [data, total] = await Promise.all([ const [data, total] = await Promise.all([
prisma.siswa.findMany({ prisma.siswa.findMany({
where, where,
include: { include: {
lembaga: true, lembaga: {
include: {
jenjangPendidikan: true,
},
},
}, },
skip, skip,
take: limit, take: limit,
orderBy: { createdAt: 'desc' }, // opsional, kalau mau urut berdasarkan waktu orderBy: { nama: 'asc' },
}), }),
prisma.siswa.count({ prisma.siswa.count({
where, where,
}) })
]); ]);
console.log('Fetched siswa data count:', data.length);
console.log('Total siswa count:', total);
return { return {
success: true, success: true,
message: "Success fetch siswa with pagination", message: "Success fetch siswa with pagination",
@@ -45,11 +86,11 @@ async function siswaFindMany(context: Context) {
totalPages: Math.ceil(total / limit), totalPages: Math.ceil(total / limit),
total, total,
}; };
} catch (e) { } catch (error) {
console.error("Find many paginated error:", e); console.error("Error in siswaFindMany:", error);
return { return {
success: false, success: false,
message: "Failed fetch siswa with pagination", message: `Failed fetch siswa: ${error instanceof Error ? error.message : 'Unknown error'}`,
}; };
} }
} }

View File

@@ -1,16 +1,68 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// /api/berita/findManyPaginated.ts
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function dataPerpustakaanFindMany() { async function dataPerpustakaanFindMany(context: Context) {
const data = await prisma.dataPerpustakaan.findMany({ // Ambil parameter dari query
include: { const page = Number(context.query.page) || 1;
kategori: true, const limit = Number(context.query.limit) || 10;
image: true, const search = (context.query.search as string) || '';
}, const kategori = (context.query.kategori as string) || ''; // 🔥 Parameter kategori baru
}); const skip = (page - 1) * limit;
return { // Buat where clause
success: true, const where: any = { isActive: true };
message: "Success get all data perpustakaan",
data, // Filter berdasarkan kategori (jika ada)
}; if (kategori) {
where.kategori = {
name: {
equals: kategori,
mode: 'insensitive' // Tidak case-sensitive
}
};
}
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ judul: { contains: search, mode: 'insensitive' } },
];
}
try {
// Ambil data dan total count secara paralel
const [data, total] = await Promise.all([
prisma.dataPerpustakaan.findMany({
where,
include: {
image: true,
kategori: true,
},
skip,
take: limit,
orderBy: { createdAt: 'desc' },
}),
prisma.dataPerpustakaan.count({ where }),
]);
return {
success: true,
message: "Berhasil ambil data perpustakaan dengan pagination",
data,
page,
limit,
total,
totalPages: Math.ceil(total / limit),
};
} catch (e) {
console.error("Error di findMany paginated:", e);
return {
success: false,
message: "Gagal mengambil data perpustakaan",
};
}
} }
export default dataPerpustakaanFindMany;

View File

@@ -1,48 +1,53 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita'; import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
import { Badge, Box, Button, Card, Center, Container, Divider, Flex, Grid, GridCol, Group, Image, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, Title } from '@mantine/core'; import {
Badge, Box, Button, Card, Center, Container, Divider,
Flex, Grid, GridCol, Group, Image, Pagination,
Paper, SimpleGrid, Skeleton, Stack, Text, Title
} from '@mantine/core';
import { IconArrowRight, IconCalendar } from '@tabler/icons-react'; import { IconArrowRight, IconCalendar } from '@tabler/icons-react';
import { useTransitionRouter } from 'next-view-transitions'; import { useTransitionRouter } from 'next-view-transitions';
import { useSearchParams } from 'next/navigation'; import { useSearchParams } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function Semua() { function Semua() {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const router = useTransitionRouter(); const router = useTransitionRouter();
// Parameter URL // Ambil parameter langsung dari URL
const search = searchParams.get('search') || ''; const search = searchParams.get('search') || '';
const currentPage = parseInt(searchParams.get('page') || '1'); const page = parseInt(searchParams.get('page') || '1');
const [page, setPage] = useState(currentPage);
// Gunakan proxy untuk state // Gunakan proxy untuk state global
const state = useProxy(stateDashboardBerita.berita); const state = useProxy(stateDashboardBerita.berita);
const featured = useProxy(stateDashboardBerita.berita.findFirst); // ✅ Berita utama const featured = useProxy(stateDashboardBerita.berita.findFirst);
const loadingGrid = state.findMany.loading; const loadingGrid = state.findMany.loading;
const loadingFeatured = featured.loading; const loadingFeatured = featured.loading;
// Load berita utama (hanya sekali) // Load berita utama sekali saja
useEffect(() => { useEffect(() => {
if (!featured.data && !loadingFeatured) { if (!featured.data && !loadingFeatured) {
stateDashboardBerita.berita.findFirst.load(); stateDashboardBerita.berita.findFirst.load();
} }
}, [featured.data, loadingFeatured]); }, [featured.data, loadingFeatured]);
// Load berita terbaru (untuk grid) saat page/search berubah // Load berita terbaru tiap page / search berubah
useEffect(() => { useEffect(() => {
const limit = 3; // Sesuaikan dengan tampilan grid const limit = 3;
state.findMany.load(page, limit, search); state.findMany.load(page, limit, search);
}, [page, search]); }, [page, search]);
// Update URL saat page berubah // Handler pagination → langsung update URL
useEffect(() => { const handlePageChange = (newPage: number) => {
const url = new URLSearchParams(); const url = new URLSearchParams(searchParams.toString());
if (search) url.set('search', search); if (search) url.set('search', search);
if (page > 1) url.set('page', page.toString()); if (newPage > 1) url.set('page', newPage.toString());
else url.delete('page'); // biar page=1 ga muncul di URL
router.replace(`?${url.toString()}`); router.replace(`?${url.toString()}`);
}, [search, page, router]); };
const featuredData = featured.data; const featuredData = featured.data;
const paginatedNews = state.findMany.data || []; const paginatedNews = state.findMany.data || [];
@@ -51,7 +56,7 @@ function Semua() {
return ( return (
<Box py={20}> <Box py={20}>
<Container size="xl" px={{ base: "md", md: "xl" }}> <Container size="xl" px={{ base: "md", md: "xl" }}>
{/* === Berita Utama (Tetap) === */} {/* === Berita Utama === */}
{loadingFeatured ? ( {loadingFeatured ? (
<Center><Skeleton h={400} /></Center> <Center><Skeleton h={400} /></Center>
) : featuredData ? ( ) : featuredData ? (
@@ -94,7 +99,9 @@ function Semua() {
<Button <Button
variant="light" variant="light"
rightSection={<IconArrowRight size={16} />} rightSection={<IconArrowRight size={16} />}
onClick={() => router.push(`/darmasaba/desa/berita/${featuredData.kategoriBerita?.name}/${featuredData.id}`)} onClick={() =>
router.push(`/darmasaba/desa/berita/${featuredData.kategoriBerita?.name}/${featuredData.id}`)
}
> >
Baca Selengkapnya Baca Selengkapnya
</Button> </Button>
@@ -106,7 +113,7 @@ function Semua() {
</Box> </Box>
) : null} ) : null}
{/* === Berita Terbaru (Berubah Saat Pagination) === */} {/* === Berita Terbaru === */}
<Box mt={50}> <Box mt={50}>
<Title order={2} mb="md">Berita Terbaru</Title> <Title order={2} mb="md">Berita Terbaru</Title>
<Divider mb="xl" /> <Divider mb="xl" />
@@ -122,13 +129,7 @@ function Semua() {
) : ( ) : (
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl" verticalSpacing="xl"> <SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl" verticalSpacing="xl">
{paginatedNews.map((item) => ( {paginatedNews.map((item) => (
<Card <Card key={item.id} shadow="sm" p="lg" radius="md" withBorder>
key={item.id}
shadow="sm"
p="lg"
radius="md"
withBorder
>
<Card.Section> <Card.Section>
<Image <Image
src={item.image?.link || '/images/placeholder-small.jpg'} src={item.image?.link || '/images/placeholder-small.jpg'}
@@ -143,7 +144,6 @@ function Semua() {
</Badge> </Badge>
<Text fw={600} size="lg" mt="sm" lineClamp={2}>{item.judul}</Text> <Text fw={600} size="lg" mt="sm" lineClamp={2}>{item.judul}</Text>
<Text size="sm" color="dimmed" lineClamp={3} mt="xs">{item.deskripsi}</Text> <Text size="sm" color="dimmed" lineClamp={3} mt="xs">{item.deskripsi}</Text>
<Flex align="center" justify="apart" mt="md" gap="xs"> <Flex align="center" justify="apart" mt="md" gap="xs">
@@ -154,20 +154,28 @@ function Semua() {
year: 'numeric' year: 'numeric'
})} })}
</Text> </Text>
<Button
<Button p="xs" variant="light" rightSection={<IconArrowRight size={16} />} onClick={() => router.push(`/darmasaba/desa/berita/${item.kategoriBerita?.name}/${item.id}`)}>Baca Selengkapnya</Button> p="xs"
variant="light"
rightSection={<IconArrowRight size={16} />}
onClick={() =>
router.push(`/darmasaba/desa/berita/${item.kategoriBerita?.name}/${item.id}`)
}
>
Baca Selengkapnya
</Button>
</Flex> </Flex>
</Card> </Card>
))} ))}
</SimpleGrid> </SimpleGrid>
)} )}
{/* Pagination hanya untuk berita terbaru */} {/* Pagination */}
<Center mt="xl"> <Center mt="xl">
<Pagination <Pagination
total={totalPages} total={totalPages}
value={page} value={page}
onChange={setPage} onChange={handlePageChange}
siblings={1} siblings={1}
boundaries={1} boundaries={1}
withEdges withEdges
@@ -179,4 +187,4 @@ function Semua() {
); );
} }
export default Semua; export default Semua;

View File

@@ -87,9 +87,9 @@ function LayoutTabsGotongRoyong({
href: "/darmasaba/lingkungan/gotong-royong/kebersihan" href: "/darmasaba/lingkungan/gotong-royong/kebersihan"
}, },
{ {
label: "Infrasturktur", label: "Infrastruktur",
value: "infrasturktur", value: "infrastruktur",
href: "/darmasaba/lingkungan/gotong-royong/infrasturktur" href: "/darmasaba/lingkungan/gotong-royong/infrastruktur"
}, },
{ {
label: "Sosial", label: "Sosial",

View File

@@ -81,8 +81,9 @@ function Page() {
<Box key={k} px={28}> <Box key={k} px={28}>
<Paper p={20} bg={colors['white-trans-1']}> <Paper p={20} bg={colors['white-trans-1']}>
<Flex gap={20} align={'center'}> <Flex gap={20} align={'center'}>
<Text>{k + 1}</Text>
<Box style={{ alignContent: 'center', alignItems: 'center' }}> <Box style={{ alignContent: 'center', alignItems: 'center' }}>
{k + 1} {iconMap[v.icon] ? React.createElement(iconMap[v.icon]) : null} {iconMap[v.icon] ? React.createElement(iconMap[v.icon]) : null}
</Box> </Box>
<Text fw={'bold'} fz={{ base: "lg", md: "xl" }} c={'black'}>{v.name}</Text> <Text fw={'bold'} fz={{ base: "lg", md: "xl" }} c={'black'}>{v.name}</Text>
</Flex> </Flex>

View File

@@ -1,14 +1,13 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
'use client' 'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Group, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { IconSearch, IconLeaf, IconTrophy, IconTent, IconChartLine, IconRecycle, IconTruckFilled, IconScale, IconClipboardTextFilled, IconTrashFilled, IconHomeEco, IconChristmasTreeFilled, IconTrendingUp, IconShieldFilled } from '@tabler/icons-react';
import BackButton from '../../desa/layanan/_com/BackButto';
import programPenghijauanState from '@/app/admin/(dashboard)/_state/lingkungan/program-penghijauan'; import programPenghijauanState from '@/app/admin/(dashboard)/_state/lingkungan/program-penghijauan';
import { useProxy } from 'valtio/utils'; import colors from '@/con/colors';
import { Box, Center, Group, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { useState } from 'react'; import { IconChartLine, IconChristmasTreeFilled, IconClipboardTextFilled, IconHomeEco, IconLeaf, IconRecycle, IconScale, IconSearch, IconShieldFilled, IconTent, IconTrashFilled, IconTrendingUp, IconTrophy, IconTruckFilled } from '@tabler/icons-react';
import React from 'react'; import React, { useState } from 'react';
import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto';
function Page() { function Page() {
const state = useProxy(programPenghijauanState); const state = useProxy(programPenghijauanState);
@@ -17,7 +16,7 @@ function Page() {
const { data, load, page, totalPages, loading } = state.findMany; const { data, load, page, totalPages, loading } = state.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, debouncedSearch); load(page, 4, debouncedSearch);
}, [page, debouncedSearch]); }, [page, debouncedSearch]);
const iconMap: Record<string, any> = { const iconMap: Record<string, any> = {
@@ -110,9 +109,6 @@ function Page() {
<Text fz="sm" ta="center" c="dimmed"> <Text fz="sm" ta="center" c="dimmed">
{v.judul} {v.judul}
</Text> </Text>
<Button variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="sm" radius="xl" mt="sm">
Pelajari Lebih Lanjut
</Button>
</Stack> </Stack>
</Paper> </Paper>
))} ))}

View File

@@ -1,46 +1,25 @@
'use client' 'use client'
import beasiswaDesaState from '@/app/admin/(dashboard)/_state/pendidikan/beasiswa-desa';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Group, Image, Modal, Paper, Select, SimpleGrid, Stack, Stepper, StepperStep, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Center, Group, Image, Modal, Paper, Select, SimpleGrid, Stack, Stepper, StepperStep, Text, TextInput, Title } from '@mantine/core';
import { useState } from 'react';
import BackButton from '../../desa/layanan/_com/BackButto';
import { useDisclosure } from '@mantine/hooks'; import { useDisclosure } from '@mantine/hooks';
import { IconArrowRight, IconCoin, IconInfoCircle, IconSchool, IconUsers } from '@tabler/icons-react';
import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import beasiswaDesaState from '@/app/admin/(dashboard)/_state/pendidikan/beasiswa-desa'; import BackButton from '../../desa/layanan/_com/BackButto';
const dataBeasiswa = [ const dataBeasiswa = [
{ { id: 1, nama: 'Penerima Beasiswa', jumlah: '250+', icon: IconUsers },
id: 1, { id: 2, nama: 'Peluang Kelulusan', jumlah: '90%', icon: IconSchool },
nama: 'Penerima Beasiswa', { id: 3, nama: 'Dana Tersalurkan', jumlah: '1.5M', icon: IconCoin },
jumlah: '250+' ];
},
{
id: 2,
nama: 'Peluang Kelulusan',
jumlah: '90%'
},
{
id: 3,
nama: 'Dana Tersalurkan',
jumlah: '1.5M'
},
]
const dataProgram = [ const dataProgram = [
{ { id: 1, judul: "Pelatihan SoftSkill", deskripsi: "Pengembangan diri untuk mempersiapkan karir masa depan." },
id: 1, { id: 2, judul: "Peningkatan Akses Pendidikan", deskripsi: "Memberi kesempatan bagi masyarakat kurang mampu untuk tetap sekolah." },
judul: "Pelatihan SoftSkill", { id: 3, judul: "Pendampingan Intensif", deskripsi: "Bimbingan dari mentor berpengalaman untuk mendukung akademik." },
deskripsi: "Program pengembangan diri untuk mempersiapkan karir masa depan", ];
},
{
id: 2,
judul: "Peningkatan Akses Pendidikan ",
deskripsi: "Program yang menjangkau masyarakat kurang mampu secara finansial, mengurangi angka putus sekolah",
},
{
id: 3,
judul: "Pendampingan Intensif",
deskripsi: "Program dengan mentor berpengalaman yang membimbing dalam perjalanan akademik",
}
]
function Page() { function Page() {
const beasiswaDesa = useProxy(beasiswaDesaState.beasiswaPendaftar) const beasiswaDesa = useProxy(beasiswaDesaState.beasiswaPendaftar)
const [opened, { open, close }] = useDisclosure(false); const [opened, { open, close }] = useDisclosure(false);
@@ -60,267 +39,173 @@ function Page() {
statusPernikahan: "", statusPernikahan: "",
ukuranBaju: "", ukuranBaju: "",
}; };
} };
const handleSubmit = async () => { const handleSubmit = async () => {
await beasiswaDesa.create.create(); await beasiswaDesa.create.create();
resetForm(); resetForm();
close(); close();
} };
const [active, setActive] = useState(1); const [active, setActive] = useState(1);
const nextStep = () => setActive((current) => (current < 5 ? current + 1 : current)); const nextStep = () => setActive((current) => (current < 5 ? current + 1 : current));
const prevStep = () => setActive((current) => (current > 0 ? current - 1 : current)); const prevStep = () => setActive((current) => (current > 0 ? current - 1 : current));
return ( return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}> <Stack pos="relative" bg={colors.Bg} py="xl" gap={40}>
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
<BackButton /> <BackButton />
</Box> </Box>
{/* Page 1 */}
<Box px={{ base: 'md', md: 100 }} pb={50}> <Box px={{ base: 'md', md: 100 }} pb={50}>
<SimpleGrid <SimpleGrid cols={{ base: 1, md: 2 }} spacing="xl">
cols={{
base: 1,
md: 2
}}
>
<Box> <Box>
<Title fz={55} fw={'bold'} c={colors['blue-button']}> <Title fz={55} fw={900} c={colors['blue-button']}>
Wujudkan Mimpi Pendidikanmu di Desa Darmasaba Wujudkan Mimpi Pendidikanmu di Desa Darmasaba
</Title> </Title>
<Text fz={'xl'} > <Text fz="lg" mt="md" c="dimmed">
Program beasiswa komprehensif untuk mendukung pendidikan berkualitas bagi putra-putri Desa Darmasaba. Program beasiswa untuk mendukung pendidikan berkualitas bagi generasi muda Desa Darmasaba.
</Text> </Text>
<SimpleGrid <Group mt="xl">
mt={10} <Button size="lg" radius="xl" bg={colors['blue-button']} rightSection={<IconArrowRight size={20} />} onClick={open}>
cols={{ Daftar Sekarang
base: 1, </Button>
md: 2 <Button size="lg" radius="xl" variant="light" color={colors['blue-button']} rightSection={<IconInfoCircle size={20} />}>
}} Pelajari Lebih Lanjut
> </Button>
<Button bg={colors['blue-button']} fz={'lg'} onClick={open}>Daftar Sekarang</Button> </Group>
<Button bg={colors['blue-button-trans']} fz={'lg'}>Pelajari Lebih Lanjut</Button>
</SimpleGrid>
</Box> </Box>
<Box> <Box>
<Image alt='' src={'/api/img/beasiswa-siswa.png'} /> <Image alt="Beasiswa Desa" src="/api/img/beasiswa-siswa.png" radius="lg" />
</Box> </Box>
</SimpleGrid> </SimpleGrid>
<SimpleGrid mt={30}
cols={{ <SimpleGrid mt={50} cols={{ base: 1, md: 3 }} spacing="lg">
base: 1,
md: 3
}}
>
{dataBeasiswa.map((v, k) => { {dataBeasiswa.map((v, k) => {
const IconComp = v.icon;
return ( return (
<Box key={k}> <Paper key={k} p="xl" radius="xl" shadow="md" bg={colors['white-trans-1']} withBorder>
<Paper p={'xl'} bg={colors['white-trans-1']}> <Stack align="center" gap="sm">
<Title ta={'center'} fz={55} fw={'bold'} c={colors['blue-button']}> <IconComp size={45} color={colors['blue-button']} />
{v.jumlah} <Title fz={42} fw={900} c={colors['blue-button']}>{v.jumlah}</Title>
</Title> <Text fz="sm" ta="center">{v.nama}</Text>
<Text ta={'center'}> </Stack>
{v.nama} </Paper>
</Text> );
</Paper>
</Box>
)
})} })}
</SimpleGrid> </SimpleGrid>
</Box> </Box>
{/* ---- */}
<Box px={{ base: 'md', md: 100 }} pb={20}> <Box px={{ base: 'md', md: 100 }} pb={20}>
<Title pb={20} ta={'center'} order={1} fw={'bold'} c={colors['blue-button']}> <Title pb={20} ta="center" order={1} fw={900} c={colors['blue-button']}>
Keunggulan Program Keunggulan Program
</Title> </Title>
<Paper p={'xl'} bg={colors['white-trans-1']}> <SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg">
<SimpleGrid {dataProgram.map((v, k) => (
cols={{ <Paper key={k} p="xl" radius="xl" shadow="sm" bg={colors['white-trans-1']}>
base: 1, <Title order={3} fw={700} c={colors['blue-button']} mb="xs">{v.judul}</Title>
md: 3 <Text fz="sm" c="dimmed">{v.deskripsi}</Text>
}} </Paper>
> ))}
{dataProgram.map((v, k) => { </SimpleGrid>
return (
<Box key={k}> <Title py={40} ta="center" order={1} fw={900} c={colors['blue-button']}>
<Title order={2} fw={'bold'} c={colors['blue-button']}>
{v.judul}
</Title>
<Text>
{v.deskripsi}
</Text>
{/* <Divider orientation="vertical" size="md" h="auto" /> */}
</Box>
)
})}
</SimpleGrid>
</Paper>
<Title py={20} ta={'center'} order={1} fw={'bold'} c={colors['blue-button']}>
Timeline Pendaftaran Timeline Pendaftaran
</Title> </Title>
<Center> <Center>
<Stepper mt={20} active={active} onStepClick={setActive} orientation="vertical" allowNextStepsSelect={false}> <Stepper mt={20} active={active} onStepClick={setActive} orientation="vertical" allowNextStepsSelect={false}>
<StepperStep label="Pembukaan Pendaftaran 1 Maret 2025" description="" /> <StepperStep label="1 Maret 2025" description="Pembukaan Pendaftaran" />
<StepperStep label="Seleksi Administrasi 15 Maret 2025" description="" /> <StepperStep label="15 Maret 2025" description="Seleksi Administrasi" />
<StepperStep label="Tes Potensi Akademik 1 April 2025" description="" /> <StepperStep label="1 April 2025" description="Tes Potensi Akademik" />
<StepperStep label="Wawancara 15 April 2025" description="" /> <StepperStep label="15 April 2025" description="Wawancara" />
<StepperStep label="Pengumuman 1 Mei 2025" description="" /> <StepperStep label="1 Mei 2025" description="Pengumuman Hasil" />
</Stepper> </Stepper>
</Center> </Center>
<Group justify="center" mt="xl"> <Group justify="center" mt="xl">
<Button variant="default" onClick={prevStep}>Back</Button> <Button variant="default" radius="xl" onClick={prevStep}>Kembali</Button>
<Button onClick={nextStep}>Next step</Button> <Button radius="xl" bg={colors['blue-button']} onClick={nextStep}>Lanjut</Button>
</Group> </Group>
</Box> </Box>
<Modal <Modal
opened={opened} opened={opened}
onClose={close} onClose={close}
radius={0} radius="xl"
size="lg"
transitionProps={{ transition: 'fade', duration: 200 }} transitionProps={{ transition: 'fade', duration: 200 }}
title={
<Text fz="xl" fw={800} c={colors['blue-button']}>
Formulir Beasiswa
</Text>
}
> >
<Paper p={"md"} withBorder> <Paper p="lg" radius="xl" withBorder shadow="sm">
<Stack gap={"xs"}> <Stack gap="sm">
<Title order={3}>Ajukan Beasiswa</Title> <TextInput
<TextInput label="Nama Lengkap"
label={<Text fz={"sm"} fw={"bold"}>Nama</Text>} placeholder="Masukkan nama lengkap"
placeholder="masukkan nama" onChange={(val) => { beasiswaDesa.create.form.namaLengkap = val.target.value }} />
onChange={(val) => { <TextInput
beasiswaDesa.create.form.namaLengkap = val.target.value type="number"
}} label="NIK"
/> placeholder="Masukkan NIK"
<TextInput onChange={(val) => { beasiswaDesa.create.form.nik = val.target.value }} />
type='number' <TextInput
label={<Text fz={"sm"} fw={"bold"}>NIK</Text>} label="Tempat Lahir"
placeholder="masukkan nik" placeholder="Masukkan tempat lahir"
onChange={(val) => { onChange={(val) => { beasiswaDesa.create.form.tempatLahir = val.target.value }} />
beasiswaDesa.create.form.nik = val.target.value <TextInput
}} type="date"
/> label="Tanggal Lahir"
<TextInput placeholder="Pilih tanggal lahir"
label={<Text fz={"sm"} fw={"bold"}>Tempat Lahir</Text>} onChange={(val) => { beasiswaDesa.create.form.tanggalLahir = val.target.value }} />
placeholder="masukkan tempat lahir" <Select
onChange={(val) => { label="Jenis Kelamin"
beasiswaDesa.create.form.tempatLahir = val.target.value placeholder="Pilih jenis kelamin"
}} data={[{ value: "LAKI_LAKI", label: "Laki-laki" }, { value: "PEREMPUAN", label: "Perempuan" }]}
/> onChange={(val) => { if (val) beasiswaDesa.create.form.jenisKelamin = val }} />
<TextInput <TextInput
type='date' label="Kewarganegaraan"
label={<Text fz={"sm"} fw={"bold"}>Tanggal Lahir</Text>} placeholder="Masukkan kewarganegaraan"
placeholder="masukkan tanggal lahir" onChange={(val) => { beasiswaDesa.create.form.kewarganegaraan = val.target.value }} />
onChange={(val) => { <Select
beasiswaDesa.create.form.tanggalLahir = val.target.value label="Agama"
}} placeholder="Pilih agama"
/> data={[{ value: "ISLAM", label: "Islam" }, { value: "KRISTEN_PROTESTAN", label: "Kristen Protestan" }, { value: "KRISTEN_KATOLIK", label: "Kristen Katolik" }, { value: "HINDU", label: "Hindu" }, { value: "BUDDHA", label: "Buddha" }, { value: "KONGHUCU", label: "Konghucu" }, { value: "LAINNYA", label: "Lainnya" }]}
<Select onChange={(val) => { if (val) beasiswaDesa.create.form.agama = val }} />
label={<Text fz={"sm"} fw={"bold"}>Jenis Kelamin</Text>} <TextInput
placeholder="Pilih jenis kelamin" label="Alamat KTP"
data={[ placeholder="Masukkan alamat sesuai KTP"
{ value: "LAKI_LAKI", label: "Laki-laki" }, onChange={(val) => { beasiswaDesa.create.form.alamatKTP = val.target.value }} />
{ value: "PEREMPUAN", label: "Perempuan" }, <TextInput
]} label="Alamat Domisili"
onChange={(val) => { placeholder="Masukkan alamat domisili"
if (val) beasiswaDesa.create.form.jenisKelamin = val as "LAKI_LAKI" | "PEREMPUAN"; onChange={(val) => { beasiswaDesa.create.form.alamatDomisili = val.target.value }} />
}} <TextInput
/> type="number"
<TextInput label="Nomor HP"
label={<Text fz={"sm"} fw={"bold"}>Kewarganegaraan</Text>} placeholder="Masukkan nomor HP"
placeholder="masukkan kewarganegaraan" onChange={(val) => { beasiswaDesa.create.form.noHp = val.target.value }} />
onChange={(val) => { <TextInput
beasiswaDesa.create.form.kewarganegaraan = val.target.value type="email"
}} label="Email"
/> placeholder="Masukkan alamat email"
<Select onChange={(val) => { beasiswaDesa.create.form.email = val.target.value }} />
label={<Text fz={"sm"} fw={"bold"}>Agama</Text>} <Select
placeholder="Pilih agama" label="Status Pernikahan"
data={[ placeholder="Pilih status pernikahan"
{ value: "ISLAM", label: "Islam" }, data={[{ value: "BELUM_MENIKAH", label: "Belum Menikah" }, { value: "MENIKAH", label: "Menikah" }, { value: "JANDA_DUDA", label: "Janda/Duda" }]}
{ value: "KRISTEN_PROTESTAN", label: "Kristen Protestan" }, onChange={(val) => { if (val) beasiswaDesa.create.form.statusPernikahan = val }} />
{ value: "KRISTEN_KATOLIK", label: "Kristen Katolik" }, <Select
{ value: "HINDU", label: "Hindu" }, label="Ukuran Baju"
{ value: "BUDDHA", label: "Buddha" }, placeholder="Pilih ukuran baju"
{ value: "KONGHUCU", label: "Konghucu" }, data={[{ value: "S", label: "S" }, { value: "M", label: "M" }, { value: "L", label: "L" }, { value: "XL", label: "XL" }, { value: "XXL", label: "XXL" }, { value: "LAINNYA", label: "Lainnya" }]}
{ value: "LAINNYA", label: "Lainnya" }, onChange={(val) => { if (val) beasiswaDesa.create.form.ukuranBaju = val }} />
]} <Group justify="flex-end" mt="md">
onChange={(val) => { <Button variant="default" radius="xl" onClick={close}>Batal</Button>
if (val) beasiswaDesa.create.form.agama = val as <Button radius="xl" bg={colors['blue-button']} onClick={handleSubmit}>Kirim</Button>
"ISLAM" </Group>
| "KRISTEN_PROTESTAN"
| "KRISTEN_KATOLIK"
| "HINDU"
| "BUDDHA"
| "KONGHUCU"
| "LAINNYA";
}}
/>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Alamat KTP</Text>}
placeholder="masukkan alamat ktp"
onChange={(val) => {
beasiswaDesa.create.form.alamatKTP = val.target.value
}}
/>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Alamat Domisili</Text>}
placeholder="masukkan alamat domisili"
onChange={(val) => {
beasiswaDesa.create.form.alamatDomisili = val.target.value
}}
/>
<TextInput
type='number'
label={<Text fz={"sm"} fw={"bold"}>No Hp</Text>}
placeholder="masukkan no hp"
onChange={(val) => {
beasiswaDesa.create.form.noHp = val.target.value
}}
/>
<TextInput
type='email'
label={<Text fz={"sm"} fw={"bold"}>Email</Text>}
placeholder="masukkan email"
onChange={(val) => {
beasiswaDesa.create.form.email = val.target.value
}}
/>
<Select
label={<Text fz={"sm"} fw={"bold"}>Status Pernikahan</Text>}
placeholder="Pilih status pernikahan"
data={[
{ value: "BELUM_MENIKAH", label: "Belum Menikah" },
{ value: "MENIKAH", label: "Menikah" },
{ value: "JANDA_DUDA", label: "Janda/Duda" },
]}
onChange={(val) => {
if (val) beasiswaDesa.create.form.statusPernikahan = val as
"BELUM_MENIKAH"
| "MENIKAH"
| "JANDA_DUDA";
}}
/>
<Select
label={<Text fz={"sm"} fw={"bold"}>Ukuran Baju</Text>}
placeholder="Pilih ukuran baju"
data={[
{ value: "S", label: "S" },
{ value: "M", label: "M" },
{ value: "L", label: "L" },
{ value: "XL", label: "XL" },
{ value: "XXL", label: "XXL" },
{ value: "LAINNYA", label: "Lainnya" },
]}
onChange={(val) => {
if (val) beasiswaDesa.create.form.ukuranBaju = val as
"S"
| "M"
| "L"
| "XL"
| "XXL"
| "LAINNYA";
}}
/>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
</Stack> </Stack>
</Paper> </Paper>
</Modal> </Modal>

View File

@@ -1,67 +1,97 @@
'use client'
import stateBimbinganBelajarDesa from '@/app/admin/(dashboard)/_state/pendidikan/bimbingan-belajar-desa';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Stack, Box, Title, Text, SimpleGrid, Paper, List, ListItem } from '@mantine/core'; import { Box, Paper, SimpleGrid, Skeleton, Stack, Text, Title, Tooltip, Divider, Badge } from '@mantine/core';
import React from 'react'; import { useShallowEffect } from '@mantine/hooks';
import { useProxy } from 'valtio/utils';
import { IconMapPin, IconCalendarTime, IconBook2 } from '@tabler/icons-react';
import BackButton from '../../desa/layanan/_com/BackButto'; import BackButton from '../../desa/layanan/_com/BackButto';
function Page() { function Page() {
const stateTujuanProgram = useProxy(stateBimbinganBelajarDesa.stateTujuanProgram);
const stateLokasiDanJadwal = useProxy(stateBimbinganBelajarDesa.lokasiDanJadwalState);
const stateFasilitas = useProxy(stateBimbinganBelajarDesa.fasilitasYangDisediakanState);
useShallowEffect(() => {
stateTujuanProgram.findById.load('edit');
stateLokasiDanJadwal.findById.load('edit');
stateFasilitas.findById.load('edit');
}, []);
if (!stateTujuanProgram.findById.data || !stateLokasiDanJadwal.findById.data || !stateFasilitas.findById.data)
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }} pb={50}>
<Skeleton h={60} radius="xl" />
<Skeleton h={200} mt="lg" radius="md" />
</Box>
</Stack>
);
return ( return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}> <Stack pos="relative" bg={colors.Bg} py="xl" gap="xl">
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
<BackButton /> <BackButton />
</Box> </Box>
<Box px={{ base: 'md', md: 100 }} pb={50}> <Box px={{ base: 'md', md: 120 }} pb={80}>
<Box> <Box mb="lg">
<Title ta={'center'} order={1} fw={'bold'} c={colors['blue-button']}> <Title ta="center" order={1} fw="bold" c={colors['blue-button']} fz={{ base: 28, md: 38 }}>
Bimbingan Belajar Desa Program Bimbingan Belajar Desa
</Title> </Title>
<Text pb={20} ta={'justify'} fz={'xl'} px={{ base: 'md', md: 100 }}> <Divider size="sm" my="md" mx="auto" w="60%" color={colors['blue-button']} />
Bimbingan Belajar Desa merupakan program unggulan untuk membantu siswa-siswi di Desa Darmasaba dalam memahami pelajaran sekolah, meningkatkan prestasi akademik, serta membangun semangat belajar yang tinggi sejak dini. <Text ta="center" fz="lg" c="black" px={{ base: 'sm', md: 120 }}>
Program unggulan untuk mendukung siswa Desa Darmasaba memahami pelajaran sekolah, meningkatkan prestasi akademik, dan menumbuhkan semangat belajar sejak dini.
</Text> </Text>
</Box> </Box>
<SimpleGrid <SimpleGrid cols={{ base: 1, md: 3 }} spacing="xl">
cols={{ <Paper p="xl" radius="lg" shadow="md" withBorder bg={colors['white-trans-1']}>
base: 1, <Stack gap="sm">
md: 3 <Box>
}} <Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
> Tujuan Program
<Box> </Badge>
<Paper p={'xl'} radius={'md'} bg={colors['white-trans-1']}> <Tooltip label="Gambaran manfaat utama program" position="top-start" withArrow>
<Title order={2} fw={'bold'} c={colors['blue-button']}> <Box>
Tujuan Program <IconBook2 size={36} stroke={1.5} color={colors['blue-button']} />
</Title> </Box>
<List> </Tooltip>
<ListItem fz={'h4'}>Memberikan pendampingan belajar secara gratis bagi siswa SD hingga SMP</ListItem> </Box>
<ListItem fz={'h4'}>Membantu siswa dalam menghadapi ujian dan menyelesaikan tugas sekolah</ListItem> <Text fz="md" lh={1.6} dangerouslySetInnerHTML={{ __html: stateTujuanProgram.findById.data?.deskripsi }} />
<ListItem fz={'h4'}>Menumbuhkan kepercayaan diri dan kemandirian dalam belajar</ListItem> </Stack>
<ListItem fz={'h4'}>Meningkatkan kesetaraan pendidikan untuk seluruh anak desa</ListItem> </Paper>
</List> <Paper p="xl" radius="lg" shadow="md" withBorder bg={colors['white-trans-1']}>
</Paper> <Stack gap="sm">
</Box> <Box>
<Box> <Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
<Paper h={{base: 0, md: 324}} p={'xl'} radius={'md'} bg={colors['white-trans-1']}> Lokasi & Jadwal
<Title order={2} fw={'bold'} c={colors['blue-button']}> </Badge>
Lokasi dan Jadwal <Tooltip label="Tempat dan waktu pelaksanaan" position="top-start" withArrow>
</Title> <Box>
<List> <IconMapPin size={36} stroke={1.5} color={colors['blue-button']} />
<ListItem fz={'h4'}>Lokasi: Balai Banjar / Balai Desa Darmasaba / Perpustakaan Desa</ListItem> </Box>
<ListItem fz={'h4'}>Jadwal: Setiap hari Senin, Rabu, dan Jumat pukul 16.0018.00 WITA</ListItem> </Tooltip>
<ListItem fz={'h4'}>Peserta: Terbuka untuk semua siswa SDSMP di wilayah desa</ListItem> </Box>
</List> <Text fz="md" lh={1.6} dangerouslySetInnerHTML={{ __html: stateLokasiDanJadwal.findById.data?.deskripsi }} />
</Paper> </Stack>
</Box> </Paper>
<Box> <Paper p="xl" radius="lg" shadow="md" withBorder bg={colors['white-trans-1']}>
<Paper h={{base: 0, md: 324}} p={'xl'} radius={'md'} bg={colors['white-trans-1']}> <Stack gap="sm">
<Title order={2} fw={'bold'} c={colors['blue-button']}> <Box>
Fasilitas yang Disediakan <Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
</Title> Fasilitas
<List> </Badge>
<ListItem fz={'h4'}>Buku-buku pelajaran dan alat tulis</ListItem> <Tooltip label="Sarana yang disediakan untuk peserta" position="top-start" withArrow>
<ListItem fz={'h4'}>Ruang belajar nyaman dan kondusif</ListItem> <Box>
<ListItem fz={'h4'}>Modul latihan dan pendampingan tugas</ListItem> <IconCalendarTime size={36} stroke={1.5} color={colors['blue-button']} />
<ListItem fz={'h4'}>Minuman ringan dan dukungan motivasi belajar</ListItem> </Box>
</List> </Tooltip>
</Paper> </Box>
</Box> <Text fz="md" lh={1.6} dangerouslySetInnerHTML={{ __html: stateFasilitas.findById.data?.deskripsi }} />
</Stack>
</Paper>
</SimpleGrid> </SimpleGrid>
</Box> </Box>
</Stack> </Stack>

View File

@@ -1,71 +1,102 @@
import colors from '@/con/colors'; 'use client'
import { Stack, Box, Title, Paper } from '@mantine/core'; import dataPendidikan from '@/app/admin/(dashboard)/_state/pendidikan/data-pendidikan';
import React from 'react'; import { Box, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconSchool } from '@tabler/icons-react';
import { useEffect, useState } from 'react';
import { Bar, BarChart, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto'; import BackButton from '../../desa/layanan/_com/BackButto';
import { BarChart } from '@mantine/charts'; import colors from '@/con/colors';
const data = [
{
kategori: 'Jumlah penduduk usia 15-64 th yang tidak bisa baca tulis',
jumlah: 30
},
{
kategori: 'Jumlah penduduk tidak tamat SD/sederajat',
jumlah: 25
},
{
kategori: 'Jumlah penduduk tidak tamat SLTP/Sederajat',
jumlah: 20
},
{
kategori: 'Jumlah penduduk tidak tamat SLTA/Sederajat',
jumlah: 10
},
{
kategori: 'Jumlah penduduk tamat Sarjana/S1',
jumlah: 15
},
{
kategori: 'Jumlah penduduk tamat Pascsarjana',
jumlah: 30
},
]
function Page() { function Page() {
type DPMrafik = {
id: string;
name: string;
jumlah: number;
};
const stateDPM = useProxy(dataPendidikan);
const [chartData, setChartData] = useState<DPMrafik[]>([]);
const [mounted, setMounted] = useState(false);
useShallowEffect(() => {
setMounted(true);
stateDPM.findMany.load();
}, []);
useEffect(() => {
if (stateDPM.findMany.data) {
setChartData(
stateDPM.findMany.data.map((item) => ({
id: item.id,
name: item.name,
jumlah: Number(item.jumlah),
}))
);
}
}, [stateDPM.findMany.data]);
if (!stateDPM.findMany.data) {
return (
<Stack px="md" py="xl">
<Skeleton h={400} radius="lg" />
</Stack>
);
}
return ( return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}> <Stack bg="var(--mantine-color-gray-0)" py="xl" gap="lg" pos="relative">
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
<BackButton /> <BackButton />
</Box> </Box>
<Box px={{ base: 'md', md: 100 }} >
<Box pb={20}> <Box px={{ base: 'md', md: 100 }}>
<Title ta={'center'} order={1} fw={'bold'} c={colors['blue-button']}> <Stack gap="xs" align="center" pb="lg">
Data Pendidikan <IconSchool size={48} stroke={1.5} color={colors['blue-button']} />
<Title order={1} fw={700} ta="center" c={colors['blue-button']}>
Statistik Data Pendidikan
</Title> </Title>
</Box> <Text c="dimmed" size="sm" ta="center">
<Paper p={'xl'} radius={'md'} bg={colors['white-trans-1']}> Visualisasi jumlah pendidikan berdasarkan kategori yang tersedia
<BarChart </Text>
p={'100'} </Stack>
h={600}
data={data} {!mounted || chartData.length === 0 ? (
dataKey="kategori" <Paper radius="lg" p="xl" withBorder shadow="sm" bg="var(--mantine-color-white)">
series={[ <Stack align="center" gap="sm" justify="center" h={350}>
{ name: 'jumlah', color: colors['blue-button'] }, <IconSchool size={40} stroke={1.5} color="var(--mantine-color-gray-5)" />
]} <Title order={4} fw={600}>
tickLine="y" Belum Ada Data
xAxisProps={{ </Title>
angle: -45, // Rotate labels by -45 degrees <Text c="dimmed" size="sm">
textAnchor: 'end', // Anchor text to the end for better alignment Data pendidikan belum tersedia. Silakan tambahkan data untuk melihat grafik.
height: 100, // Increase height for rotated labels </Text>
interval: 0, // Show all labels </Stack>
style: { </Paper>
fontSize: '12px', // Adjust font size if needed ) : (
overflow: 'visible', <Paper radius="lg" p="xl" withBorder shadow="sm" bg="var(--mantine-color-white)">
whiteSpace: 'nowrap' <Title order={4} fw={600} mb="md">
} Grafik Pendidikan
}} </Title>
/> <ResponsiveContainer width="100%" height={350}>
</Paper> <BarChart data={chartData}>
<XAxis dataKey="name" />
<YAxis />
<Tooltip
contentStyle={{
borderRadius: 12,
background: 'var(--mantine-color-gray-0)',
border: '1px solid var(--mantine-color-gray-3)',
}}
cursor={{ fill: 'var(--mantine-color-gray-1)' }}
/>
<Legend />
<Bar dataKey="jumlah" fill={colors['blue-button']} name="Jumlah Pendidikan" radius={[8, 8, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</Paper>
)}
</Box> </Box>
</Stack> </Stack>
); );

View File

@@ -1,363 +0,0 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import React, { useMemo, useState } from 'react';
import {
ActionIcon,
Badge,
Box,
Button,
Center,
Container,
Group,
Paper,
Progress,
SimpleGrid,
Stack,
Text,
TextInput,
Tooltip,
VisuallyHidden,
} from '@mantine/core';
import {
IconChalkboard,
IconInfoCircle,
IconMicroscope,
IconSchool,
IconSearch,
IconArrowLeft,
} from '@tabler/icons-react';
import { motion } from 'framer-motion';
import type { IconProps } from '@tabler/icons-react';
type Stat = {
id: number;
icon: React.ComponentType<IconProps>;
jumlah: number;
nama: string;
helper?: string;
};
const dataSekolah: Stat[] = [
{
id: 1,
icon: IconChalkboard,
jumlah: 15,
nama: 'Lembaga Pendidikan',
helper: 'Jumlah institusi pendidikan resmi di wilayah ini',
},
{
id: 2,
icon: IconSchool,
jumlah: 3209,
nama: 'Siswa Terdaftar',
helper: 'Total siswa aktif di semua jenjang',
},
{
id: 3,
icon: IconMicroscope,
jumlah: 285,
nama: 'Tenaga Pengajar',
helper: 'Jumlah guru dan staf pengajar aktif',
},
];
export default function SekolahPage() {
const [query, setQuery] = useState('');
const [kategoriAktif, setKategoriAktif] = useState('Semua');
const kategoriList = ['Semua', 'TK/PAUD', 'SD', 'SMP', 'SMA/SMK'];
const maxJumlah = useMemo(() => Math.max(...dataSekolah.map((d) => d.jumlah)), []);
const filtered = useMemo(() => {
const q = query.trim().toLowerCase();
return dataSekolah.filter((d) => {
const teks = `${d.nama} ${d.jumlah}`.toLowerCase();
const matchQuery = q ? teks.includes(q) : true;
return matchQuery;
});
}, [query, kategoriAktif]);
const hasilCount = filtered.length;
return (
<Box style={{ minHeight: '100vh', background: '#f8fafc', paddingBottom: 48 }}>
<Container size="xl" py={{ base: 'md', md: 'xl' }}>
<Stack gap="lg">
<Box>
<ActionIcon
aria-label="Kembali"
onClick={() => window.history.back()}
size="lg"
radius="md"
variant="light"
style={{
color: '#1e293b',
background: 'white',
boxShadow: '0 2px 12px rgba(0,0,0,0.06)',
}}
>
<IconArrowLeft size={20} stroke={2} />
<VisuallyHidden>Tombol kembali</VisuallyHidden>
</ActionIcon>
</Box>
<Paper
radius="lg"
p={{ base: 'md', md: 'xl' }}
style={{
background: 'linear-gradient(180deg, #ffffff 0%, #f1f5f9 100%)',
border: '1px solid #e2e8f0',
boxShadow: '0 6px 24px rgba(0,0,0,0.06)',
}}
role="search"
aria-label="Pencarian sekolah"
>
<Stack gap="md">
<Center>
<motion.div
initial={{ opacity: 0, y: -8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.45, ease: 'easeOut' }}
>
<Text
ta="center"
c="#0f172a"
fz={{ base: 22, md: 30 }}
fw={800}
style={{ letterSpacing: -0.3 }}
>
Cari Informasi Sekolah
</Text>
<Text ta="center" c="dimmed" fz="sm" mt={6}>
Masukkan nama, jenjang, atau alamat sekolah untuk hasil lebih spesifik.
</Text>
</motion.div>
</Center>
<Group align="center" justify="center" gap="sm" style={{ width: '100%' }}>
<TextInput
value={query}
onChange={(e) => setQuery(e.currentTarget.value)}
placeholder="Contoh: SMP Negeri, SD 01, Kelurahan..."
leftSection={<IconSearch size={18} aria-hidden />}
aria-label="Masukkan kata kunci pencarian"
radius="xl"
size="md"
rightSection={
<Button
radius="xl"
size="sm"
aria-label="Telusuri"
onClick={() => {}}
style={{
height: 38,
minWidth: 110,
background: 'linear-gradient(90deg, #3b82f6 0%, #60a5fa 100%)',
color: 'white',
boxShadow: '0 4px 16px rgba(59,130,246,0.3)',
}}
>
Telusuri
</Button>
}
rightSectionWidth={120}
style={{
width: '100%',
maxWidth: 920,
}}
/>
</Group>
<Group justify="center" gap="xs" wrap="wrap" style={{ marginTop: 4 }}>
{kategoriList.map((k) => {
const aktif = k === kategoriAktif;
return (
<motion.div
key={k}
initial={{ scale: 0.98, opacity: 0.9 }}
animate={{ scale: 1, opacity: 1 }}
whileHover={{ scale: 1.02 }}
>
<Button
onClick={() => setKategoriAktif(k)}
radius="xl"
size="sm"
variant={aktif ? 'filled' : 'light'}
style={{
background: aktif
? 'linear-gradient(135deg, #3b82f6 0%, #60a5fa 100%)'
: 'white',
color: aktif ? 'white' : '#2563eb',
boxShadow: aktif ? '0 4px 16px rgba(59,130,246,0.25)' : 'none',
border: '1px solid #e2e8f0',
}}
>
{k}
</Button>
</motion.div>
);
})}
</Group>
</Stack>
</Paper>
<Box aria-live="polite" aria-atomic>
<Text fz="sm" c="dimmed">
Menampilkan <Text component="span" c="#0f172a" fw={700}>{hasilCount}</Text> hasil.
</Text>
</Box>
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg">
{filtered.length === 0 ? (
<Paper
p="xl"
radius="md"
style={{
background: '#f9fafb',
border: '1px dashed #e2e8f0',
minHeight: 220,
}}
role="status"
aria-label="Tidak ada hasil"
>
<Center style={{ minHeight: 180, flexDirection: 'column' }}>
<Text fz="lg" fw={800} c="#2563eb">
Tidak ditemukan
</Text>
<Text c="dimmed" mt="6px">
Coba gunakan kata kunci lain atau setel ulang filter.
</Text>
<Button
mt="md"
radius="xl"
onClick={() => {
setQuery('');
setKategoriAktif('Semua');
}}
style={{
background: 'linear-gradient(90deg, #3b82f6 0%, #60a5fa 100%)',
color: 'white',
boxShadow: '0 6px 18px rgba(59,130,246,0.25)',
}}
aria-label="Tampilkan semua"
>
Tampilkan Semua
</Button>
</Center>
</Paper>
) : (
filtered.map((v) => {
const percent = Math.round((v.jumlah / maxJumlah) * 100) || 0;
return (
<motion.div
key={v.id}
whileHover={{ scale: 1.025 }}
whileTap={{ scale: 0.995 }}
style={{ width: '100%' }}
>
<Paper
p="lg"
radius="lg"
style={{
background: 'white',
border: '1px solid #e2e8f0',
boxShadow: '0 8px 28px rgba(0,0,0,0.06)',
minHeight: 260,
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
}}
role="article"
aria-label={`${v.nama} kartu statistik`}
>
<Stack gap="sm">
<Center>
<Box
style={{
width: 80,
height: 80,
borderRadius: 16,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: '#eff6ff',
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.6)',
}}
aria-hidden
>
{React.createElement(v.icon, {
color: '#2563eb',
size: 34,
stroke: 1.6,
})}
</Box>
</Center>
<Group justify="apart" align="center" gap="xs">
<Stack gap={0}>
<Text fz={{ base: 18, md: 22 }} fw={800} c="#0f172a">
{v.jumlah.toLocaleString()}
</Text>
<Group gap={6} align="center">
<Text fz="sm" fw={700} c="#2563eb">
{v.nama}
</Text>
<Tooltip label={v.helper ?? ''} position="right" withArrow>
<ActionIcon aria-label={`Info ${v.nama}`} variant="transparent" size="xs">
<IconInfoCircle size={16} style={{ color: '#2563eb' }} />
</ActionIcon>
</Tooltip>
</Group>
</Stack>
<Badge
radius="md"
variant="light"
style={{
background: '#eff6ff',
color: '#2563eb',
border: '1px solid #e2e8f0',
}}
>
Statistik
</Badge>
</Group>
<Box>
<Progress
value={percent}
size="sm"
radius="xl"
aria-label={`${v.nama} progres ${percent} persen`}
/>
<Text fz="xs" c="dimmed" mt="6px">
Perbandingan dengan jumlah terbesar.
</Text>
</Box>
</Stack>
<Group justify="right" mt="8px">
<Button
radius="xl"
variant="outline"
onClick={() => {}}
aria-label={`Lihat detail ${v.nama}`}
style={{
borderColor: '#e2e8f0',
color: '#2563eb',
paddingLeft: 20,
paddingRight: 20,
}}
>
Lihat Detail
</Button>
</Group>
</Paper>
</motion.div>
);
})
)}
</SimpleGrid>
</Stack>
</Container>
</Box>
);
}

View File

@@ -0,0 +1,260 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client';
import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud';
import { Box, Button, Center, Container, Group, Paper, SimpleGrid, Skeleton, Stack, Text, Tooltip, ActionIcon } from '@mantine/core';
import { IconChalkboard, IconMicroscope, IconProps, IconRefresh, IconSchool, IconInfoCircle } from '@tabler/icons-react';
import { motion } from 'framer-motion';
import { useTransitionRouter } from 'next-view-transitions';
import React from 'react';
import { useCallback, useEffect, useState } from 'react';
interface Stat {
jenjangPendidikan: any;
icon: React.ComponentType<IconProps>;
jumlah: number;
nama: string;
helper?: string;
loading?: boolean;
}
export default function KategoriPage({ jenjangPendidikan }: { jenjangPendidikan: string }) {
const router = useTransitionRouter();
const [stats, setStats] = useState<Stat[]>([]);
const [isLoading, setIsLoading] = useState(true);
// Decode the URL parameter
const decodedJenjangPendidikan = decodeURIComponent(jenjangPendidikan);
const jenjangFilter = decodedJenjangPendidikan.toLowerCase() === 'semua'
? undefined
: decodedJenjangPendidikan;
const loadData = useCallback(async () => {
if (!decodedJenjangPendidikan) return;
try {
setIsLoading(true);
// Load all data in parallel with the jenjang filter
await Promise.all([
infoSekolahPaud.lembagaPendidikan.findMany.load(1, 100, '', jenjangFilter),
infoSekolahPaud.siswa.findMany.load(1, 100, '', jenjangFilter),
infoSekolahPaud.pengajar.findMany.load(1, 100, '', jenjangFilter),
]);
// Get filtered totals based on jenjang
const totalLembaga = infoSekolahPaud.lembagaPendidikan.findMany.total || 0;
const totalSiswa = infoSekolahPaud.siswa.findMany.total || 0;
const totalPengajar = infoSekolahPaud.pengajar.findMany.total || 0;
setStats([
{
icon: IconChalkboard,
jumlah: totalLembaga,
nama: 'Lembaga Pendidikan',
helper: 'Jumlah institusi pendidikan resmi di wilayah ini',
loading: false,
jenjangPendidikan: decodedJenjangPendidikan,
},
{
icon: IconSchool,
jumlah: totalSiswa,
nama: 'Siswa Terdaftar',
helper: 'Total siswa aktif di semua jenjang',
loading: false,
jenjangPendidikan: decodedJenjangPendidikan,
},
{
icon: IconMicroscope,
jumlah: totalPengajar,
nama: 'Tenaga Pengajar',
helper: 'Jumlah guru dan staf pengajar aktif',
loading: false,
jenjangPendidikan: decodedJenjangPendidikan,
},
]);
} catch (error) {
console.error('Error loading data:', error);
// Set error state or show toast notification
} finally {
setIsLoading(false);
}
}, [decodedJenjangPendidikan, jenjangFilter]);
useEffect(() => {
loadData();
}, [loadData, decodedJenjangPendidikan]);
const handleRefresh = () => {
loadData();
};
const hasilCount = stats.reduce((sum, stat) => sum + stat.jumlah, 0);
const filtered = stats;
if (isLoading) {
return (
<Box style={{ minHeight: '100vh', background: '#f8fafc', padding: '48px 0' }}>
<Container size="xl">
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg">
{[1, 2, 3].map((i) => (
<Skeleton key={i} height={260} radius="lg" />
))}
</SimpleGrid>
</Container>
</Box>
);
}
return (
<Box style={{ minHeight: '100vh', background: '#f8fafc', paddingBottom: 48 }}>
<Container size="xl" py={{ base: 'md', md: 'xl' }}>
<Box>
<Group justify="space-between" mb="md">
<Box aria-live="polite" aria-atomic>
<Text fz="sm" c="dimmed">
Menampilkan <Text component="span" c="#0f172a" fw={700}>{hasilCount}</Text> hasil.
</Text>
</Box>
<Button
leftSection={<IconRefresh size={16} />}
variant="outline"
size="xs"
onClick={handleRefresh}
loading={stats.some(stat => stat.loading)}
>
Segarkan Data
</Button>
</Group>
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg">
{filtered.length === 0 ? (
<Paper
p="xl"
radius="md"
style={{
background: '#f9fafb',
border: '1px dashed #e2e8f0',
minHeight: 220,
}}
role="status"
aria-label="Tidak ada hasil"
>
<Center style={{ minHeight: 180, flexDirection: 'column' }}>
<Text fz="lg" fw={800} c="#2563eb">
Tidak ditemukan
</Text>
<Text c="dimmed" mt="6px">
Coba gunakan kata kunci lain atau setel ulang filter.
</Text>
<Button
mt="md"
radius="xl"
onClick={handleRefresh}
style={{
background: 'linear-gradient(90deg, #3b82f6 0%, #60a5fa 100%)',
color: 'white',
boxShadow: '0 6px 18px rgba(59,130,246,0.25)',
}}
aria-label="Tampilkan semua"
>
Tampilkan Semua
</Button>
</Center>
</Paper>
) : (
filtered.map((v) => (
<motion.div
key={v.nama}
whileHover={{ scale: 1.025 }}
whileTap={{ scale: 0.995 }}
style={{ width: '100%' }}
>
<Skeleton visible={v.loading}>
<Paper
p="lg"
radius="lg"
style={{
background: 'white',
border: '1px solid #e2e8f0',
boxShadow: '0 8px 28px rgba(0,0,0,0.06)',
minHeight: 260,
}}
role="article"
aria-label={`${v.nama} kartu statistik`}
>
<Stack gap="sm" mb="md">
<Center>
<Box
style={{
width: 80,
height: 80,
borderRadius: 16,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: '#eff6ff',
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.6)',
}}
aria-hidden
>
{React.createElement(v.icon, {
color: '#2563eb',
size: 34,
stroke: 1.6,
})}
</Box>
</Center>
<Group justify="center" align="center" gap="xs">
<Stack gap={0}>
<Text ta={"center"} fz={{ base: 18, md: 22 }} fw={800} c="#0f172a">
{v.jumlah.toLocaleString()}
</Text>
<Group gap={6} align="center">
<Text ta={"center"} fz="sm" fw={700} c="#2563eb">
{v.nama}
</Text>
<Tooltip label={v.helper ?? ''} position="right" withArrow>
<ActionIcon aria-label={`Info ${v.nama}`} variant="transparent" size="xs">
<IconInfoCircle size={16} style={{ color: '#2563eb' }} />
</ActionIcon>
</Tooltip>
</Group>
</Stack>
</Group>
</Stack>
<Group justify="center" mt="8px">
<Button
radius="xl"
variant="outline"
aria-label={`Lihat detail ${v.nama}`}
style={{
borderColor: '#e2e8f0',
color: '#2563eb',
paddingLeft: 20,
paddingRight: 20,
}}
onClick={() => {
if (v.nama === "Lembaga Pendidikan") router.push(`/darmasaba/pendidikan/info-sekolah/${jenjangPendidikan}/lembaga`);
if (v.nama === "Siswa Terdaftar") router.push(`/darmasaba/pendidikan/info-sekolah/${jenjangPendidikan}/siswa`);
if (v.nama === "Tenaga Pengajar") router.push(`/darmasaba/pendidikan/info-sekolah/${jenjangPendidikan}/pengajar`);
}}
>
Lihat Detail
</Button>
</Group>
</Paper>
</Skeleton>
</motion.div>
))
)}
</SimpleGrid>
</Box>
</Container>
</Box>
);
}

View File

@@ -0,0 +1,117 @@
'use client'
import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud';
import colors from '@/con/colors';
import { Box, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, TextInput, Title } from '@mantine/core';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconChalkboard, IconLayersSubtract, IconSearch } from '@tabler/icons-react';
import { use, useState } from 'react';
import { useProxy } from 'valtio/utils';
interface PageProps {
params: Promise<{ jenjangPendidikan: string }>
}
function Page({ params }: PageProps) {
const [search, setSearch] = useState("")
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const stateList = useProxy(infoSekolahPaud.lembagaPendidikan)
const { jenjangPendidikan } = use(params);
const {
data,
page,
totalPages,
loading,
load,
} = stateList.findMany
useShallowEffect(() => {
// Decode the URL parameter and pass it to load
const decodedJenjang = decodeURIComponent(jenjangPendidikan).toLowerCase()
load(page, 10, debouncedSearch, decodedJenjang === 'semua' ? '' : decodedJenjang)
}, [page, jenjangPendidikan, debouncedSearch])
const filteredData = data || []
if (loading || !data) {
return (
<Stack py="lg" gap="md">
<Skeleton h={40} radius="xl" />
<Skeleton h={500} radius="md" />
</Stack>
)
}
return (
<Box py="lg">
<Paper
bg={colors['white-1']}
p="lg"
radius="xl"
shadow="sm"
withBorder
>
<Group justify="space-between" align="center" mb="md">
<Group gap="sm">
<IconChalkboard size={28} stroke={1.5} color={colors['blue-button']} />
<Title order={2} fz="xl">Daftar Lembaga Pendidikan</Title>
</Group>
<TextInput
placeholder='pencarian'
leftSection={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
</Group>
{filteredData.length === 0 ? (
<Stack align="center" justify="center" py="xl" gap="sm">
<IconLayersSubtract size={48} stroke={1.5} color={"gray"} />
<Text fz="lg" c="dimmed">Belum ada data lembaga pendidikan</Text>
</Stack>
) : (
<Table
striped
highlightOnHover
withTableBorder
withRowBorders
horizontalSpacing="md"
verticalSpacing="sm"
fz="sm"
>
<TableThead>
<TableTr>
<TableTh w="50%">Nama Lembaga</TableTh>
<TableTh w="50%">Jenjang Pendidikan</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd fw={500}>{item.nama}</TableTd>
<TableTd>{item.jenjangPendidikan?.nama || '-'}</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
)}
</Paper>
{filteredData.length > 0 && (
<Center mt="lg">
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
total={totalPages}
size="md"
radius="xl"
withEdges
/>
</Center>
)}
</Box>
);
}
export default Page;

View File

@@ -0,0 +1,14 @@
// src/app/darmasaba/(pages)/desa/berita/[kategori]/page.tsx
import { Suspense } from "react";
import Content from "./content";
export default async function Page({ params }: { params: Promise<{ jenjangPendidikan: string }> }) {
const { jenjangPendidikan } = await params;
return (
<Suspense fallback={<div>Loading...</div>}>
<Content jenjangPendidikan={jenjangPendidikan} />
</Suspense>
);
}

View File

@@ -0,0 +1,119 @@
'use client'
import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud';
import colors from '@/con/colors';
import { Box, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, TextInput, Title } from '@mantine/core';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconLayersSubtract, IconMicroscope, IconSearch } from '@tabler/icons-react';
import { use, useState } from 'react';
import { useProxy } from 'valtio/utils';
interface PageProps {
params: Promise<{ jenjangPendidikan: string }>
}
function Page({ params }: PageProps) {
const [search, setSearch] = useState("")
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const stateList = useProxy(infoSekolahPaud.pengajar)
const { jenjangPendidikan } = use(params);
const {
data,
page,
totalPages,
loading,
load,
} = stateList.findMany
useShallowEffect(() => {
// Decode the URL parameter and pass it to load
const decodedJenjang = decodeURIComponent(jenjangPendidikan).toLowerCase()
load(page, 10, debouncedSearch, decodedJenjang === 'semua' ? '' : decodedJenjang)
}, [page, jenjangPendidikan, debouncedSearch])
const filteredData = data || []
if (loading || !data) {
return (
<Stack py="lg" gap="md">
<Skeleton h={40} radius="xl" />
<Skeleton h={500} radius="md" />
</Stack>
)
}
return (
<Box py="lg">
<Paper
bg={colors['white-1']}
p="lg"
radius="xl"
shadow="sm"
withBorder
>
<Group justify="space-between" align="center" mb="md">
<Group gap="sm">
<IconMicroscope size={28} stroke={1.5} color={colors['blue-button']} />
<Title order={2} fz="xl">Daftar Pengajar</Title>
</Group>
<TextInput
placeholder='pencarian'
leftSection={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
</Group>
{filteredData.length === 0 ? (
<Stack align="center" justify="center" py="xl" gap="sm">
<IconLayersSubtract size={48} stroke={1.5} color={"gray"} />
<Text fz="lg" c="dimmed">Belum ada data pengajar</Text>
</Stack>
) : (
<Table
striped
highlightOnHover
withTableBorder
withRowBorders
horizontalSpacing="md"
verticalSpacing="sm"
fz="sm"
>
<TableThead>
<TableTr>
<TableTh w="30%">Nama Pengajar</TableTh>
<TableTh w="30%">Nama Lembaga</TableTh>
<TableTh w="40%">Jenjang Pendidikan</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd fw={500}>{item.nama}</TableTd>
<TableTd>{item.lembaga.nama}</TableTd>
<TableTd>{item.lembaga.jenjangPendidikan?.nama || '-'}</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
)}
</Paper>
{filteredData.length > 0 && (
<Center mt="lg">
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
total={totalPages}
size="md"
radius="xl"
withEdges
/>
</Center>
)}
</Box>
);
}
export default Page;

View File

@@ -0,0 +1,119 @@
'use client'
import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud';
import colors from '@/con/colors';
import { Box, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, TextInput, Title } from '@mantine/core';
import { IconSchool, IconLayersSubtract, IconSearch } from '@tabler/icons-react';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { useProxy } from 'valtio/utils';
import { use, useState } from 'react';
interface PageProps {
params: Promise<{ jenjangPendidikan: string }>
}
function Page({ params }: PageProps) {
const [search, setSearch] = useState("")
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const stateList = useProxy(infoSekolahPaud.siswa)
const { jenjangPendidikan } = use(params);
const {
data,
page,
totalPages,
loading,
load,
} = stateList.findMany
useShallowEffect(() => {
// Decode the URL parameter and pass it to load
const decodedJenjang = decodeURIComponent(jenjangPendidikan).toLowerCase()
load(page, 10, debouncedSearch, decodedJenjang === 'semua' ? '' : decodedJenjang)
}, [page, jenjangPendidikan, debouncedSearch])
const filteredData = data || []
if (loading || !data) {
return (
<Stack py="lg" gap="md">
<Skeleton h={40} radius="xl" />
<Skeleton h={500} radius="md" />
</Stack>
)
}
return (
<Box py="lg">
<Paper
bg={colors['white-1']}
p="lg"
radius="xl"
shadow="sm"
withBorder
>
<Group justify="space-between" align="center" mb="md">
<Group gap="sm">
<IconSchool size={28} stroke={1.5} color={colors['blue-button']} />
<Title order={2} fz="xl">Daftar Siswa</Title>
</Group>
<TextInput
placeholder='pencarian'
leftSection={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
</Group>
{filteredData.length === 0 ? (
<Stack align="center" justify="center" py="xl" gap="sm">
<IconLayersSubtract size={48} stroke={1.5} color={"gray"} />
<Text fz="lg" c="dimmed">Belum ada data siswa</Text>
</Stack>
) : (
<Table
striped
highlightOnHover
withTableBorder
withRowBorders
horizontalSpacing="md"
verticalSpacing="sm"
fz="sm"
>
<TableThead>
<TableTr>
<TableTh w="30%">Nama Siswa</TableTh>
<TableTh w="30%">Nama Lembaga</TableTh>
<TableTh w="40%">Jenjang Pendidikan</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd fw={500}>{item.nama}</TableTd>
<TableTd>{item.lembaga.nama}</TableTd>
<TableTd>{item.lembaga.jenjangPendidikan?.nama || '-'}</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
)}
</Paper>
{filteredData.length > 0 && (
<Center mt="lg">
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
total={totalPages}
size="md"
radius="xl"
withEdges
/>
</Center>
)}
</Box>
);
}
export default Page;

View File

@@ -0,0 +1,94 @@
'use client'
import colors from '@/con/colors';
import {
ActionIcon,
Box,
Button,
Container,
Group,
Paper,
Stack,
Text,
VisuallyHidden
} from '@mantine/core';
import { IconArrowLeft } from '@tabler/icons-react';
import { useRouter, useSearchParams } from 'next/navigation';
import React, { useState } from 'react';
type LayoutSekolahProps = {
title?: string;
jenjangPendidikanList?: string[];
children: React.ReactNode;
};
export default function LayoutSekolah({
title = 'Cari Informasi Sekolah',
jenjangPendidikanList = ['Semua', 'TK', 'SD', 'SMP', 'SMA'],
children,
}: LayoutSekolahProps) {
const router = useRouter();
const searchParams = useSearchParams();
const initialJenjangPendidikan = searchParams.get('jenjangPendidikan') || 'Semua';
const [jenjangPendidikanAktif, setJenjangPendidikanAktif] = useState(initialJenjangPendidikan);
// Cleanup timeout
// Handle jenjang pendidikan click
const handleJenjangPendidikanChange = (k: string) => {
// arahkan langsung ke route jenjang pendidikan
if (k.toLowerCase() === 'semua') {
setJenjangPendidikanAktif(k);
router.push(`/darmasaba/pendidikan/info-sekolah/semua`);
} else {
setJenjangPendidikanAktif(k);
router.push(`/darmasaba/pendidikan/info-sekolah/${encodeURIComponent(k.toLowerCase())}`);
}
};
return (
<Box style={{ minHeight: '100vh', background: colors.Bg, paddingBottom: 48 }}>
<Container size="xl" py={{ base: 'md', md: 'xl' }}>
<Stack gap="lg">
{/* Back Button */}
<ActionIcon onClick={() => window.history.back()} variant="light" radius="md" size="lg">
<IconArrowLeft size={20} />
<VisuallyHidden>Kembali</VisuallyHidden>
</ActionIcon>
{/* Search & Filter */}
<Paper radius="lg" p="xl" withBorder>
<Stack gap="md">
<Text ta="center" fw={800} fz={28}>{title}</Text>
<Text ta="center" fz={"md"} c="black">
Temukan data lengkap mengenai lembaga pendidikan, jumlah siswa terdaftar, dan tenaga pengajar berdasarkan jenjang pendidikan yang tersedia (TK, SD, SMP, SMA). Gunakan tombol di bawah untuk melihat detail sesuai kebutuhanmu.
</Text>
<Group justify="center" gap="xs" wrap="wrap">
{jenjangPendidikanList.map((k) => {
const aktif = k === jenjangPendidikanAktif;
return (
<Button
key={k}
onClick={() => handleJenjangPendidikanChange(k)}
radius="xl"
size="sm"
variant={aktif ? 'filled' : 'light'}
>
{k}
</Button>
);
})}
</Group>
</Stack>
</Paper>
{/* Slot konten */}
{children}
</Stack>
</Container>
</Box>
);
}

View File

@@ -0,0 +1,18 @@
'use client'
import dynamic from 'next/dynamic';
import React from 'react';
const LayoutSekolah = dynamic(
() => import('./_lib/layoutTabs'),
{ ssr: false }
);
function Layout({children} : {children: React.ReactNode}) {
return (
<LayoutSekolah>
{children}
</LayoutSekolah>
);
}
export default Layout;

View File

@@ -0,0 +1,109 @@
'use client'
import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud';
import colors from '@/con/colors';
import { Box, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, TextInput, Title } from '@mantine/core';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconChalkboard, IconLayersSubtract, IconSearch } from '@tabler/icons-react';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function Page() {
const [search, setSearch] = useState("")
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const stateList = useProxy(infoSekolahPaud.lembagaPendidikan)
const {
data,
page,
totalPages,
loading,
load,
} = stateList.findMany
useShallowEffect(() => {
load(page, 10, debouncedSearch)
}, [page, debouncedSearch])
const filteredData = data || []
if (loading || !data) {
return (
<Stack py="lg" gap="md">
<Skeleton h={40} radius="xl" />
<Skeleton h={500} radius="md" />
</Stack>
)
}
return (
<Box py="lg">
<Paper
bg={colors['white-1']}
p="lg"
radius="xl"
shadow="sm"
withBorder
>
<Group justify="space-between" align="center" mb="md">
<Group gap="sm">
<IconChalkboard size={28} stroke={1.5} color={colors['blue-button']} />
<Title order={2} fz="xl">Daftar Lembaga Pendidikan</Title>
</Group>
<TextInput
placeholder='pencarian'
leftSection={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
</Group>
{filteredData.length === 0 ? (
<Stack align="center" justify="center" py="xl" gap="sm">
<IconLayersSubtract size={48} stroke={1.5} color={"gray"} />
<Text fz="lg" c="dimmed">Belum ada data lembaga pendidikan</Text>
</Stack>
) : (
<Table
striped
highlightOnHover
withTableBorder
withRowBorders
horizontalSpacing="md"
verticalSpacing="sm"
fz="sm"
>
<TableThead>
<TableTr>
<TableTh w="60%">Nama Lembaga</TableTh>
<TableTh w="40%">Jenjang Pendidikan</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd fw={500}>{item.nama}</TableTd>
<TableTd>{item.jenjangPendidikan?.nama || '-'}</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
)}
</Paper>
{filteredData.length > 0 && (
<Center mt="lg">
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
total={totalPages}
size="md"
radius="xl"
withEdges
/>
</Center>
)}
</Box>
);
}
export default Page;

View File

@@ -0,0 +1,271 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud';
import {
ActionIcon,
Box,
Button,
Center,
Container,
Group,
Paper,
SimpleGrid,
Skeleton,
Stack,
Text,
Tooltip,
} from '@mantine/core';
import type { IconProps } from '@tabler/icons-react';
import {
IconChalkboard,
IconInfoCircle,
IconMicroscope,
IconRefresh,
IconSchool,
} from '@tabler/icons-react';
import { motion } from 'framer-motion';
import { useTransitionRouter } from 'next-view-transitions';
import React, { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
type Stat = {
icon: React.ComponentType<IconProps>;
jumlah: number;
nama: string;
helper?: string;
loading?: boolean;
};
export default function SekolahPage() {
const [stats, setStats] = useState<Stat[]>([
{
icon: IconChalkboard,
jumlah: 0,
nama: 'Lembaga Pendidikan',
helper: 'Jumlah institusi pendidikan resmi di wilayah ini',
loading: true,
},
{
icon: IconSchool,
jumlah: 0,
nama: 'Siswa Terdaftar',
helper: 'Total siswa aktif di semua jenjang',
loading: true,
},
{
icon: IconMicroscope,
jumlah: 0,
nama: 'Tenaga Pengajar',
helper: 'Jumlah guru dan staf pengajar aktif',
loading: true,
},
]);
const router = useTransitionRouter()
const stateLembaga = useProxy(infoSekolahPaud.lembagaPendidikan);
const stateSiswa = useProxy(infoSekolahPaud.siswa);
const statePengajar = useProxy(infoSekolahPaud.pengajar);
const loadData = async () => {
try {
// Load lembaga data
await stateLembaga.findMany.load(1, 1, '');
const totalLembaga = stateLembaga.findMany.total || 0;
// Load siswa data
await stateSiswa.findMany.load(1, 1, '');
const totalSiswa = stateSiswa.findMany.total || 0;
// Load pengajar data
await statePengajar.findMany.load(1, 1, '');
const totalPengajar = statePengajar.findMany.total || 0;
setStats([
{
icon: IconChalkboard,
jumlah: totalLembaga,
nama: 'Lembaga Pendidikan',
helper: 'Jumlah institusi pendidikan resmi di wilayah ini',
loading: false,
},
{
icon: IconSchool,
jumlah: totalSiswa,
nama: 'Siswa Terdaftar',
helper: 'Total siswa aktif di semua jenjang',
loading: false,
},
{
icon: IconMicroscope,
jumlah: totalPengajar,
nama: 'Tenaga Pengajar',
helper: 'Jumlah guru dan staf pengajar aktif',
loading: false,
},
]);
} catch (error) {
console.error('Error loading data:', error);
// Set error state or show toast notification
}
};
useEffect(() => {
loadData();
}, []);
const handleRefresh = () => {
setStats(prev => prev.map(stat => ({ ...stat, loading: true })));
loadData();
};
const [query] = useState('');
const filtered = stats.filter((d) => {
const q = query.trim().toLowerCase();
if (!q) return true;
const teks = `${d.nama} ${d.jumlah}`.toLowerCase();
return teks.includes(q);
});
return (
<Paper radius="md" style={{ minHeight: '100vh', background: '#f8fafc', paddingBottom: 48 }}>
<Container size="xl" py={{ base: 'md', md: 'xl' }}>
<Box>
<Group justify="start" mb="md">
<Button
leftSection={<IconRefresh size={16} />}
variant="outline"
size="xs"
onClick={handleRefresh}
loading={stats.some(stat => stat.loading)}
>
Segarkan Data
</Button>
</Group>
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg">
{filtered.length === 0 ? (
<Paper
p="xl"
radius="md"
style={{
background: '#f9fafb',
border: '1px dashed #e2e8f0',
minHeight: 220,
}}
role="status"
aria-label="Tidak ada hasil"
>
<Center style={{ minHeight: 180, flexDirection: 'column' }}>
<Text fz="lg" fw={800} c="#2563eb">
Tidak ditemukan
</Text>
<Text c="dimmed" mt="6px">
Coba gunakan kata kunci lain atau setel ulang filter.
</Text>
<Button
mt="md"
radius="xl"
onClick={handleRefresh}
style={{
background: 'linear-gradient(90deg, #3b82f6 0%, #60a5fa 100%)',
color: 'white',
boxShadow: '0 6px 18px rgba(59,130,246,0.25)',
}}
aria-label="Tampilkan semua"
>
Tampilkan Semua
</Button>
</Center>
</Paper>
) : (
filtered.map((v) => (
<motion.div
key={v.nama}
whileHover={{ scale: 1.025 }}
whileTap={{ scale: 0.995 }}
style={{ width: '100%' }}
>
<Skeleton visible={v.loading}>
<Paper
p="lg"
radius="lg"
style={{
background: 'white',
border: '1px solid #e2e8f0',
boxShadow: '0 8px 28px rgba(0,0,0,0.06)',
minHeight: 260,
}}
role="article"
aria-label={`${v.nama} kartu statistik`}
>
<Stack gap="sm" mb="md">
<Center>
<Box
style={{
width: 80,
height: 80,
borderRadius: 16,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: '#eff6ff',
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.6)',
}}
aria-hidden
>
{React.createElement(v.icon, {
color: '#2563eb',
size: 34,
stroke: 1.6,
})}
</Box>
</Center>
<Group justify="center" align="center" gap="xs">
<Stack gap={0}>
<Text ta={"center"} fz={{ base: 18, md: 22 }} fw={800} c="#0f172a">
{v.jumlah.toLocaleString()}
</Text>
<Group gap={6} align="center">
<Text ta={"center"} fz="sm" fw={700} c="#2563eb">
{v.nama}
</Text>
<Tooltip label={v.helper ?? ''} position="right" withArrow>
<ActionIcon aria-label={`Info ${v.nama}`} variant="transparent" size="xs">
<IconInfoCircle size={16} style={{ color: '#2563eb' }} />
</ActionIcon>
</Tooltip>
</Group>
</Stack>
</Group>
</Stack>
<Group justify="center" mt="8px">
<Button
radius="xl"
variant="outline"
aria-label={`Lihat detail ${v.nama}`}
style={{
borderColor: '#e2e8f0',
color: '#2563eb',
paddingLeft: 20,
paddingRight: 20,
}}
onClick={() => {
if (v.nama === "Lembaga Pendidikan") router.push(`/darmasaba/pendidikan/info-sekolah/semua/lembaga`);
if (v.nama === "Siswa Terdaftar") router.push(`/darmasaba/pendidikan/info-sekolah/semua/siswa`);
if (v.nama === "Tenaga Pengajar") router.push(`/darmasaba/pendidikan/info-sekolah/semua/pengajar`);
}}
>
Lihat Detail
</Button>
</Group>
</Paper>
</Skeleton>
</motion.div>
))
)}
</SimpleGrid>
</Box>
</Container>
</Paper>
);
}

View File

@@ -0,0 +1,110 @@
'use client'
import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud';
import colors from '@/con/colors';
import { Box, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, TextInput, Title } from '@mantine/core';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconLayersSubtract, IconMicroscope, IconSearch } from '@tabler/icons-react';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function Page() {
const stateList = useProxy(infoSekolahPaud.pengajar)
const [search, setSearch] = useState("")
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const {
data,
page,
totalPages,
loading,
load,
} = stateList.findMany
useShallowEffect(() => {
load(page, 10, debouncedSearch)
}, [page, debouncedSearch])
const filteredData = data || []
if (loading || !data) {
return (
<Stack py="lg" gap="md">
<Skeleton h={40} radius="xl" />
<Skeleton h={500} radius="md" />
</Stack>
)
}
return (
<Box py="lg">
<Paper
bg={colors['white-1']}
p="lg"
radius="xl"
shadow="sm"
withBorder
>
<Group justify="space-between" align="center" mb="md">
<Group gap="sm">
<IconMicroscope size={28} stroke={1.5} color={colors['blue-button']} />
<Title order={2} fz="xl">Daftar Pengajar</Title>
</Group>
<TextInput
placeholder='pencarian'
leftSection={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
</Group>
{filteredData.length === 0 ? (
<Stack align="center" justify="center" py="xl" gap="sm">
<IconLayersSubtract size={48} stroke={1.5} color={"gray"} />
<Text fz="lg" c="dimmed">Belum ada data pengajar</Text>
</Stack>
) : (
<Table
striped
highlightOnHover
withTableBorder
withRowBorders
horizontalSpacing="md"
verticalSpacing="sm"
fz="sm"
>
<TableThead>
<TableTr>
<TableTh w="30%">Nama Pengajar</TableTh>
<TableTh w="30%">Nama Lembaga</TableTh>
<TableTh w="40%">Jenjang Pendidikan</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.nama}</TableTd>
<TableTd>{item.lembaga.nama}</TableTd>
<TableTd>{item.lembaga.jenjangPendidikan?.nama || '-'}</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
)}
</Paper>
{filteredData.length > 0 && (
<Center mt="lg">
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
total={totalPages}
size="md"
radius="xl"
withEdges
/>
</Center>
)}
</Box>
);
}
export default Page;

View File

@@ -0,0 +1,111 @@
'use client'
import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud';
import colors from '@/con/colors';
import { Box, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, TextInput, Title } from '@mantine/core';
import { IconSchool, IconLayersSubtract, IconSearch } from '@tabler/icons-react';
import { useShallowEffect, useDebouncedValue } from '@mantine/hooks';
import { useProxy } from 'valtio/utils';
import { useState } from 'react';
function Page() {
const [search, setSearch] = useState("")
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const stateList = useProxy(infoSekolahPaud.siswa)
const {
data,
page,
totalPages,
loading,
load,
} = stateList.findMany
useShallowEffect(() => {
load(page, 10, debouncedSearch)
}, [page, debouncedSearch])
const filteredData = data || []
if (loading || !data) {
return (
<Stack py="lg" gap="md">
<Skeleton h={40} radius="xl" />
<Skeleton h={500} radius="md" />
</Stack>
)
}
return (
<Box py="lg">
<Paper
bg={colors['white-1']}
p="lg"
radius="xl"
shadow="sm"
withBorder
>
<Group justify="space-between" align="center" mb="md">
<Group gap="sm">
<IconSchool size={28} stroke={1.5} color={colors['blue-button']} />
<Title order={2} fz="xl">Daftar Siswa</Title>
</Group>
<TextInput
placeholder='pencarian'
leftSection={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
</Group>
{filteredData.length === 0 ? (
<Stack align="center" justify="center" py="xl" gap="sm">
<IconLayersSubtract size={48} stroke={1.5} color={"gray"} />
<Text fz="lg" c="dimmed">Belum ada data siswa</Text>
</Stack>
) : (
<Table
striped
highlightOnHover
withTableBorder
withRowBorders
horizontalSpacing="md"
verticalSpacing="sm"
fz="sm"
>
<TableThead>
<TableTr>
<TableTh w="30%">Nama Siswa</TableTh>
<TableTh w="30%">Nama Lembaga</TableTh>
<TableTh w="40%">Jenjang Pendidikan</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.nama}</TableTd>
<TableTd>{item.lembaga.nama}</TableTd>
<TableTd>{item.lembaga.jenjangPendidikan?.nama || '-'}</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
)}
</Paper>
{filteredData.length > 0 && (
<Center mt="lg">
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
total={totalPages}
size="md"
radius="xl"
withEdges
/>
</Center>
)}
</Box>
);
}
export default Page;

View File

@@ -1,92 +1,107 @@
'use client'
import pendidikanNonFormalState from '@/app/admin/(dashboard)/_state/pendidikan/pendidikan-non-formal';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Stack, Box, Title, Text, SimpleGrid, Paper, List, ListItem } from '@mantine/core'; import { Box, Paper, SimpleGrid, Skeleton, Stack, Text, Title, Tooltip } from '@mantine/core';
import React from 'react'; import { useShallowEffect } from '@mantine/hooks';
import { useProxy } from 'valtio/utils';
import { IconMapPin, IconTarget, IconBook2 } from '@tabler/icons-react';
import BackButton from '../../desa/layanan/_com/BackButto'; import BackButton from '../../desa/layanan/_com/BackButto';
function Page() { function Page() {
const stateTujuanPendidikanNonFormal = useProxy(pendidikanNonFormalState.stateTujuanPendidikanNonFormal);
const stateTempatKegiatan = useProxy(pendidikanNonFormalState.stateTempatKegiatan);
const stateJenisProgram = useProxy(pendidikanNonFormalState.stateJenisProgram);
useShallowEffect(() => {
stateTujuanPendidikanNonFormal.findById.load('edit');
stateTempatKegiatan.findById.load('edit');
stateJenisProgram.findById.load('edit');
}, []);
if (!stateTujuanPendidikanNonFormal.findById.data || !stateTempatKegiatan.findById.data || !stateJenisProgram.findById.data)
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="lg" mih="100vh" justify="flex-start">
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }} pb={50}>
<Skeleton h={50} radius="xl" />
<Skeleton h={150} mt="lg" radius="md" />
<Skeleton h={150} mt="lg" radius="md" />
</Box>
</Stack>
);
return ( return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}> <Stack pos="relative" bg={colors.Bg} py="xl" gap="lg" mih="100vh">
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
<BackButton /> <BackButton />
</Box> </Box>
<Box px={{ base: 'md', md: 100 }} pb={50}> <Box px={{ base: 'md', md: 100 }} pb={50}>
<Box> <Box>
<Title ta={'center'} order={1} fw={'bold'} c={colors['blue-button']}> <Title ta="center" order={1} fw="bold" c={colors['blue-button']} mb="sm">
Pendidikan Non Formal Pendidikan Non Formal
</Title> </Title>
<Text pb={20} ta={'justify'} fz={'xl'} px={{ base: 'md', md: 100 }}> <Text ta="center" fz="lg" lh={1.6} c="black" maw={800} mx="auto">
Pendidikan Non Formal adalah bentuk pendidikan di luar sekolah yang diselenggarakan secara terstruktur dan bertujuan memberikan keterampilan, pengetahuan, serta pengembangan karakter masyarakat dari berbagai usia dan latar belakang. Bentuk pendidikan di luar sekolah yang terstruktur, bertujuan memberikan keterampilan, pengetahuan, dan pengembangan karakter masyarakat dari berbagai usia serta latar belakang.
</Text> </Text>
</Box> </Box>
<SimpleGrid <SimpleGrid
cols={{ cols={{ base: 1, md: 2 }}
base: 1, spacing="lg"
md: 2 mt={40}
}}
> >
<Box> <Paper
<Paper p={'xl'} radius={'md'} bg={colors['white-trans-1']}> p="xl"
<Title order={2} fw={'bold'} c={colors['blue-button']}> radius="lg"
Tujuan Program bg={colors['white-trans-1']}
</Title> shadow="md"
<List> withBorder
<ListItem fz={'h4'}>Memberikan kesempatan belajar yang fleksibel bagi warga desa</ListItem> >
<ListItem fz={'h4'}>Meningkatkan keterampilan hidup dan kemandirian ekonomi</ListItem>
<ListItem fz={'h4'}>Mendorong partisipasi masyarakat dalam pembangunan desa</ListItem>
<ListItem fz={'h4'}>Mengurangi angka putus sekolah dan meningkatkan kualitas SDM</ListItem>
</List>
</Paper>
</Box>
<Box>
<Paper h={{ base: 0, md: 210 }} p={'xl'} radius={'md'} bg={colors['white-trans-1']}>
<Title order={2} fw={'bold'} c={colors['blue-button']}>
Tempat Kegiatan
</Title>
<List>
<ListItem fz={'h4'}>Balai Desa Darmasaba</ListItem>
<ListItem fz={'h4'}>TPK, Perpustakaan Desa, atau Posyandu</ListItem>
<ListItem fz={'h4'}>Bisa juga dilakukan secara mobile atau door to door</ListItem>
</List>
</Paper>
</Box>
</SimpleGrid>
<Box py={20}>
<Paper p={'xl'} radius={'md'} bg={colors['white-trans-1']}>
<Title order={2} fw={'bold'} c={colors['blue-button']}>
Jenis Program yang Diselenggarakan
</Title>
<Text fz={'h4'}>Program Pendidikan Non Formal yang diselenggarakan di Desa Darmasaba meliputi:</Text>
<Stack> <Stack>
<Box> <Tooltip label="Fokus utama program" withArrow>
<Text fz={'h4'}> 1) Keaksaraan Fungsional</Text> <Title order={2} fw="bold" c={colors['blue-button']} mb="xs" flex="center">
<List> <IconTarget size={28} style={{ marginRight: 8 }} />
<ListItem fz={'h4'}>Untuk warga yang belum bisa membaca dan menulis</ListItem> Tujuan Program
</List> </Title>
</Box> </Tooltip>
<Box> <Text fz="md" lh={1.7} c="dark" dangerouslySetInnerHTML={{ __html: stateTujuanPendidikanNonFormal.findById.data?.deskripsi }} />
<Text fz={'h4'}> 2) Pendidikan Kesetaraan (Paket A, B, C)</Text> </Stack>
<List> </Paper>
<ListItem fz={'h4'}>Setara SD, SMP, dan SMA bagi yang tidak menyelesaikan pendidikan formal</ListItem> <Paper
</List> p="xl"
</Box> radius="lg"
<Box> bg={colors['white-trans-1']}
<Text fz={'h4'}> 3) Pelatihan Keterampilan</Text> shadow="md"
<List> withBorder
<ListItem fz={'h4'}>Menjahit, memasak, sablon, pertanian, peternakan, hingga teknologi digital</ListItem> >
</List> <Stack>
</Box> <Tooltip label="Lokasi pelaksanaan kegiatan" withArrow>
<Box> <Title order={2} fw="bold" c={colors['blue-button']} mb="xs" flex="center">
<Text fz={'h4'}> 4) Kursus & Pelatihan Soft Skill</Text> <IconMapPin size={28} style={{ marginRight: 8 }} />
<List> Tempat Kegiatan
<ListItem fz={'h4'}>Public speaking, pengelolaan keuangan, kepemimpinan pemuda</ListItem> </Title>
</List> </Tooltip>
</Box> <Text fz="md" lh={1.7} c="dark" dangerouslySetInnerHTML={{ __html: stateTempatKegiatan.findById.data?.deskripsi }} />
<Box> </Stack>
<Text fz={'h4'}> 5) Pendidikan Keluarga & Parenting</Text> </Paper>
<List> </SimpleGrid>
<ListItem fz={'h4'}>Untuk membekali orang tua dalam mendampingi tumbuh kembang anak</ListItem> <Box py={40}>
</List> <Paper
</Box> p="xl"
radius="lg"
bg={colors['white-trans-1']}
shadow="md"
withBorder
>
<Stack>
<Tooltip label="Ragam jenis program yang tersedia" withArrow>
<Title order={2} fw="bold" c={colors['blue-button']} mb="xs" flex="center">
<IconBook2 size={28} style={{ marginRight: 8 }} />
Jenis Program yang Diselenggarakan
</Title>
</Tooltip>
<Text fz="md" lh={1.7} c="dark" dangerouslySetInnerHTML={{ __html: stateJenisProgram.findById.data?.deskripsi }} />
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

@@ -0,0 +1,168 @@
'use client'
import perpustakaanDigitalState from '@/app/admin/(dashboard)/_state/pendidikan/perpustakaan-digital';
import colors from '@/con/colors';
import { ActionIcon, Box, Center, Group, Image, Paper, SimpleGrid, Skeleton, Spoiler, Stack, Text, Tooltip, Badge } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconBook2, IconRefresh } from '@tabler/icons-react';
import { motion } from 'framer-motion';
import { useSearchParams } from 'next/navigation';
import { useCallback, useState } from 'react';
import { useProxy } from 'valtio/utils';
function Content({ kategoriBuku }: { kategoriBuku: string }) {
const state = useProxy(perpustakaanDigitalState);
const [expandedId, setExpandedId] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
const searchParams = useSearchParams();
const searchQuery = searchParams.get('search') || '';
const decodedKategoriBuku = decodeURIComponent(kategoriBuku);
const kategoriFilter = decodedKategoriBuku.toLowerCase() === 'semua' ? '' : decodedKategoriBuku;
const loadData = useCallback(async (searchQuery: string = '') => {
try {
setIsLoading(true);
await state.dataPerpustakaan.findMany.load(1, 100, searchQuery, kategoriFilter);
} finally {
setIsLoading(false);
}
}, [kategoriFilter, state.dataPerpustakaan.findMany]);
useShallowEffect(() => {
loadData(searchQuery);
}, [searchQuery, loadData]);
const handleRefresh = () => {
loadData();
};
if (isLoading || !state.dataPerpustakaan.findMany.load || !state.dataPerpustakaan.findMany.data) {
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
<Box px={{ base: 'md', md: 100 }} pb={50}>
<Skeleton h={50} radius="xl" />
<Skeleton h={180} mt="lg" radius="md" />
</Box>
</Stack>
);
}
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
<Box px={{ base: 'md', md: 100 }} pb={20}>
<Group justify="space-between" mb="lg">
<Group gap="xs">
<IconBook2 size={28} color={colors['blue-button']} />
<Text fw={700} size="xl" c={colors['blue-button']}>
Koleksi Buku
</Text>
</Group>
<Tooltip label="Muat ulang koleksi" withArrow>
<ActionIcon
variant="gradient"
gradient={{ from: 'blue', to: 'cyan' }}
onClick={handleRefresh}
loading={isLoading}
radius="xl"
size="lg"
>
<IconRefresh size={20} />
</ActionIcon>
</Tooltip>
</Group>
{!state.dataPerpustakaan.findMany.data || state.dataPerpustakaan.findMany.data.length === 0 ? (
<Center py="xl">
<Stack gap="xs" align="center">
<Image src="/empty-books.svg" alt="Kosong" w={140} h="auto" />
<Text c="dimmed" fz="sm">Belum ada buku yang tersedia dalam kategori ini</Text>
</Stack>
</Center>
) : (
<SimpleGrid
cols={{ base: 1, sm: 2, md: 3 }}
spacing="lg"
verticalSpacing="lg"
>
{state.dataPerpustakaan.findMany.data?.map((v, k) => (
<motion.div
key={k}
whileHover={{ scale: 1.03 }}
transition={{ duration: 0.2 }}
style={{ width: '100%', height: '100%' }}
>
<Paper
p="lg"
radius="2xl"
shadow="md"
bg={colors['white-trans-1']}
style={{
height: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between'
}}
>
<Stack gap="md" style={{ flex: 1 }}>
<Center>
<Image
src={v.image?.link}
alt={v.judul}
h={180}
w="auto"
fit="contain"
fallbackSrc="/placeholder-book.jpg"
radius="md"
/>
</Center>
<Stack gap={4} align="center">
<Text
c={colors["blue-button"]}
ta="center"
fw={700}
fz={{ base: "md", md: "lg" }}
lineClamp={2}
>
{v.judul}
</Text>
{v.kategori && (
<Badge color="cyan" radius="sm" variant="light" size="sm">
{v.kategori.name}
</Badge>
)}
</Stack>
<Spoiler
maxHeight={80}
showLabel={
<Text fw={600} fz="sm" c={colors['blue-button']} ta="center" mt="xs">
Lihat deskripsi
</Text>
}
hideLabel={
<Text fw={600} fz="sm" c={colors['blue-button']} ta="center" mt="xs">
Sembunyikan deskripsi
</Text>
}
expanded={expandedId === v.id}
onExpandedChange={(isExpanded) => setExpandedId(isExpanded ? v.id : null)}
>
<Text
ta="justify"
fz="sm"
lh={1.5}
c="dimmed"
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
/>
</Spoiler>
</Stack>
</Paper>
</motion.div>
))}
</SimpleGrid>
)}
</Box>
</Stack>
);
}
export default Content;

View File

@@ -0,0 +1,14 @@
// src/app/darmasaba/(pages)/desa/berita/[kategori]/page.tsx
import { Suspense } from "react";
import Content from "./content";
export default async function Page({ params }: { params: Promise<{ kategoriBuku: string }> }) {
const { kategoriBuku } = await params;
return (
<Suspense fallback={<div>Loading...</div>}>
<Content kategoriBuku={kategoriBuku} />
</Suspense>
);
}

View File

@@ -0,0 +1,140 @@
'use client'
import { useEffect, useState } from 'react';
import { ActionIcon, Box, Flex, Grid, GridCol, Stack, Tabs, TabsList, TabsTab, Text, TextInput } from '@mantine/core';
import { IconSearch, IconUser } from '@tabler/icons-react';
import Link from 'next/link';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import BackButton from '../../../desa/layanan/_com/BackButto';
import colors from '@/con/colors';
type LayoutBukuProps = {
placeholder?: string;
searchIcon?: React.ReactNode;
value?: string;
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
children?: React.ReactNode;
};
function LayoutTabs({
placeholder = 'Cari buku digital...',
searchIcon = <IconSearch size={20} />,
children,
}: LayoutBukuProps) {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const activeTab = pathname.split('/').pop() || 'semua';
const initialSearch = searchParams.get('search') || '';
const [searchValue, setSearchValue] = useState(initialSearch);
const [searchTimeout, setSearchTimeout] = useState<number | null>(null);
const [activeTabState, setActiveTabState] = useState(activeTab);
useEffect(() => {
setActiveTabState(activeTab);
}, [activeTab]);
useEffect(() => {
return () => {
if (searchTimeout !== null) clearTimeout(searchTimeout);
};
}, [searchTimeout]);
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
setSearchValue(value);
if (searchTimeout !== null) clearTimeout(searchTimeout);
const updateSearch = () => {
const params = new URLSearchParams();
if (value) params.set('search', value);
router.push(
`/darmasaba/pendidikan/perpustakaan-digital/${activeTab}${params.toString() ? `?${params.toString()}` : ''}`
);
};
if (value === '') {
updateSearch();
} else {
const newTimeout = window.setTimeout(updateSearch, 350);
setSearchTimeout(newTimeout);
}
};
const tabs = [
{ label: 'Semua', value: 'semua', href: '/darmasaba/pendidikan/perpustakaan-digital/semua' },
{ label: 'Dokumenter', value: 'dokumenter', href: '/darmasaba/pendidikan/perpustakaan-digital/dokumenter' },
{ label: 'Sayuran', value: 'sayuran', href: '/darmasaba/pendidikan/perpustakaan-digital/sayuran' },
{ label: 'Dongeng', value: 'dongeng', href: '/darmasaba/pendidikan/perpustakaan-digital/dongeng' },
];
const handleTabChange = (value: string | null) => {
if (!value) return;
const params = new URLSearchParams(searchParams.toString());
router.push(`/darmasaba/pendidikan/perpustakaan-digital/${value}${params.toString() ? `?${params.toString()}` : ''}`);
};
return (
<Stack pos="relative" bg="var(--mantine-color-gray-0)" py="xl" gap="22">
<Box px={{ base: 'md', md: 100 }}>
<Flex justify="space-between" align="center">
<BackButton />
<ActionIcon
variant="light"
component={Link}
href="/login"
radius="xl"
size="lg"
aria-label="Masuk ke akun"
>
<IconUser size={26} stroke={1.5} />
</ActionIcon>
</Flex>
</Box>
<Box pb={20}>
<Text ta="center" fz={{ base: '1.6rem', md: '2.4rem' }} fw={700} c={colors['blue-button']}>
Perpustakaan Digital Darmasaba
</Text>
<Tabs color="blue" variant="pills" value={activeTabState} onChange={handleTabChange}>
<Box px={{ base: 'md', md: 100 }} py="md" bg="var(--mantine-color-gray-1)" style={{ borderRadius: 16 }}>
<Grid align="center" gutter="md">
<GridCol span={{ base: 12, md: 9 }}>
<TabsList>
{tabs.map((tab) => (
<TabsTab
color={colors['blue-button']}
key={tab.value}
value={tab.value}
onClick={() => router.push(tab.href)}
style={{ fontWeight: 500 }}
>
{tab.label}
</TabsTab>
))}
</TabsList>
</GridCol>
<GridCol span={{ base: 12, md: 3 }}>
<TextInput
radius="xl"
size="md"
placeholder={placeholder}
leftSection={searchIcon}
w="100%"
value={searchValue}
onChange={handleSearchChange}
aria-label="Cari judul buku"
/>
</GridCol>
</Grid>
</Box>
{children}
</Tabs>
</Box>
</Stack>
);
}
export default LayoutTabs;

View File

@@ -0,0 +1,15 @@
'use client'
import React, { Suspense } from 'react';
import LayoutTabs from './_lib/layoutTabs';
function Layout({ children }: { children: React.ReactNode }) {
return (
<Suspense fallback={<div>Loading...</div>}>
<LayoutTabs>
{children}
</LayoutTabs>
</Suspense>
);
}
export default Layout;

View File

@@ -1,111 +0,0 @@
'use client'
import colors from '@/con/colors';
import { ActionIcon, Box, Button, Center, Flex, Group, Image, Paper, SimpleGrid, Stack, Text, TextInput } from '@mantine/core';
import { IconSearch, IconUser } from '@tabler/icons-react';
import { motion } from 'framer-motion';
import BackButton from '../../desa/layanan/_com/BackButto';
import Link from 'next/link';
const dataSekolah = [
{
id: 1,
gambar: '/api/img/buku-1.png',
judul: 'Angkasa dan 56 Hari',
deskripsi: 'Angkasa dan 56 hari mengisahkan tentang sebuah perjuangan perihal asa yang belum usai. Tentang cinta pertama yang secara tiba-tiba menghilang dari kehidupan.'
},
{
id: 2,
gambar: '/api/img/buku-2.png',
judul: 'Sayuran Organik',
deskripsi: 'Buku ini membahas cara menanam sayuran secara organik, jenis-jenis sayuran organik, dan cara mengatasi hama dan penyakit. '
},
{
id: 3,
gambar: '/api/img/buku-3.png',
judul: 'Bali Tempo Dulu',
deskripsi: 'Buku Bali Tempo Doeloe oleh Adrian Vickers berisi berbagai catatan perjalanan yang menggambarkan kehidupan sosial budaya Bali di masa lampau.'
},
]
function Page() {
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}>
<Flex justify={'space-between'} align={'center'}>
<BackButton />
<ActionIcon variant='transparent' component={Link} href={'/login'}>
<IconUser color={colors["blue-button"]} size={30} />
</ActionIcon>
</Flex>
</Box>
<Box px={{ base: 'md', md: 100 }} pb={20}>
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
E-Book Desa Darmasaba
</Text>
<Group justify='center' pb={20}>
<TextInput
w={{ base: "50%", md: "70%" }}
placeholder='Cari Buku...'
rightSection={
<Button
size="xs"
style={{ height: '80%', marginRight: '5px' }}
bg={colors["blue-button"]}
>
Cari
</Button>
}
rightSectionWidth={70}
leftSection={<IconSearch size={20} />}
/>
</Group>
<Group mb={20} gap="md" justify='center' wrap="wrap">
<Paper bg={colors['blue-button']} radius="xl" py={5} px={20}>
<Text c={colors['white-1']} size="sm">
Semua
</Text>
</Paper>
{['Non Fiksi', 'Sejarah', 'Edukasi', 'Fiksi'].map((kategori) => (
<Paper key={kategori} bg={'gray'} radius="xl" py={5} px={20}>
<Text c={colors['white-1']} size="sm">
{kategori}
</Text>
</Paper>
))}
</Group>
<SimpleGrid
cols={{
base: 1,
md: 3
}}
>
{dataSekolah.map((v, k) => {
return (
<Box key={k}>
<motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.8 }}
style={{ width: '100%', height: '100%' }}
>
<Paper p={"xl"} bg={colors['white-trans-1']} w={{ base: "100%", md: "100%" }}>
<Stack>
<Center>
<Image src={v.gambar} alt='' w={{ base: 390, md: 1000 }}/>
</Center>
<Text c={colors["blue-button"]} ta={'center'} fw={'bold'} fz={{ base: "h2", md: "h1" }}>{v.judul}</Text>
<Text c={colors["blue-button"]} ta={'center'} fw={'bold'}>{v.deskripsi}</Text>
</Stack>
</Paper>
</motion.div>
</Box>
)
})}
</SimpleGrid>
</Box>
</Stack>
);
}
export default Page;

View File

@@ -0,0 +1,163 @@
'use client'
import perpustakaanDigitalState from '@/app/admin/(dashboard)/_state/pendidikan/perpustakaan-digital';
import colors from '@/con/colors';
import { Badge, Box, Center, Group, Image, Paper, SimpleGrid, Skeleton, Spoiler, Stack, Text, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { motion } from 'framer-motion';
import { useCallback, useState } from 'react';
import { useProxy } from 'valtio/utils';
import { IconBook2, IconInfoCircle } from '@tabler/icons-react';
type ContentProps = {
searchQuery: string;
};
function Content({ searchQuery }: ContentProps) {
const state = useProxy(perpustakaanDigitalState);
const [expandedId, setExpandedId] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
const loadData = useCallback(
async (query: string = '') => {
try {
setIsLoading(true);
await state.dataPerpustakaan.findMany.load(1, 100, query, '');
} catch (error) {
console.error('Gagal memuat data:', error);
} finally {
setIsLoading(false);
}
},
[state.dataPerpustakaan.findMany]
);
useShallowEffect(() => {
loadData(searchQuery);
}, [searchQuery, loadData]);
if (
isLoading ||
!state.dataPerpustakaan.findMany.load ||
!state.dataPerpustakaan.findMany.data
) {
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
<Box px={{ base: 'md', md: 100 }} pb={50}>
<Skeleton h={60} radius="xl" />
<Skeleton h={200} mt="lg" radius="md" />
</Box>
</Stack>
);
}
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
<Box px={{ base: 'md', md: 100 }} pb={20}>
<Group justify="space-between" mb="lg">
<Group gap="xs">
<IconBook2 size={28} color={colors['blue-button']} />
<Text fw={700} size="xl" c={colors['blue-button']}>
Koleksi Semua Buku
</Text>
</Group>
<Tooltip label="Temukan buku favorit Anda di sini" withArrow>
<IconInfoCircle size={22} color={colors['blue-button']} />
</Tooltip>
</Group>
{!state.dataPerpustakaan.findMany.data ||
state.dataPerpustakaan.findMany.data.length === 0 ? (
<Center py="xl">
<Stack gap="xs" align="center">
<Image src="/empty-books.svg" alt="Kosong" w={140} h="auto" />
<Text c="dimmed" fz="sm">Belum ada buku yang tersedia</Text>
</Stack>
</Center>
) : (
<SimpleGrid
cols={{ base: 1, sm: 2, md: 3 }}
spacing="lg"
verticalSpacing="lg"
>
{state.dataPerpustakaan.findMany.data?.map((v, k) => (
<motion.div
key={k}
whileHover={{ scale: 1.03 }}
transition={{ duration: 0.2 }}
style={{ width: '100%', height: '100%' }}
>
<Paper
p="lg"
radius="2xl"
shadow="md"
bg="white"
style={{
height: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between'
}}
>
<Stack gap="md" style={{ flex: 1 }}>
<Center>
<Image
src={v.image?.link}
alt={v.judul}
h={180}
w="auto"
fit="contain"
fallbackSrc="/placeholder-book.jpg"
radius="md"
/>
</Center>
<Stack gap={4} align="center">
<Text
c={colors["blue-button"]}
ta="center"
fw={700}
fz={{ base: "md", md: "lg" }}
lineClamp={2}
>
{v.judul}
</Text>
{v.kategori && (
<Badge color="cyan" radius="sm" variant="light" size="sm">
{v.kategori.name}
</Badge>
)}
</Stack>
<Spoiler
maxHeight={80}
showLabel={
<Text fw={600} fz="sm" c={colors['blue-button']} ta="center" mt="xs">
Lihat deskripsi
</Text>
}
hideLabel={
<Text fw={600} fz="sm" c={colors['blue-button']} ta="center" mt="xs">
Sembunyikan deskripsi
</Text>
}
expanded={expandedId === v.id}
onExpandedChange={(isExpanded) => setExpandedId(isExpanded ? v.id : null)}
>
<Text
ta="justify"
fz="sm"
lh={1.5}
c="dimmed"
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
/>
</Spoiler>
</Stack>
</Paper>
</motion.div>
))}
</SimpleGrid>
)}
</Box>
</Stack>
);
}
export default Content;

View File

@@ -0,0 +1,13 @@
// src/app/darmasaba/(pages)/desa/berita/[kategori]/page.tsx
import { Suspense } from "react";
import Content from "./content";
export default async function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Content searchQuery="" />
</Suspense>
);
}

View File

@@ -1,55 +1,96 @@
'use client'
import stateProgramPendidikanAnak from '@/app/admin/(dashboard)/_state/pendidikan/program-pendidikan-anak';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Stack, Box, Title, Text, SimpleGrid, Paper, List, ListItem } from '@mantine/core'; import { Box, Paper, SimpleGrid, Skeleton, Stack, Text, Title, Tooltip, Divider, Group } from '@mantine/core';
import React from 'react'; import { useShallowEffect } from '@mantine/hooks';
import { useProxy } from 'valtio/utils';
import { IconBook2, IconTargetArrow } from '@tabler/icons-react';
import BackButton from '../../desa/layanan/_com/BackButto'; import BackButton from '../../desa/layanan/_com/BackButto';
function Page() { function Page() {
const stateUnggulan = useProxy(stateProgramPendidikanAnak.programUnggulanState);
const stateTujuan = useProxy(stateProgramPendidikanAnak.stateTujuanProgram);
useShallowEffect(() => {
stateUnggulan.findById.load('edit');
stateTujuan.findById.load('edit');
}, []);
if (!stateUnggulan.findById.data || !stateTujuan.findById.data)
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }} pb={50}>
<Skeleton h={50} radius="xl" />
<Skeleton h={150} mt="lg" radius="md" />
</Box>
</Stack>
);
return ( return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}> <Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
<BackButton /> <BackButton />
</Box> </Box>
<Box px={{ base: 'md', md: 100 }} pb={50}> <Box px={{ base: 'md', md: 100 }} pb={50}>
<Box> <Box mb="xl">
<Title ta={'center'} order={1} fw={'bold'} c={colors['blue-button']}> <Title ta="center" order={1} fw="bold" c={colors['blue-button']} mb="sm">
Program Pendidikan Anak Program Pendidikan Anak
</Title> </Title>
<Text pb={20} ta={'justify'} fz={'xl'} px={{ base: 'md', md: 100 }}> <Text ta="center" fz="lg" c="black" mb="lg" maw={800} mx="auto">
Desa Darmasaba berkomitmen untuk menciptakan generasi muda yang cerdas, berkarakter, dan berdaya saing melalui berbagai program pendidikan yang inklusif dan berkelanjutan. Pendidikan anak menjadi pondasi utama dalam mewujudkan masa depan desa yang lebih baik. Desa Darmasaba berkomitmen mencetak generasi muda yang cerdas, berkarakter, dan siap bersaing melalui program pendidikan yang inklusif dan berkelanjutan.
</Text> </Text>
<Divider size="sm" color={colors['blue-button']} mx="auto" maw={120} />
</Box> </Box>
<SimpleGrid <SimpleGrid
cols={{ cols={{ base: 1, md: 2 }}
base: 1, spacing="xl"
md: 2
}}
> >
<Box> <Paper
<Paper p={'xl'} radius={'md'} bg={colors['white-trans-1']}> p="xl"
<Title order={2} fw={'bold'} c={colors['blue-button']}> radius="xl"
Tujuan Program withBorder
</Title> bg="white"
<List> shadow="md"
<ListItem fz={'h4'}>Meningkatkan akses pendidikan yang merata dan berkualitas</ListItem> style={{ transition: 'transform 0.2s ease', cursor: 'default' }}
<ListItem fz={'h4'}>Menumbuhkan semangat belajar sejak dini</ListItem> >
<ListItem fz={'h4'}>Membentuk karakter anak yang berakhlak dan berwawasan lingkungan</ListItem> <Stack gap="sm">
<ListItem fz={'h4'}>Mendukung tumbuh kembang anak melalui pendekatan pendidikan yang holistik</ListItem> <Group gap="sm">
</List> <IconTargetArrow size={28} color={colors['blue-button']} />
</Paper> <Title order={2} fw="bold" c={colors['blue-button']}>
</Box> Tujuan Program
<Box> </Title>
<Paper h={{base: 0, md: 239}} p={'xl'} radius={'md'} bg={colors['white-trans-1']}> </Group>
<Title order={2} fw={'bold'} c={colors['blue-button']}> <Tooltip label="Detail tujuan program pendidikan anak" position="top-start" withArrow>
Program Unggulan <Text fz="lg" lh={1.6} c="dark" dangerouslySetInnerHTML={{ __html: stateTujuan.findById.data?.deskripsi }} />
</Title> </Tooltip>
<List> </Stack>
<ListItem fz={'h4'}>Bimbingan Belajar Gratis: Untuk siswa kurang mampu</ListItem> </Paper>
<ListItem fz={'h4'}>Gerakan Literasi Desa: Meningkatkan minat baca sejak dini</ListItem>
<ListItem fz={'h4'}>Pelatihan Digital untuk Anak dan Remaja</ListItem> <Paper
<ListItem fz={'h4'}>Beasiswa Anak Berprestasi & Kurang Mampu</ListItem> p="xl"
</List> radius="xl"
</Paper> withBorder
</Box> bg="white"
shadow="md"
style={{ transition: 'transform 0.2s ease', cursor: 'default' }}
>
<Stack gap="sm">
<Group gap="sm">
<IconBook2 size={28} color={colors['blue-button']} />
<Title order={2} fw="bold" c={colors['blue-button']}>
Program Unggulan
</Title>
</Group>
<Tooltip label="Detail program unggulan yang sedang berjalan" position="top-start" withArrow>
<Text fz="lg" lh={1.6} c="dark" dangerouslySetInnerHTML={{ __html: stateUnggulan.findById.data?.deskripsi }} />
</Tooltip>
</Stack>
</Paper>
</SimpleGrid> </SimpleGrid>
</Box> </Box>
</Stack> </Stack>

View File

@@ -5,7 +5,7 @@ import { IconAt, IconBrandFacebook, IconBrandInstagram, IconBrandTwitter, IconBr
function Footer() { function Footer() {
return ( return (
<Stack bg="linear-gradient(180deg, #1C6EA4, #124170)" c="white"> <Stack bg="linear-gradient(180deg, #1C6EA4, #124170)" c="white">
<Box w="100%" p="xl" h={{ base: 1800, md: 1100 }}> <Box w="100%" p="xl">
<Center> <Center>
<Paper w="100%" bg="transparent" shadow="md" radius="lg" p="xl"> <Paper w="100%" bg="transparent" shadow="md" radius="lg" p="xl">
<Box component="footer"> <Box component="footer">

View File

@@ -26,8 +26,8 @@ export function Navbar() {
> >
<NavbarMainMenu listNavbar={navbarListMenu} /> <NavbarMainMenu listNavbar={navbarListMenu} />
<Stack hiddenFrom="sm" bg={colors.grey[2]} px="md" py="sm"> <Box hiddenFrom="sm" bg={colors.grey[2]} px="md" py="sm">
<Group justify="space-between"> <Group justify="space-between" wrap="nowrap">
<ActionIcon <ActionIcon
variant="transparent" variant="transparent"
size="xl" size="xl"
@@ -51,16 +51,23 @@ export function Navbar() {
</Tooltip> </Tooltip>
</Group> </Group>
{mobileOpen && ( {mobileOpen && (
<motion.div <Paper
initial={{ x: 300 }} component={motion.div}
initial={{ x: '100%' }}
animate={{ x: 0 }} animate={{ x: 0 }}
exit={{ x: '100%' }}
transition={{ duration: 0.2 }} transition={{ duration: 0.2 }}
style={{ height: "100vh" }} pos="absolute"
left={0}
right={0}
top="100%"
m={0}
radius={0}
> >
<NavbarMobile listNavbar={navbarListMenu} /> <NavbarMobile listNavbar={navbarListMenu} />
</motion.div> </Paper>
)} )}
</Stack> </Box>
</Paper> </Paper>
{(item || isSearch) && <Box className="glass" />} {(item || isSearch) && <Box className="glass" />}
</Box> </Box>
@@ -70,28 +77,34 @@ export function Navbar() {
function NavbarMobile({ listNavbar }: { listNavbar: MenuItem[] }) { function NavbarMobile({ listNavbar }: { listNavbar: MenuItem[] }) {
const router = useRouter(); const router = useRouter();
return ( return (
<ScrollArea h="100vh" offsetScrollbars> <ScrollArea.Autosize mah="calc(100vh - 80px)" offsetScrollbars>
<Stack p="lg" gap="md" style={{ backgroundColor: "rgba(255, 255, 255, 0.25)" }}> <Stack p="md" gap="xs">
{listNavbar.map((item, k) => ( {listNavbar.map((item, k) => (
<Stack key={k} gap={4}> <Box key={k}>
<Group <Group
justify="space-between" justify="space-between"
align="center" align="center"
p="xs"
onClick={() => { onClick={() => {
router.push(item.href); router.push(item.href);
stateNav.mobileOpen = false; stateNav.mobileOpen = false;
}} }}
style={{ cursor: "pointer" }} style={{ cursor: "pointer" }}
> >
<Text c="dark.9" fw={600} fz="lg"> <Text c="dark.9" fw={600} fz="md">
{item.name} {item.name}
</Text> </Text>
<IconSquareArrowRight size={20} /> <IconSquareArrowRight size={18} />
</Group> </Group>
{item.children && <NavbarMobile listNavbar={item.children} />} {item.children && (
</Stack> <Box pl="md">
<NavbarMobile listNavbar={item.children} />
</Box>
)}
</Box>
))} ))}
</Stack> </Stack>
</ScrollArea> </ScrollArea.Autosize>
); );
} }

View File

@@ -2,7 +2,8 @@ import colors from '@/con/colors';
import { Box, Button, Center, Flex, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Center, Flex, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconUserFilled } from '@tabler/icons-react'; import { IconUserFilled } from '@tabler/icons-react';
import Link from 'next/link'; import Link from 'next/link';
import BackButton from '../../../desa/layanan/_com/BackButto'; import BackButton from '../(pages)/desa/layanan/_com/BackButto';
function Page() { function Page() {
return ( return (

View File

@@ -1,7 +1,8 @@
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Checkbox, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Center, Checkbox, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import Link from 'next/link'; import Link from 'next/link';
import BackButton from '../../../desa/layanan/_com/BackButto'; import BackButton from '../(pages)/desa/layanan/_com/BackButto';
function Page() { function Page() {
return ( return (

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