- QC User & Admin Menu Pendidikan V

- Fix SubMenu :
- Beasiswa Desa ( Baca Selengkapnya terdapatkan konten ) V
- Info Sekolah ( Kategori Menyesuaikan Dengan Datanya ) V
- Perpustakaan Digital (  V
- Kategori Menyesuaikan Dengan Datanya V
- Saat Mau minjam muncul modal data diri peminjam buku V
- Ada Status Peminjamannya V
)
This commit is contained in:
2025-10-13 11:20:38 +08:00
parent 80c5dc6361
commit a158241c0b
21 changed files with 2040 additions and 202 deletions

View File

@@ -1,5 +1,101 @@
/* eslint-disable react-hooks/exhaustive-deps */
// 'use client'
// import colors from '@/con/colors';
// import {
// ActionIcon,
// Box,
// Button,
// Container,
// Group,
// Paper,
// Stack,
// Text,
// VisuallyHidden
// } from '@mantine/core';
// import { IconArrowLeft } from '@tabler/icons-react';
// import { useRouter, useSearchParams } from 'next/navigation';
// import React, { useState } from 'react';
// type LayoutSekolahProps = {
// title?: string;
// jenjangPendidikanList?: string[];
// children: React.ReactNode;
// };
// export default function LayoutSekolah({
// title = 'Cari Informasi Sekolah',
// jenjangPendidikanList = ['Semua', 'TK', 'SD', 'SMP', 'SMA'],
// children,
// }: LayoutSekolahProps) {
// const router = useRouter();
// const searchParams = useSearchParams();
// const initialJenjangPendidikan = searchParams.get('jenjangPendidikan') || 'Semua';
// const [jenjangPendidikanAktif, setJenjangPendidikanAktif] = useState(initialJenjangPendidikan);
// // Cleanup timeout
// // Handle jenjang pendidikan click
// const handleJenjangPendidikanChange = (k: string) => {
// // arahkan langsung ke route jenjang pendidikan
// if (k.toLowerCase() === 'semua') {
// setJenjangPendidikanAktif(k);
// router.push(`/darmasaba/pendidikan/info-sekolah/semua`);
// } else {
// setJenjangPendidikanAktif(k);
// router.push(`/darmasaba/pendidikan/info-sekolah/${encodeURIComponent(k.toLowerCase())}`);
// }
// };
// return (
// <Box style={{ minHeight: '100vh', background: colors.Bg, paddingBottom: 48 }}>
// <Container size="xl" py={{ base: 'md', md: 'xl' }}>
// <Stack gap="lg">
// {/* Back Button */}
// <ActionIcon onClick={() => window.history.back()} variant="light" radius="md" size="lg">
// <IconArrowLeft size={20} />
// <VisuallyHidden>Kembali</VisuallyHidden>
// </ActionIcon>
// {/* Search & Filter */}
// <Paper radius="lg" p="xl" withBorder>
// <Stack gap="md">
// <Text ta="center" fw={800} fz={28}>{title}</Text>
// <Text ta="center" fz={"md"} c="black">
// Temukan data lengkap mengenai lembaga pendidikan, jumlah siswa terdaftar, dan tenaga pengajar berdasarkan jenjang pendidikan yang tersedia (TK, SD, SMP, SMA). Gunakan tombol di bawah untuk melihat detail sesuai kebutuhanmu.
// </Text>
// <Group justify="center" gap="xs" wrap="wrap">
// {jenjangPendidikanList.map((k) => {
// const aktif = k === jenjangPendidikanAktif;
// return (
// <Button
// key={k}
// onClick={() => handleJenjangPendidikanChange(k)}
// radius="xl"
// size="sm"
// variant={aktif ? 'filled' : 'light'}
// >
// {k}
// </Button>
// );
// })}
// </Group>
// </Stack>
// </Paper>
// {/* Slot konten */}
// {children}
// </Stack>
// </Container>
// </Box>
// );
// }
'use client'
import colors from '@/con/colors';
// pastikan path benar
import {
ActionIcon,
Box,
@@ -9,44 +105,48 @@ import {
Paper,
Stack,
Text,
VisuallyHidden
VisuallyHidden,
Loader,
} from '@mantine/core';
import { IconArrowLeft } from '@tabler/icons-react';
import { useRouter, useSearchParams } from 'next/navigation';
import React, { useState } from 'react';
import { useSnapshot } from 'valtio';
import React, { useEffect, useState } from 'react';
import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud';
type LayoutSekolahProps = {
title?: string;
jenjangPendidikanList?: string[];
children: React.ReactNode;
};
export default function LayoutSekolah({
title = 'Cari Informasi Sekolah',
jenjangPendidikanList = ['Semua', 'TK', 'SD', 'SMP', 'SMA'],
children,
}: LayoutSekolahProps) {
const router = useRouter();
const searchParams = useSearchParams();
const initialJenjangPendidikan = searchParams.get('jenjangPendidikan') || 'Semua';
const snap = useSnapshot(infoSekolahPaud.jenjangPendidikan);
const [jenjangPendidikanAktif, setJenjangPendidikanAktif] = useState(initialJenjangPendidikan);
const [jenjangPendidikanAktif, setJenjangPendidikanAktif] = useState<string>(
searchParams.get('jenjangPendidikan') || 'Semua'
);
// Cleanup timeout
// Load jenjang pendidikan dari backend
useEffect(() => {
if (!snap.findMany.data) infoSekolahPaud.jenjangPendidikan.findMany.load(1, 100);
}, []);
// Handle jenjang pendidikan click
const handleJenjangPendidikanChange = (k: string) => {
// arahkan langsung ke route jenjang pendidikan
if (k.toLowerCase() === 'semua') {
setJenjangPendidikanAktif(k);
router.push(`/darmasaba/pendidikan/info-sekolah/semua`);
} else {
setJenjangPendidikanAktif(k);
router.push(`/darmasaba/pendidikan/info-sekolah/${encodeURIComponent(k.toLowerCase())}`);
}
const handleJenjangPendidikanChange = (nama: string) => {
setJenjangPendidikanAktif(nama);
const path =
nama.toLowerCase() === 'semua'
? `/darmasaba/pendidikan/info-sekolah/semua`
: `/darmasaba/pendidikan/info-sekolah/${encodeURIComponent(nama.toLowerCase())}`;
router.push(path);
};
// List tab dari data state
const jenjangList = ['Semua', ...(snap.findMany.data?.map((v) => v.nama) || [])];
return (
<Box style={{ minHeight: '100vh', background: colors.Bg, paddingBottom: 48 }}>
@@ -61,31 +161,41 @@ export default function LayoutSekolah({
{/* Search & Filter */}
<Paper radius="lg" p="xl" withBorder>
<Stack gap="md">
<Text ta="center" fw={800} fz={28}>{title}</Text>
<Text ta="center" fz={"md"} c="black">
Temukan data lengkap mengenai lembaga pendidikan, jumlah siswa terdaftar, dan tenaga pengajar berdasarkan jenjang pendidikan yang tersedia (TK, SD, SMP, SMA). Gunakan tombol di bawah untuk melihat detail sesuai kebutuhanmu.
<Text ta="center" fw={800} fz={28}>
{title}
</Text>
<Group justify="center" gap="xs" wrap="wrap">
{jenjangPendidikanList.map((k) => {
const aktif = k === jenjangPendidikanAktif;
return (
<Button
key={k}
onClick={() => handleJenjangPendidikanChange(k)}
radius="xl"
size="sm"
variant={aktif ? 'filled' : 'light'}
>
{k}
</Button>
);
})}
</Group>
<Text ta="center" fz="md" c="black">
Temukan data lengkap mengenai lembaga pendidikan, jumlah siswa terdaftar, dan tenaga
pengajar berdasarkan jenjang pendidikan (TK, SD, SMP, SMA).
</Text>
{snap.findMany.loading ? (
<Group justify="center" py="sm">
<Loader size="sm" />
</Group>
) : (
<Group justify="center" gap="xs" wrap="wrap">
{jenjangList.map((k) => {
const aktif = k === jenjangPendidikanAktif;
return (
<Button
key={k}
onClick={() => handleJenjangPendidikanChange(k)}
radius="xl"
size="sm"
variant={aktif ? 'filled' : 'light'}
>
{k}
</Button>
);
})}
</Group>
)}
</Stack>
</Paper>
{/* Slot konten */}
{/* Konten anak */}
{children}
</Stack>
</Container>

View File

@@ -0,0 +1,126 @@
'use client';
import perpustakaanDigitalState from '@/app/admin/(dashboard)/_state/pendidikan/perpustakaan-digital';
import colors from '@/con/colors';
import {
Badge,
Box,
Button,
Center,
Image,
Loader,
Paper,
Stack,
Text,
Title,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowLeft, IconBook2 } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import ModalPeminjaman from '../../_lib/modalPeminjaman';
export default function DetailBukuUser() {
const router = useRouter();
const params = useParams();
const stateDetail = useProxy(perpustakaanDigitalState.dataPerpustakaan);
const [opened, setOpened] = useState(false);
useShallowEffect(() => {
if (params?.id) stateDetail.findUnique.load(params.id as string);
}, [params?.id]);
const data = stateDetail.findUnique.data;
if (!data) {
return (
<Center h="70vh">
<Loader color={colors['blue-button']} size="lg" />
</Center>
);
}
return (
<Box bg={colors.Bg} py="xl" px={{ base: 'md', md: 100 }}>
{/* Tombol Kembali */}
<Button
variant="subtle"
leftSection={<IconArrowLeft size={20} color={colors['blue-button']} />}
onClick={() => router.back()}
mb="lg"
>
Kembali
</Button>
<Paper
withBorder
shadow="md"
radius="xl"
bg="white"
p={{ base: 'md', md: 'xl' }}
maw={800}
mx="auto"
>
<Stack gap="lg" align="center">
{/* Cover Buku */}
<Image
src={data.image?.link || '/placeholder-book.jpg'}
alt={data.judul}
w={220}
h={300}
fit="contain"
radius="md"
loading="lazy"
/>
{/* Judul & Kategori */}
<Stack gap={4} align="center">
<Title order={2} ta="center" c={colors['blue-button']}>
{data.judul}
</Title>
{data.kategori?.name && (
<Badge color="cyan" variant="light" size="md">
{data.kategori.name}
</Badge>
)}
</Stack>
{/* Deskripsi Buku */}
<Paper bg="#F9FAFF" p="md" radius="md" shadow="xs" w="100%">
<Text fw={600} mb={6} c={colors['blue-button']}>
Deskripsi Buku
</Text>
<Text
fz="sm"
lh={1.6}
ta="justify"
c="dimmed"
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
/>
</Paper>
{/* Tombol Pinjam */}
<Button
size="md"
radius="xl"
color="blue"
mt="sm"
leftSection={<IconBook2 size={20} />}
onClick={() => setOpened(true)}
>
Pinjam Buku Ini
</Button>
</Stack>
</Paper>
{/* Modal Peminjaman */}
<ModalPeminjaman
opened={opened}
onClose={() => setOpened(false)}
buku={data}
/>
</Box>
);
}

View File

@@ -1,42 +1,52 @@
'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 { ActionIcon, Badge, Box, Button, Center, Group, Image, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconBook2, IconRefresh } from '@tabler/icons-react';
import { motion } from 'framer-motion';
import { useTransitionRouter } from 'next-view-transitions';
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 [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const searchParams = useSearchParams();
const searchQuery = searchParams.get('search') || '';
const router = useTransitionRouter()
const decodedKategoriBuku = decodeURIComponent(kategoriBuku);
const kategoriFilter = decodedKategoriBuku.toLowerCase() === 'semua' ? '' : decodedKategoriBuku;
const loadData = useCallback(async (searchQuery: string = '') => {
const loadData = useCallback(async (searchQuery: string = '', page: number = 1) => {
try {
setIsLoading(true);
await state.dataPerpustakaan.findMany.load(1, 100, searchQuery, kategoriFilter);
const currentKategoriFilter = decodedKategoriBuku.toLowerCase() === 'semua' ? '' : decodedKategoriBuku;
await state.dataPerpustakaan.findMany.load(page, 3, searchQuery, currentKategoriFilter);
setCurrentPage(page);
setTotalPages(state.dataPerpustakaan.findMany.totalPages);
} finally {
setIsLoading(false);
}
}, [kategoriFilter, state.dataPerpustakaan.findMany]);
}, [state.dataPerpustakaan.findMany, decodedKategoriBuku]);
useShallowEffect(() => {
loadData(searchQuery);
}, [searchQuery, loadData]);
}, [searchQuery, loadData, kategoriBuku]);
const handleRefresh = () => {
loadData();
};
if (isLoading || !state.dataPerpustakaan.findMany.load || !state.dataPerpustakaan.findMany.data) {
const handlePageChange = (newPage: number) => {
loadData(searchQuery, newPage);
window.scrollTo({ top: 0, behavior: 'smooth' });
};
if ((isLoading && !state.dataPerpustakaan.findMany.data) || !state.dataPerpustakaan.findMany.load) {
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
<Box px={{ base: 'md', md: 100 }} pb={50}>
@@ -57,18 +67,23 @@ function Content({ kategoriBuku }: { kategoriBuku: string }) {
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>
<Text size="sm" c="dimmed">
Halaman {currentPage} dari {totalPages}
</Text>
<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>
</Group>
{!state.dataPerpustakaan.findMany.data || state.dataPerpustakaan.findMany.data.length === 0 ? (
@@ -132,30 +147,23 @@ function Content({ kategoriBuku }: { kategoriBuku: string }) {
</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 }}
style={{wordBreak: "break-word", whiteSpace: "normal"}}
/>
</Spoiler>
<Text
ta="justify"
fz="sm"
lh={1.5}
lineClamp={5}
truncate="end"
c="dimmed"
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
style={{
wordBreak: 'break-word',
whiteSpace: 'normal',
}}
/>
{/* 📗 Tombol Detail */}
<Button variant='light' color='blue' onClick={() => router.push(`/darmasaba/pendidikan/perpustakaan-digital/${v.kategori?.name}/${v.id}`)}>
Lihat Detail
</Button>
</Stack>
</Paper>
</motion.div>
@@ -163,6 +171,15 @@ function Content({ kategoriBuku }: { kategoriBuku: string }) {
</SimpleGrid>
)}
</Box>
<Center mt="lg">
<Pagination
value={currentPage}
onChange={handlePageChange}
total={totalPages}
color="blue"
radius="md"
/>
</Center>
</Stack>
);
}

View File

@@ -1,11 +1,24 @@
'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';
'use client';
import perpustakaanDigitalState from '@/app/admin/(dashboard)/_state/pendidikan/perpustakaan-digital';
import colors from '@/con/colors';
import {
Box,
Grid,
GridCol,
Stack,
Tabs,
TabsList,
TabsTab,
Text,
TextInput
} from '@mantine/core';
import { IconSearch } from '@tabler/icons-react';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useSnapshot } from 'valtio';
import BackButton from '../../../desa/layanan/_com/BackButto';
type LayoutBukuProps = {
placeholder?: string;
@@ -15,7 +28,7 @@ type LayoutBukuProps = {
children?: React.ReactNode;
};
function LayoutTabs({
export default function LayoutTabs({
placeholder = 'Cari buku digital...',
searchIcon = <IconSearch size={20} />,
children,
@@ -23,6 +36,7 @@ function LayoutTabs({
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const snap = useSnapshot(perpustakaanDigitalState);
const activeTab = pathname.split('/').pop() || 'semua';
const initialSearch = searchParams.get('search') || '';
@@ -30,6 +44,11 @@ function LayoutTabs({
const [searchTimeout, setSearchTimeout] = useState<number | null>(null);
const [activeTabState, setActiveTabState] = useState(activeTab);
// 🟦 Ambil kategori buku saat mount
useEffect(() => {
perpustakaanDigitalState.kategoriBuku.findMany.load();
}, []);
useEffect(() => {
setActiveTabState(activeTab);
}, [activeTab]);
@@ -51,7 +70,9 @@ function LayoutTabs({
if (value) params.set('search', value);
router.push(
`/darmasaba/pendidikan/perpustakaan-digital/${activeTab}${params.toString() ? `?${params.toString()}` : ''}`
`/darmasaba/pendidikan/perpustakaan-digital/${activeTab}${
params.toString() ? `?${params.toString()}` : ''
}`
);
};
@@ -63,41 +84,40 @@ function LayoutTabs({
}
};
// 🟩 Tabs dinamis berdasarkan kategori dari state
const kategoriTabs =
snap.kategoriBuku.findMany.data?.map((item) => ({
label: item.name,
value: item.name.toLowerCase().replace(/\s+/g, '-'),
href: `/darmasaba/pendidikan/perpustakaan-digital/${encodeURIComponent(item.name.toLowerCase().replace(/\s+/g, '-'))}`,
})) ?? [];
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' },
...kategoriTabs,
];
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()}` : ''}`);
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">
@@ -116,6 +136,7 @@ function LayoutTabs({
))}
</TabsList>
</GridCol>
<GridCol span={{ base: 12, md: 3 }}>
<TextInput
radius="xl"
@@ -130,11 +151,10 @@ function LayoutTabs({
</GridCol>
</Grid>
</Box>
{children}
</Tabs>
</Box>
</Stack>
);
}
export default LayoutTabs;

View File

@@ -0,0 +1,220 @@
'use client';
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import perpustakaanDigitalState from '@/app/admin/(dashboard)/_state/pendidikan/perpustakaan-digital';
import colors from '@/con/colors';
import {
Badge,
Box,
Button,
Divider,
Group,
Image,
Modal,
Stack,
Text,
TextInput,
} from '@mantine/core';
import { DateInput } from '@mantine/dates';
import {
IconArrowRight,
IconBook2,
IconUser
} from '@tabler/icons-react';
import { useEffect } from 'react';
import { toast } from 'react-toastify';
import { useSnapshot } from 'valtio';
export interface ModalPeminjamanProps {
opened: boolean;
onClose: () => void;
buku: {
id: string;
judul: string;
deskripsi?: string;
image?: { link?: string };
kategori?: { name?: string };
} | null;
}
export default function ModalPeminjaman({
opened,
onClose,
buku,
}: ModalPeminjamanProps) {
const snap = useSnapshot(perpustakaanDigitalState.peminjamanBuku);
// reset form setiap modal dibuka
useEffect(() => {
if (opened && buku) {
perpustakaanDigitalState.peminjamanBuku.create.form = {
...perpustakaanDigitalState.peminjamanBuku.create.form,
bukuId: buku.id,
nama: '',
noTelp: '',
alamat: '',
tanggalPinjam: '',
batasKembali: '',
tanggalKembali: '',
catatan: '',
};
}
}, [opened, buku]);
const handleSubmit = async () => {
if (!buku) return toast.error('Data buku tidak ditemukan');
await perpustakaanDigitalState.peminjamanBuku.create.create();
onClose();
};
return (
<Modal
opened={opened}
onClose={onClose}
centered
size="lg"
radius="xl"
title={<Text fw={700}>Formulir Peminjaman Buku</Text>}
>
{buku ? (
<Stack>
{/* --- Info Buku --- */}
<Group align="flex-start">
<Image
src={buku.image?.link || '/placeholder-book.jpg'}
alt={buku.judul}
w={100}
radius="md"
/>
<Stack gap={4}>
<Group>
<IconBook2 size={18} color={colors['blue-button']} />
<Text fw={700}>{buku.judul}</Text>
</Group>
{buku.kategori?.name && (
<Badge color="cyan" variant="light">
{buku.kategori.name}
</Badge>
)}
<Text fz="sm" c="dimmed" lineClamp={3} dangerouslySetInnerHTML={{ __html: buku.deskripsi || 'Tidak ada deskripsi' }} />
</Stack>
</Group>
<Divider my="sm" />
{/* --- Form Input --- */}
<TextInput
label="Nama Peminjam"
placeholder="Masukkan nama lengkap"
leftSection={<IconUser size={16} />}
value={snap.create.form.nama}
onChange={(e) =>
(perpustakaanDigitalState.peminjamanBuku.create.form.nama = e.currentTarget.value)
}
required
/>
<TextInput
label="No Telp"
placeholder="Masukkan no telp"
leftSection={<IconUser size={16} />}
value={snap.create.form.noTelp}
onChange={(e) =>
(perpustakaanDigitalState.peminjamanBuku.create.form.noTelp = e.currentTarget.value)
}
required
/>
<TextInput
label="Alamat"
placeholder="Masukkan alamat"
leftSection={<IconUser size={16} />}
value={snap.create.form.alamat}
onChange={(e) =>
(perpustakaanDigitalState.peminjamanBuku.create.form.alamat = e.currentTarget.value)
}
required
/>
<DateInput
label="Tanggal Pinjam"
placeholder="Pilih tanggal pinjam"
value={
snap.create.form.tanggalPinjam
? new Date(snap.create.form.tanggalPinjam)
: null
}
onChange={(date) => {
perpustakaanDigitalState.peminjamanBuku.create.form.tanggalPinjam =
date ? new Date(date).toISOString() : '';
}}
required
/>
<Box>
<Text>Catatan</Text>
<CreateEditor
value={snap.create.form.catatan}
onChange={(e) =>
(perpustakaanDigitalState.peminjamanBuku.create.form.catatan = e)
}
/>
</Box>
<DateInput
label="Tanggal Kembali"
placeholder="Pilih tanggal kembali"
value={
snap.create.form.tanggalKembali
? new Date(snap.create.form.tanggalKembali)
: null
}
onChange={(date) => {
perpustakaanDigitalState.peminjamanBuku.create.form.tanggalKembali =
date ? new Date(date).toISOString() : '';
}}
required
/>
<DateInput
label="Batas Pengembalian"
placeholder="Pilih tanggal kembali"
value={
snap.create.form.batasKembali
? new Date(snap.create.form.batasKembali)
: null
}
onChange={(date) => {
perpustakaanDigitalState.peminjamanBuku.create.form.batasKembali =
date ? new Date(date).toISOString() : '';
}}
required
/>
<Button
onClick={handleSubmit}
loading={snap.create.loading}
disabled={
!snap.create.form.nama ||
!snap.create.form.tanggalPinjam ||
!snap.create.form.batasKembali ||
!snap.create.form.tanggalKembali
}
rightSection={<IconArrowRight size={16} />}
radius="xl"
>
Pinjam Buku
</Button>
</Stack>
) : (
<Text c="dimmed" ta="center">
Tidak ada data buku yang dipilih
</Text>
)}
</Modal>
);
}

View File

@@ -1,45 +1,89 @@
'use client'
/* eslint-disable react-hooks/exhaustive-deps */
'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 {
Badge,
Box,
Button,
Center,
Group,
Image,
Paper,
SimpleGrid,
Skeleton,
Stack,
Text,
Tooltip
} from '@mantine/core';
import { IconBook2, IconInfoCircle } from '@tabler/icons-react';
import { Pagination } from '@mantine/core';
import { motion } from 'framer-motion';
type ContentProps = {
searchQuery: string;
};
import { useEffect, useState } from 'react';
import { useSearchParams } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import ModalPeminjaman from '../_lib/modalPeminjaman';
import { useTransitionRouter } from 'next-view-transitions';
function Content({ searchQuery }: ContentProps) {
export default function Content() {
const state = useProxy(perpustakaanDigitalState);
const [expandedId, setExpandedId] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
const router = useTransitionRouter()
const [opened, setOpened] = useState(false);
const searchParams = useSearchParams();
const searchQuery = searchParams.get('search') || '';
const [currentPage, setCurrentPage] = useState(1);
const { data: books = [], loading, totalPages } = state.dataPerpustakaan.findMany;
const loadData = useCallback(
async (query: string = '') => {
const [selectedBook, setSelectedBook] = useState<{
id: string;
judul: string;
deskripsi?: string;
image?: { link?: string };
kategori?: { name?: string };
} | null>(null);
// Handle data loading and search
useEffect(() => {
let isMounted = true;
const controller = new AbortController();
const loadData = async () => {
if (!isMounted) return;
try {
setIsLoading(true);
await state.dataPerpustakaan.findMany.load(1, 100, query, '');
await state.dataPerpustakaan.findMany.load(
currentPage,
10,
searchQuery,
''
);
} catch (error) {
console.error('Gagal memuat data:', error);
} finally {
setIsLoading(false);
if (!controller.signal.aborted) {
console.error('Gagal memuat data:', error);
}
}
},
[state.dataPerpustakaan.findMany]
);
};
useShallowEffect(() => {
loadData(searchQuery);
}, [searchQuery, loadData]);
const timer = setTimeout(loadData, 300);
return () => {
isMounted = false;
controller.abort();
clearTimeout(timer);
};
}, [searchQuery, currentPage]);
if (
isLoading ||
!state.dataPerpustakaan.findMany.load ||
!state.dataPerpustakaan.findMany.data
) {
// Handle page change
const handlePageChange = (newPage: number) => {
setCurrentPage(newPage);
window.scrollTo({ top: 0, behavior: 'smooth' });
};
// Loading state
if (loading || !books) {
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
<Box px={{ base: 'md', md: 100 }} pb={50}>
@@ -53,6 +97,7 @@ function Content({ searchQuery }: ContentProps) {
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
<Box px={{ base: 'md', md: 100 }} pb={20}>
{/* 🔹 Header */}
<Group justify="space-between" mb="lg">
<Group gap="xs">
<IconBook2 size={28} color={colors['blue-button']} />
@@ -65,101 +110,152 @@ function Content({ searchQuery }: ContentProps) {
</Tooltip>
</Group>
{!state.dataPerpustakaan.findMany.data ||
state.dataPerpustakaan.findMany.data.length === 0 ? (
{/* 📚 Empty State */}
{books.length === 0 ? (
<Center py="xl">
<Stack gap="xs" align="center">
<Image loading="lazy" src="/empty-books.svg" alt="Kosong" w={140} h="auto" />
<Text c="dimmed" fz="sm">Belum ada buku yang tersedia</Text>
<Image
loading="lazy"
src="/empty-books.svg"
alt="Kosong"
w={140}
h="auto"
/>
<Text c="dimmed" fz="sm">
Belum ada buku yang tersedia
</Text>
</Stack>
</Center>
) : (
// 📘 Daftar Buku
<SimpleGrid
cols={{ base: 1, sm: 2, md: 3 }}
spacing="lg"
verticalSpacing="lg"
style={{ alignItems: 'stretch' }}
>
{state.dataPerpustakaan.findMany.data?.map((v, k) => (
{books.map((v) => (
<motion.div
key={k}
key={v.id}
whileHover={{ scale: 1.03 }}
transition={{ duration: 0.2 }}
style={{ width: '100%', height: '100%' }}
style={{ height: '100%' }}
>
<Paper
p="lg"
<Paper
p="lg"
radius="2xl"
shadow="md"
bg="white"
style={{
height: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between'
justifyContent: 'space-between',
height: '100%',
minHeight: 440,
}}
>
<Stack gap="md" style={{ flex: 1 }}>
<Stack gap="md" style={{ flexGrow: 1 }}>
{/* 🖼 Gambar Buku */}
<Center>
<Image
src={v.image?.link}
alt={v.judul}
<Image
src={v.image?.link}
alt={v.judul}
h={180}
w="auto"
fit="contain"
fallbackSrc="/placeholder-book.jpg"
radius="md"
fallbackSrc="/placeholder-book.jpg"
loading="lazy"
/>
</Center>
<Stack gap={4} align="center">
<Text
c={colors["blue-button"]}
ta="center"
fw={700}
fz={{ base: "md", md: "lg" }}
{/* 📖 Judul & Kategori */}
<Stack gap={4} align="center" px="sm">
<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">
<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 }}
style={{wordBreak: "break-word", whiteSpace: "normal"}}
/>
</Spoiler>
{/* 📝 Deskripsi */}
<Text
ta="justify"
fz="sm"
lh={1.5}
lineClamp={5}
truncate="end"
c="dimmed"
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
style={{
wordBreak: 'break-word',
whiteSpace: 'normal',
}}
/>
</Stack>
{/* 📗 Tombol Detail */}
<Button variant='light' color='blue' onClick={() => router.push(`/darmasaba/pendidikan/perpustakaan-digital/${v.kategori?.name}/${v.id}`)}>
Lihat Detail
</Button>
{/* 📗 Tombol Peminjaman */}
<Button
mt="md"
variant="outline"
color="blue"
radius="xl"
size="md"
fullWidth
leftSection={<IconBook2 size={20} />}
onClick={() => {
setSelectedBook(v);
setOpened(true);
}}
>
Peminjaman
</Button>
</Paper>
</motion.div>
))}
</SimpleGrid>
)}
<Center mt="lg">
<Pagination
value={currentPage}
onChange={handlePageChange}
total={totalPages}
color="blue"
radius="md"
/>
</Center>
</Box>
{/* 🔸 Modal Peminjaman */}
<ModalPeminjaman
opened={opened}
onClose={() => {
setOpened(false);
setSelectedBook(null);
}}
buku={selectedBook}
/>
</Stack>
);
}
export default Content;

View File

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