Compare commits

...

2 Commits

73 changed files with 2526 additions and 731 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,10 +333,11 @@ 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,
@@ -363,13 +364,18 @@ const lembagaPendidikan = proxy({
console.log('API Response:', res); console.log('API Response:', res);
if (res.status === 200 && res.data?.success) { if (res.status === 200 && res.data?.success) {
lembagaPendidikan.findMany.data = Array.isArray(res.data.data) ? res.data.data : []; const data = Array.isArray(res.data.data) ? res.data.data : [];
lembagaPendidikan.findMany.total = typeof res.data.total === 'number' ? res.data.total : 0; const total = typeof res.data.total === 'number' ? res.data.total : 0;
lembagaPendidikan.findMany.totalPages = typeof res.data.totalPages === 'number' ? 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:', { console.log('Successfully loaded lembaga data:', {
count: lembagaPendidikan.findMany.data.length, count: data.length,
total: lembagaPendidikan.findMany.total, total,
totalPages: lembagaPendidikan.findMany.totalPages totalPages
}); });
} else { } else {
console.error( console.error(

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,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

@@ -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

@@ -46,13 +46,11 @@ async function lembagaPendidikanFindMany(context: Context) {
} }
} }
// Tambahkan pencarian (jika ada) // Add search functionality
if (search) { if (search) {
where.OR = [ where.OR = [
{ nama: { contains: search, mode: "insensitive" } }, { nama: { contains: search, mode: 'insensitive' } },
{ siswa: { nama: { contains: search, mode: "insensitive" } } }, { jenjangPendidikan: { nama: { contains: search, mode: 'insensitive' } } },
{ pengajar: { nama: { contains: search, mode: "insensitive" } } },
{ jenjangPendidikan: { nama: { contains: search, mode: "insensitive" } } },
]; ];
} }
@@ -61,12 +59,10 @@ async function lembagaPendidikanFindMany(context: Context) {
where, where,
include: { include: {
jenjangPendidikan: true, jenjangPendidikan: true,
siswa: true,
pengajar: true,
}, },
skip, skip,
take: limit, take: limit,
orderBy: { jenjangPendidikan: { nama: 'asc' } }, orderBy: { nama: 'asc' },
}), }),
prisma.lembaga.count({ prisma.lembaga.count({
where, where,

View File

@@ -67,7 +67,7 @@ async function pengajarFindMany(context: Context) {
}, },
skip, skip,
take: limit, take: limit,
orderBy: { lembaga: { jenjangPendidikan: { nama: 'asc' } } }, orderBy: { nama: 'asc' },
}), }),
prisma.pengajar.count({ prisma.pengajar.count({
where, where,

View File

@@ -47,10 +47,10 @@ async function siswaFindMany(context: Context) {
} }
} }
// Tambahkan pencarian (jika ada) // Add search functionality
if (search) { if (search) {
where.OR = [ where.OR = [
{ nama: { contains: search, mode: "insensitive" } }, { nama: { contains: search, mode: 'insensitive' } },
{ lembaga: { nama: { contains: search, mode: 'insensitive' } } } { lembaga: { nama: { contains: search, mode: 'insensitive' } } }
]; ];
} }
@@ -67,7 +67,7 @@ async function siswaFindMany(context: Context) {
}, },
skip, skip,
take: limit, take: limit,
orderBy: { lembaga: { jenjangPendidikan: { nama: 'asc' } } }, orderBy: { nama: 'asc' },
}), }),
prisma.siswa.count({ prisma.siswa.count({
where, where,

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

@@ -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

@@ -42,7 +42,7 @@ function Page() {
Program Bimbingan Belajar Desa Program Bimbingan Belajar Desa
</Title> </Title>
<Divider size="sm" my="md" mx="auto" w="60%" color={colors['blue-button']} /> <Divider size="sm" my="md" mx="auto" w="60%" color={colors['blue-button']} />
<Text ta="center" fz="lg" c="dimmed" px={{ base: 'sm', md: 120 }}> <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. Program unggulan untuk mendukung siswa Desa Darmasaba memahami pelajaran sekolah, meningkatkan prestasi akademik, dan menumbuhkan semangat belajar sejak dini.
</Text> </Text>
</Box> </Box>

View File

@@ -239,9 +239,9 @@ export default function KategoriPage({ jenjangPendidikan }: { jenjangPendidikan:
paddingRight: 20, paddingRight: 20,
}} }}
onClick={() => { onClick={() => {
if (v.nama === "Lembaga Pendidikan") router.push(`/darmasaba/pendidikan/info-sekolah-paud/${jenjangPendidikan}/lembaga`); if (v.nama === "Lembaga Pendidikan") router.push(`/darmasaba/pendidikan/info-sekolah/${jenjangPendidikan}/lembaga`);
if (v.nama === "Siswa Terdaftar") router.push(`/darmasaba/pendidikan/info-sekolah-paud/${jenjangPendidikan}/siswa`); if (v.nama === "Siswa Terdaftar") router.push(`/darmasaba/pendidikan/info-sekolah/${jenjangPendidikan}/siswa`);
if (v.nama === "Tenaga Pengajar") router.push(`/darmasaba/pendidikan/info-sekolah-paud/${jenjangPendidikan}/pengajar`); if (v.nama === "Tenaga Pengajar") router.push(`/darmasaba/pendidikan/info-sekolah/${jenjangPendidikan}/pengajar`);
}} }}
> >
Lihat Detail Lihat Detail

View File

@@ -1,11 +1,11 @@
'use client' 'use client'
import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud'; import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import { Box, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, TextInput, Title } from '@mantine/core';
import { IconSchool, IconLayersSubtract } from '@tabler/icons-react'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { useShallowEffect } from '@mantine/hooks'; import { IconChalkboard, IconLayersSubtract, IconSearch } from '@tabler/icons-react';
import { use, useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { use } from 'react';
interface PageProps { interface PageProps {
@@ -13,6 +13,8 @@ interface PageProps {
} }
function Page({ params }: PageProps) { function Page({ params }: PageProps) {
const [search, setSearch] = useState("")
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const stateList = useProxy(infoSekolahPaud.lembagaPendidikan) const stateList = useProxy(infoSekolahPaud.lembagaPendidikan)
const { jenjangPendidikan } = use(params); const { jenjangPendidikan } = use(params);
@@ -27,8 +29,8 @@ function Page({ params }: PageProps) {
useShallowEffect(() => { useShallowEffect(() => {
// Decode the URL parameter and pass it to load // Decode the URL parameter and pass it to load
const decodedJenjang = decodeURIComponent(jenjangPendidikan).toLowerCase() const decodedJenjang = decodeURIComponent(jenjangPendidikan).toLowerCase()
load(page, 10, '', decodedJenjang === 'semua' ? '' : decodedJenjang) load(page, 10, debouncedSearch, decodedJenjang === 'semua' ? '' : decodedJenjang)
}, [page, jenjangPendidikan]) }, [page, jenjangPendidikan, debouncedSearch])
const filteredData = data || [] const filteredData = data || []
@@ -52,9 +54,15 @@ function Page({ params }: PageProps) {
> >
<Group justify="space-between" align="center" mb="md"> <Group justify="space-between" align="center" mb="md">
<Group gap="sm"> <Group gap="sm">
<IconSchool size={28} stroke={1.5} color={colors['blue-button']} /> <IconChalkboard size={28} stroke={1.5} color={colors['blue-button']} />
<Title order={2} fz="xl">Daftar Lembaga Pendidikan</Title> <Title order={2} fz="xl">Daftar Lembaga Pendidikan</Title>
</Group> </Group>
<TextInput
placeholder='pencarian'
leftSection={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
</Group> </Group>
{filteredData.length === 0 ? ( {filteredData.length === 0 ? (
@@ -74,8 +82,8 @@ function Page({ params }: PageProps) {
> >
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh w="60%">Nama Lembaga</TableTh> <TableTh w="50%">Nama Lembaga</TableTh>
<TableTh w="40%">Jenjang Pendidikan</TableTh> <TableTh w="50%">Jenjang Pendidikan</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>

View File

@@ -1,11 +1,11 @@
'use client' 'use client'
import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud'; import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import { Box, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, TextInput, Title } from '@mantine/core';
import { IconSchool, IconLayersSubtract } from '@tabler/icons-react'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { useShallowEffect } from '@mantine/hooks'; import { IconLayersSubtract, IconMicroscope, IconSearch } from '@tabler/icons-react';
import { use, useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { use } from 'react';
interface PageProps { interface PageProps {
@@ -13,6 +13,8 @@ interface PageProps {
} }
function Page({ params }: PageProps) { function Page({ params }: PageProps) {
const [search, setSearch] = useState("")
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const stateList = useProxy(infoSekolahPaud.pengajar) const stateList = useProxy(infoSekolahPaud.pengajar)
const { jenjangPendidikan } = use(params); const { jenjangPendidikan } = use(params);
@@ -27,8 +29,8 @@ function Page({ params }: PageProps) {
useShallowEffect(() => { useShallowEffect(() => {
// Decode the URL parameter and pass it to load // Decode the URL parameter and pass it to load
const decodedJenjang = decodeURIComponent(jenjangPendidikan).toLowerCase() const decodedJenjang = decodeURIComponent(jenjangPendidikan).toLowerCase()
load(page, 10, '', decodedJenjang === 'semua' ? '' : decodedJenjang) load(page, 10, debouncedSearch, decodedJenjang === 'semua' ? '' : decodedJenjang)
}, [page, jenjangPendidikan]) }, [page, jenjangPendidikan, debouncedSearch])
const filteredData = data || [] const filteredData = data || []
@@ -52,9 +54,15 @@ function Page({ params }: PageProps) {
> >
<Group justify="space-between" align="center" mb="md"> <Group justify="space-between" align="center" mb="md">
<Group gap="sm"> <Group gap="sm">
<IconSchool size={28} stroke={1.5} color={colors['blue-button']} /> <IconMicroscope size={28} stroke={1.5} color={colors['blue-button']} />
<Title order={2} fz="xl">Daftar Pengajar</Title> <Title order={2} fz="xl">Daftar Pengajar</Title>
</Group> </Group>
<TextInput
placeholder='pencarian'
leftSection={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
</Group> </Group>
{filteredData.length === 0 ? ( {filteredData.length === 0 ? (
@@ -75,7 +83,7 @@ function Page({ params }: PageProps) {
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh w="30%">Nama Pengajar</TableTh> <TableTh w="30%">Nama Pengajar</TableTh>
<TableTh w="60%">Nama Lembaga</TableTh> <TableTh w="30%">Nama Lembaga</TableTh>
<TableTh w="40%">Jenjang Pendidikan</TableTh> <TableTh w="40%">Jenjang Pendidikan</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>

View File

@@ -1,11 +1,11 @@
'use client' 'use client'
import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud'; import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import { Box, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, TextInput, Title } from '@mantine/core';
import { IconSchool, IconLayersSubtract } from '@tabler/icons-react'; import { IconSchool, IconLayersSubtract, IconSearch } from '@tabler/icons-react';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { use } from 'react'; import { use, useState } from 'react';
interface PageProps { interface PageProps {
@@ -13,6 +13,8 @@ interface PageProps {
} }
function Page({ params }: PageProps) { function Page({ params }: PageProps) {
const [search, setSearch] = useState("")
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const stateList = useProxy(infoSekolahPaud.siswa) const stateList = useProxy(infoSekolahPaud.siswa)
const { jenjangPendidikan } = use(params); const { jenjangPendidikan } = use(params);
@@ -27,8 +29,8 @@ function Page({ params }: PageProps) {
useShallowEffect(() => { useShallowEffect(() => {
// Decode the URL parameter and pass it to load // Decode the URL parameter and pass it to load
const decodedJenjang = decodeURIComponent(jenjangPendidikan).toLowerCase() const decodedJenjang = decodeURIComponent(jenjangPendidikan).toLowerCase()
load(page, 10, '', decodedJenjang === 'semua' ? '' : decodedJenjang) load(page, 10, debouncedSearch, decodedJenjang === 'semua' ? '' : decodedJenjang)
}, [page, jenjangPendidikan]) }, [page, jenjangPendidikan, debouncedSearch])
const filteredData = data || [] const filteredData = data || []
@@ -55,6 +57,12 @@ function Page({ params }: PageProps) {
<IconSchool size={28} stroke={1.5} color={colors['blue-button']} /> <IconSchool size={28} stroke={1.5} color={colors['blue-button']} />
<Title order={2} fz="xl">Daftar Siswa</Title> <Title order={2} fz="xl">Daftar Siswa</Title>
</Group> </Group>
<TextInput
placeholder='pencarian'
leftSection={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
</Group> </Group>
{filteredData.length === 0 ? ( {filteredData.length === 0 ? (
@@ -75,7 +83,7 @@ function Page({ params }: PageProps) {
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh w="30%">Nama Siswa</TableTh> <TableTh w="30%">Nama Siswa</TableTh>
<TableTh w="60%">Nama Lembaga</TableTh> <TableTh w="30%">Nama Lembaga</TableTh>
<TableTh w="40%">Jenjang Pendidikan</TableTh> <TableTh w="40%">Jenjang Pendidikan</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>

View File

@@ -9,12 +9,11 @@ import {
Paper, Paper,
Stack, Stack,
Text, Text,
TextInput, VisuallyHidden
VisuallyHidden,
} from '@mantine/core'; } from '@mantine/core';
import { IconArrowLeft, IconSearch } from '@tabler/icons-react'; import { IconArrowLeft } from '@tabler/icons-react';
import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import { useRouter, useSearchParams } from 'next/navigation';
import React, { useState, useEffect } from 'react'; import React, { useState } from 'react';
type LayoutSekolahProps = { type LayoutSekolahProps = {
title?: string; title?: string;
@@ -29,48 +28,22 @@ export default function LayoutSekolah({
}: LayoutSekolahProps) { }: LayoutSekolahProps) {
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const pathname = usePathname();
const initialQuery = searchParams.get('search') || '';
const initialJenjangPendidikan = searchParams.get('jenjangPendidikan') || 'Semua'; const initialJenjangPendidikan = searchParams.get('jenjangPendidikan') || 'Semua';
const [query, setQuery] = useState(initialQuery);
const [jenjangPendidikanAktif, setJenjangPendidikanAktif] = useState(initialJenjangPendidikan); const [jenjangPendidikanAktif, setJenjangPendidikanAktif] = useState(initialJenjangPendidikan);
const [searchTimeout, setSearchTimeout] = useState<number | null>(null);
// Cleanup timeout // Cleanup timeout
useEffect(() => {
return () => {
if (searchTimeout) clearTimeout(searchTimeout);
};
}, [searchTimeout]);
// Handle Search with debounce
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const val = e.target.value;
setQuery(val);
if (searchTimeout) clearTimeout(searchTimeout);
const t = window.setTimeout(() => {
const params = new URLSearchParams(searchParams.toString());
if (val) params.set('search', val);
else params.delete('search');
params.set('jenjangPendidikan', jenjangPendidikanAktif);
router.push(`${pathname}?${params.toString()}`);
}, 500);
setSearchTimeout(t);
};
// Handle jenjang pendidikan click // Handle jenjang pendidikan click
const handleJenjangPendidikanChange = (k: string) => { const handleJenjangPendidikanChange = (k: string) => {
// arahkan langsung ke route jenjang pendidikan // arahkan langsung ke route jenjang pendidikan
if (k.toLowerCase() === 'semua') { if (k.toLowerCase() === 'semua') {
setJenjangPendidikanAktif(k); setJenjangPendidikanAktif(k);
router.push(`/darmasaba/pendidikan/info-sekolah-paud/semua`); router.push(`/darmasaba/pendidikan/info-sekolah/semua`);
} else { } else {
setJenjangPendidikanAktif(k); setJenjangPendidikanAktif(k);
router.push(`/darmasaba/pendidikan/info-sekolah-paud/${encodeURIComponent(k.toLowerCase())}`); router.push(`/darmasaba/pendidikan/info-sekolah/${encodeURIComponent(k.toLowerCase())}`);
} }
}; };
@@ -90,15 +63,9 @@ export default function LayoutSekolah({
<Stack gap="md"> <Stack gap="md">
<Text ta="center" fw={800} fz={28}>{title}</Text> <Text ta="center" fw={800} fz={28}>{title}</Text>
<TextInput <Text ta="center" fz={"md"} c="black">
value={query} 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.
onChange={handleSearchChange} </Text>
placeholder="Cari sekolah..."
leftSection={<IconSearch size={18} />}
radius="xl"
size="md"
/>
<Group justify="center" gap="xs" wrap="wrap"> <Group justify="center" gap="xs" wrap="wrap">
{jenjangPendidikanList.map((k) => { {jenjangPendidikanList.map((k) => {
const aktif = k === jenjangPendidikanAktif; const aktif = k === jenjangPendidikanAktif;

View File

@@ -1,12 +1,15 @@
'use client' 'use client'
import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud'; import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import { Box, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, TextInput, Title } from '@mantine/core';
import { IconSchool, IconLayersSubtract } from '@tabler/icons-react'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { useShallowEffect } from '@mantine/hooks'; import { IconChalkboard, IconLayersSubtract, IconSearch } from '@tabler/icons-react';
import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function Page() { function Page() {
const [search, setSearch] = useState("")
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const stateList = useProxy(infoSekolahPaud.lembagaPendidikan) const stateList = useProxy(infoSekolahPaud.lembagaPendidikan)
const { const {
@@ -18,8 +21,8 @@ function Page() {
} = stateList.findMany } = stateList.findMany
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10) load(page, 10, debouncedSearch)
}, [page]) }, [page, debouncedSearch])
const filteredData = data || [] const filteredData = data || []
@@ -43,9 +46,15 @@ function Page() {
> >
<Group justify="space-between" align="center" mb="md"> <Group justify="space-between" align="center" mb="md">
<Group gap="sm"> <Group gap="sm">
<IconSchool size={28} stroke={1.5} color={colors['blue-button']} /> <IconChalkboard size={28} stroke={1.5} color={colors['blue-button']} />
<Title order={2} fz="xl">Daftar Lembaga Pendidikan</Title> <Title order={2} fz="xl">Daftar Lembaga Pendidikan</Title>
</Group> </Group>
<TextInput
placeholder='pencarian'
leftSection={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
</Group> </Group>
{filteredData.length === 0 ? ( {filteredData.length === 0 ? (

View File

@@ -250,9 +250,9 @@ export default function SekolahPage() {
paddingRight: 20, paddingRight: 20,
}} }}
onClick={() => { onClick={() => {
if (v.nama === "Lembaga Pendidikan") router.push(`/darmasaba/pendidikan/info-sekolah-paud/semua/lembaga`); if (v.nama === "Lembaga Pendidikan") router.push(`/darmasaba/pendidikan/info-sekolah/semua/lembaga`);
if (v.nama === "Siswa Terdaftar") router.push(`/darmasaba/pendidikan/info-sekolah-paud/semua/siswa`); if (v.nama === "Siswa Terdaftar") router.push(`/darmasaba/pendidikan/info-sekolah/semua/siswa`);
if (v.nama === "Tenaga Pengajar") router.push(`/darmasaba/pendidikan/info-sekolah-paud/semua/pengajar`); if (v.nama === "Tenaga Pengajar") router.push(`/darmasaba/pendidikan/info-sekolah/semua/pengajar`);
}} }}
> >
Lihat Detail Lihat Detail

View File

@@ -1,14 +1,16 @@
'use client' 'use client'
import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud'; import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import { Box, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, TextInput, Title } from '@mantine/core';
import { IconSchool, IconLayersSubtract } from '@tabler/icons-react'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { useShallowEffect } from '@mantine/hooks'; import { IconLayersSubtract, IconMicroscope, IconSearch } from '@tabler/icons-react';
import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function Page() { function Page() {
const stateList = useProxy(infoSekolahPaud.pengajar) const stateList = useProxy(infoSekolahPaud.pengajar)
const [search, setSearch] = useState("")
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const { const {
data, data,
page, page,
@@ -18,8 +20,8 @@ function Page() {
} = stateList.findMany } = stateList.findMany
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10) load(page, 10, debouncedSearch)
}, [page]) }, [page, debouncedSearch])
const filteredData = data || [] const filteredData = data || []
@@ -43,9 +45,15 @@ function Page() {
> >
<Group justify="space-between" align="center" mb="md"> <Group justify="space-between" align="center" mb="md">
<Group gap="sm"> <Group gap="sm">
<IconSchool size={28} stroke={1.5} color={colors['blue-button']} /> <IconMicroscope size={28} stroke={1.5} color={colors['blue-button']} />
<Title order={2} fz="xl">Daftar Pengajar</Title> <Title order={2} fz="xl">Daftar Pengajar</Title>
</Group> </Group>
<TextInput
placeholder='pencarian'
leftSection={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
</Group> </Group>
{filteredData.length === 0 ? ( {filteredData.length === 0 ? (

View File

@@ -1,12 +1,15 @@
'use client' 'use client'
import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud'; import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import { Box, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, TextInput, Title } from '@mantine/core';
import { IconSchool, IconLayersSubtract } from '@tabler/icons-react'; import { IconSchool, IconLayersSubtract, IconSearch } from '@tabler/icons-react';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect, useDebouncedValue } from '@mantine/hooks';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { useState } from 'react';
function Page() { function Page() {
const [search, setSearch] = useState("")
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const stateList = useProxy(infoSekolahPaud.siswa) const stateList = useProxy(infoSekolahPaud.siswa)
const { const {
@@ -18,8 +21,8 @@ function Page() {
} = stateList.findMany } = stateList.findMany
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10) load(page, 10, debouncedSearch)
}, [page]) }, [page, debouncedSearch])
const filteredData = data || [] const filteredData = data || []
@@ -46,6 +49,12 @@ function Page() {
<IconSchool size={28} stroke={1.5} color={colors['blue-button']} /> <IconSchool size={28} stroke={1.5} color={colors['blue-button']} />
<Title order={2} fz="xl">Daftar Siswa</Title> <Title order={2} fz="xl">Daftar Siswa</Title>
</Group> </Group>
<TextInput
placeholder='pencarian'
leftSection={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
</Group> </Group>
{filteredData.length === 0 ? ( {filteredData.length === 0 ? (

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

@@ -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 (

View File

@@ -302,12 +302,12 @@ const navbarListMenu = [
}, { }, {
id: "8", id: "8",
name: "Pendidikan", name: "Pendidikan",
href: "/darmasaba/pendidikan/info-sekolah-paud", href: "/darmasaba/pendidikan/info-sekolah",
children: [ children: [
{ {
id: "8.1", id: "8.1",
name: "Info Sekolah & PAUD", name: "Info Sekolah",
href: "/darmasaba/pendidikan/info-sekolah-paud/semua" href: "/darmasaba/pendidikan/info-sekolah/semua"
}, },
{ {
id: "8.2", id: "8.2",
@@ -332,7 +332,7 @@ const navbarListMenu = [
{ {
id: "8.6", id: "8.6",
name: "Perpustakaan Digital", name: "Perpustakaan Digital",
href: "/darmasaba/pendidikan/perpustakaan-digital" href: "/darmasaba/pendidikan/perpustakaan-digital/semua"
}, },
{ {
id: "8.7", id: "8.7",