Compare commits

...

5 Commits

Author SHA1 Message Date
bbf13c1cf7 Mengerjakan QC Kak Inno & Kak Ayu Tanggal 16 Oktober
Fix Search
2025-10-17 17:45:56 +08:00
75bf0652b1 Fix QC Kak Inno & Kak Ayu Tanggal 15 Oct 2025-10-17 10:03:03 +08:00
0b574406e2 Fix QC Kak Inno : tanggal 14 Oktober
Fitur Search bisa digunakan di 6 Menu, sisa 3 Menu Lagi
2025-10-15 17:29:57 +08:00
ccf39bc778 Penambahan fungsi search disetiap menu & submenu,
Menu Landing Page
Menu PPID
Menu Desa
2025-10-15 10:13:02 +08:00
3c21f7742c Yang sudh dikerjakan:
- Saat Mau minjam muncul modal data diri peminjam buku V
- Ada Status Peminjamannya ( status buku bisa engga otomatis dipinjemnya), kalau dikembalikan statusnya otomatis
)
Yang Mau Dikerjakan:
Cek fungsi menu yang kompleks
2025-10-14 10:38:55 +08:00
77 changed files with 4168 additions and 1083 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -82,6 +82,7 @@
"react-simple-toasts": "^6.1.0", "react-simple-toasts": "^6.1.0",
"react-toastify": "^11.0.5", "react-toastify": "^11.0.5",
"react-transition-group": "^4.4.5", "react-transition-group": "^4.4.5",
"react-zoom-pan-pinch": "^3.7.0",
"readdirp": "^4.1.1", "readdirp": "^4.1.1",
"recharts": "^2.15.3", "recharts": "^2.15.3",
"sharp": "^0.34.3", "sharp": "^0.34.3",

View File

@@ -1,16 +1,4 @@
[ [
{
"id": "cmds8w2q60002vnbe6i8qhkuo",
"name": "Telephone Desa Darmasaba",
"iconUrl": "081239580000",
"imageId": "cmff3nv180003vn6h5jvedidq"
},
{
"id": "cmds8z7u20005vnbegyyvnbk0",
"name": "Email Desa Darmasaba",
"iconUrl": "desadarmasaba@badungkab.go.id",
"imageId": "cmff3ll130001vn6hkhls3f5y"
},
{ {
"id": "cmds9023u0008vnbe3oxmhwyf", "id": "cmds9023u0008vnbe3oxmhwyf",
"name": "Desa Darmasaba", "name": "Desa Darmasaba",

View File

@@ -143,7 +143,7 @@ model MediaSosial {
isActive Boolean @default(true) isActive Boolean @default(true)
} }
//========================================= PROFILE ========================================= // //========================================= DESA ANTI KORUPSI ========================================= //
model DesaAntiKorupsi { model DesaAntiKorupsi {
id String @id @default(cuid()) id String @id @default(cuid())
name String @unique name String @unique
@@ -1606,7 +1606,7 @@ model Pembiayaan {
ApbDesa ApbDesa[] @relation("ApbDesaPembiayaan") ApbDesa ApbDesa[] @relation("ApbDesaPembiayaan")
} }
// ========================================= INOVASI ========================================= // // ========================================= MENU INOVASI ========================================= //
// ========================================= DESA DIGITAL / SMART VILLAGE ========================================= // // ========================================= DESA DIGITAL / SMART VILLAGE ========================================= //
model DesaDigital { model DesaDigital {
id String @id @default(cuid()) id String @id @default(cuid())

Binary file not shown.

Before

Width:  |  Height:  |  Size: 266 KiB

After

Width:  |  Height:  |  Size: 378 KiB

View File

@@ -75,7 +75,8 @@ const berita = proxy({
loading: false, loading: false,
search: "", search: "",
load: async (page = 1, limit = 10, search = "", kategori = "") => { load: async (page = 1, limit = 10, search = "", kategori = "") => {
berita.findMany.loading = true; // ✅ Akses langsung via nama path const startTime = Date.now();
berita.findMany.loading = true;
berita.findMany.page = page; berita.findMany.page = page;
berita.findMany.search = search; berita.findMany.search = search;
@@ -98,7 +99,14 @@ const berita = proxy({
berita.findMany.data = []; berita.findMany.data = [];
berita.findMany.totalPages = 1; berita.findMany.totalPages = 1;
} finally { } finally {
// pastikan minimal 300ms sebelum loading = false (biar UX smooth)
const elapsed = Date.now() - startTime;
const minDelay = 300;
const delay = elapsed < minDelay ? minDelay - elapsed : 0;
setTimeout(() => {
berita.findMany.loading = false; berita.findMany.loading = false;
}, delay);
} }
}, },
}, },

View File

@@ -68,6 +68,7 @@ const dataPerpustakaan = proxy({
loading: false, loading: false,
search: "", search: "",
load: async (page = 1, limit = 10, search = "", kategori = "") => { load: async (page = 1, limit = 10, search = "", kategori = "") => {
const startTime = Date.now();
dataPerpustakaan.findMany.loading = true; // ✅ Akses langsung via nama path dataPerpustakaan.findMany.loading = true; // ✅ Akses langsung via nama path
dataPerpustakaan.findMany.page = page; dataPerpustakaan.findMany.page = page;
dataPerpustakaan.findMany.search = search; dataPerpustakaan.findMany.search = search;
@@ -77,7 +78,10 @@ const dataPerpustakaan = proxy({
if (search) query.search = search; if (search) query.search = search;
if (kategori) query.kategori = kategori; if (kategori) query.kategori = kategori;
const res = await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan["findMany"].get({ query }); const res =
await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan[
"findMany"
].get({ query });
if (res.status === 200 && res.data?.success) { if (res.status === 200 && res.data?.success) {
dataPerpustakaan.findMany.data = res.data.data ?? []; dataPerpustakaan.findMany.data = res.data.data ?? [];
@@ -91,7 +95,52 @@ const dataPerpustakaan = proxy({
dataPerpustakaan.findMany.data = []; dataPerpustakaan.findMany.data = [];
dataPerpustakaan.findMany.totalPages = 1; dataPerpustakaan.findMany.totalPages = 1;
} finally { } finally {
// pastikan minimal 300ms sebelum loading = false (biar UX smooth)
const elapsed = Date.now() - startTime;
const minDelay = 300;
const delay = elapsed < minDelay ? minDelay - elapsed : 0;
setTimeout(() => {
dataPerpustakaan.findMany.loading = false; dataPerpustakaan.findMany.loading = false;
}, delay);
}
},
},
findManyAll: {
data: null as
| Prisma.DataPerpustakaanGetPayload<{
include: {
image: true;
kategori: true;
};
}>[]
| null,
loading: false,
search: "",
load: async (search = "", kategori = "") => {
dataPerpustakaan.findMany.loading = true; // ✅ Akses langsung via nama path
dataPerpustakaan.findMany.search = search;
try {
const query: any = {};
if (search) query.search = search;
if (kategori) query.kategori = kategori;
const res =
await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan[
"findManyAll"
].get({ query });
if (res.status === 200 && res.data?.success) {
dataPerpustakaan.findManyAll.data = res.data.data ?? [];
} else {
dataPerpustakaan.findManyAll.data = [];
}
} catch (err) {
console.error("Gagal fetch data perpustakaan paginated:", err);
dataPerpustakaan.findManyAll.data = [];
} finally {
dataPerpustakaan.findManyAll.loading = false;
} }
}, },
}, },
@@ -330,7 +379,10 @@ const kategoriBuku = proxy({
const query: any = { page, limit }; const query: any = { page, limit };
if (search) query.search = search; if (search) query.search = search;
const res = await ApiFetch.api.pendidikan.perpustakaandigital.kategoribuku["findMany"].get({ query }); const res =
await ApiFetch.api.pendidikan.perpustakaandigital.kategoribuku[
"findMany"
].get({ query });
if (res.status === 200 && res.data?.success) { if (res.status === 200 && res.data?.success) {
kategoriBuku.findMany.data = res.data.data ?? []; kategoriBuku.findMany.data = res.data.data ?? [];
@@ -522,7 +574,7 @@ const templatePeminjamanBuku = z.object({
tanggalPinjam: z.string().min(1, "Tanggal Pinjam harus diisi"), tanggalPinjam: z.string().min(1, "Tanggal Pinjam harus diisi"),
batasKembali: z.string().min(1, "Batas Kembali harus diisi"), batasKembali: z.string().min(1, "Batas Kembali harus diisi"),
tanggalKembali: z.string().min(1, "Tanggal Kembali harus diisi"), tanggalKembali: z.string().min(1, "Tanggal Kembali harus diisi"),
catatan: z.string().min(1, "Catatan harus diisi") catatan: z.string().min(1, "Catatan harus diisi"),
}); });
const defaultPeminjamanBuku = { const defaultPeminjamanBuku = {
@@ -533,7 +585,7 @@ const defaultPeminjamanBuku = {
tanggalPinjam: "", tanggalPinjam: "",
batasKembali: "", batasKembali: "",
tanggalKembali: "", tanggalKembali: "",
catatan: "" catatan: "",
}; };
interface FormEditData { interface FormEditData {
@@ -549,7 +601,7 @@ interface FormEditData {
batasKembali: string; batasKembali: string;
tanggalKembali: string; tanggalKembali: string;
catatan: string; catatan: string;
status: 'Dipinjam' | 'Dikembalikan' | 'Terlambat' | 'Dibatalkan'; status: "Dipinjam" | "Dikembalikan" | "Terlambat" | "Dibatalkan";
} }
const editForm: FormEditData = { const editForm: FormEditData = {
@@ -561,8 +613,8 @@ const editForm: FormEditData = {
batasKembali: "", batasKembali: "",
tanggalKembali: "", tanggalKembali: "",
catatan: "", catatan: "",
status: "Dipinjam" status: "Dipinjam",
} };
const peminjamanBuku = proxy({ const peminjamanBuku = proxy({
create: { create: {
@@ -616,7 +668,10 @@ const peminjamanBuku = proxy({
const query: any = { page, limit }; const query: any = { page, limit };
if (search) query.search = search; if (search) query.search = search;
const res = await ApiFetch.api.pendidikan.perpustakaandigital.peminjamanbuku["findMany"].get({ query }); const res =
await ApiFetch.api.pendidikan.perpustakaandigital.peminjamanbuku[
"findMany"
].get({ query });
if (res.status === 200 && res.data?.success) { if (res.status === 200 && res.data?.success) {
peminjamanBuku.findMany.data = res.data.data ?? []; peminjamanBuku.findMany.data = res.data.data ?? [];
@@ -685,7 +740,9 @@ const peminjamanBuku = proxy({
); );
await peminjamanBuku.findMany.load(); // refresh list await peminjamanBuku.findMany.load(); // refresh list
} else { } else {
toast.error(result?.message || "Gagal menghapus Data Peminjaman Buku"); toast.error(
result?.message || "Gagal menghapus Data Peminjaman Buku"
);
} }
} catch (error) { } catch (error) {
console.error("Gagal delete:", error); console.error("Gagal delete:", error);
@@ -733,7 +790,7 @@ const peminjamanBuku = proxy({
batasKembali: data.batasKembali, batasKembali: data.batasKembali,
tanggalKembali: data.tanggalKembali, tanggalKembali: data.tanggalKembali,
catatan: data.catatan, catatan: data.catatan,
status: data.status status: data.status,
}; };
return data; // Return the loaded data return data; // Return the loaded data
} else { } else {
@@ -776,7 +833,7 @@ const peminjamanBuku = proxy({
batasKembali: this.form.batasKembali, batasKembali: this.form.batasKembali,
tanggalKembali: this.form.tanggalKembali, tanggalKembali: this.form.tanggalKembali,
catatan: this.form.catatan, catatan: this.form.catatan,
status: this.form.status status: this.form.status,
}), }),
} }
); );
@@ -795,7 +852,9 @@ const peminjamanBuku = proxy({
await peminjamanBuku.findMany.load(); // refresh list await peminjamanBuku.findMany.load(); // refresh list
return true; return true;
} else { } else {
throw new Error(result.message || "Gagal update data peminjaman buku"); throw new Error(
result.message || "Gagal update data peminjaman buku"
);
} }
} catch (error) { } catch (error) {
console.error("Error updating data peminjaman buku:", error); console.error("Error updating data peminjaman buku:", error);
@@ -814,7 +873,7 @@ const peminjamanBuku = proxy({
peminjamanBuku.update.form = { ...editForm }; peminjamanBuku.update.form = { ...editForm };
}, },
}, },
}) });
const perpustakaanDigitalState = proxy({ const perpustakaanDigitalState = proxy({
dataPerpustakaan, dataPerpustakaan,

View File

@@ -561,6 +561,45 @@ const pegawai = proxy({
} }
}, },
}, },
findManyAll: {
data: null as
| Prisma.PegawaiPPIDGetPayload<{
include: {
image: true;
posisi: true;
};
}>[]
| null,
loading: false,
search: "",
load: async (search = "") => {
// Change to arrow function
pegawai.findManyAll.loading = true; // Use the full path to access the property
pegawai.findManyAll.search = search;
try {
const query: any = { search };
if (search) query.search = search;
const res = await ApiFetch.api.ppid.strukturppid.pegawai[
"find-many-all"
].get({
query,
});
if (res.status === 200 && res.data?.success) {
pegawai.findManyAll.data = res.data.data || [];
} else {
console.error("Failed to load pegawai:", res.data?.message);
pegawai.findManyAll.data = [];
}
} catch (error) {
console.error("Error loading pegawai:", error);
pegawai.findManyAll.data = [];
} finally {
pegawai.findManyAll.loading = false;
}
},
},
findUnique: { findUnique: {
data: null as data: null as
| (Prisma.PegawaiPPIDGetPayload<{ | (Prisma.PegawaiPPIDGetPayload<{

View File

@@ -27,7 +27,7 @@ function PelayananPendudukNonPermanent() {
); );
useShallowEffect(() => { useShallowEffect(() => {
pelayananPendudukNonPermanen.findById.load('1'); pelayananPendudukNonPermanen.findById.load('edit');
}, []); }, []);
if (!pelayananPendudukNonPermanen.findById.data) { if (!pelayananPendudukNonPermanen.findById.data) {

View File

@@ -43,7 +43,7 @@ function PerizinanBerusaha() {
try { try {
setLoading(true); setLoading(true);
// You should get the ID from your router query or params // You should get the ID from your router query or params
const id = '1'; // Replace with actual ID or get from URL params const id = 'edit'; // Replace with actual ID or get from URL params
await pelayananPerizinanBerusaha.findById.load(id); await pelayananPerizinanBerusaha.findById.load(id);
} catch (err) { } catch (err) {
setError('Gagal memuat data'); setError('Gagal memuat data');

View File

@@ -89,26 +89,26 @@ function ListArtikelKesehatan({ search }: { search: string }) {
<Table highlightOnHover> <Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Judul</TableTh> <TableTh style={{ minWidth: 200 }}>Judul</TableTh>
<TableTh>Konten</TableTh> <TableTh style={{ minWidth: 200 }}>Konten</TableTh>
<TableTh>Aksi</TableTh> <TableTh style={{ minWidth: 200 }}>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.length > 0 ? ( {filteredData.length > 0 ? (
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd style={{ minWidth: 200 }}>
<Text fw={500} truncate="end" lineClamp={1}> <Text fw={500} truncate="end" lineClamp={1}>
{item.title} {item.title}
</Text> </Text>
</TableTd> </TableTd>
<TableTd> <TableTd style={{ minWidth: 200 }} >
<Text truncate fz="sm" c="dimmed" lineClamp={1}> <Text truncate fz="sm" c="dimmed" lineClamp={1}>
{item.content} {item.content}
</Text> </Text>
</TableTd> </TableTd>
<TableTd> <TableTd style={{ minWidth: 200 }}>
<Button <Button
variant="light" variant="light"
color="blue" color="blue"

View File

@@ -223,7 +223,7 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
{/* Chart */} {/* Chart */}
<Box mt="lg" style={{ width: '100%', minWidth: 300, height: 420, minHeight: 300 }}> <Box mt="lg" style={{ width: '100%', minWidth: 300, height: 420, minHeight: 300 }}>
<Paper bg={colors['white-1']} p={'md'}> <Paper withBorder bg={colors['white-1']} p={'md'}>
<Title pb={10} order={4}>Grafik Hasil Kepuasan Masyarakat</Title> <Title pb={10} order={4}>Grafik Hasil Kepuasan Masyarakat</Title>
{mounted && diseaseChartData.length > 0 ? ( {mounted && diseaseChartData.length > 0 ? (
<Center> <Center>

View File

@@ -111,9 +111,7 @@ function ListInfoWabahPenyakit({ search }: { search: string }) {
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={200}> <Box w={200}>
<Text truncate fz="sm" c="dimmed"> <Text truncate="end" fz="sm" c="dimmed" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat }} />
{item.deskripsiSingkat}
</Text>
</Box> </Box>
</TableTd> </TableTd>
<TableTd> <TableTd>

View File

@@ -66,7 +66,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
return ( return (
<Stack gap="lg"> <Stack gap="lg">
<Title order={2} fw={700} style={{ color: "#1A1B1E" }}> <Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
Profil Desa Profile Desa
</Title> </Title>
<Tabs <Tabs

View File

@@ -65,6 +65,10 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
background: "linear-gradient(135deg, #e7ebf7, #f9faff)", background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem", borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)", boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}} }}
> >
{tabs.map((tab, i) => ( {tabs.map((tab, i) => (

View File

@@ -72,6 +72,10 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
background: "linear-gradient(135deg, #e7ebf7, #f9faff)", background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem", borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)", boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}} }}
> >
{tabs.map((tab, i) => ( {tabs.map((tab, i) => (

View File

@@ -79,6 +79,10 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
background: "linear-gradient(135deg, #e7ebf7, #f9faff)", background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem", borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)", boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}} }}
> >
{tabs.map((tab, i) => ( {tabs.map((tab, i) => (

View File

@@ -73,6 +73,10 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
background: "linear-gradient(135deg, #e7ebf7, #f9faff)", background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem", borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)", boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}} }}
> >
{tabs.map((tab, i) => ( {tabs.map((tab, i) => (

View File

@@ -72,6 +72,10 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
background: "linear-gradient(135deg, #e7ebf7, #f9faff)", background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem", borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)", boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}} }}
> >
{tabs.map((tab, i) => ( {tabs.map((tab, i) => (

View File

@@ -17,6 +17,7 @@ import {
Tooltip, Tooltip,
} from '@mantine/core'; } from '@mantine/core';
import { DateInput } from '@mantine/dates'; import { DateInput } from '@mantine/dates';
import { useShallowEffect } from '@mantine/hooks';
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';
@@ -56,6 +57,10 @@ function EditPeminjam() {
catatan: '', catatan: '',
}); });
useShallowEffect(() => {
perpustakaanDigitalState.dataPerpustakaan.findManyAll.load()
})
useEffect(() => { useEffect(() => {
const loadPeminjam = async () => { const loadPeminjam = async () => {
const id = params?.id as string; const id = params?.id as string;
@@ -159,13 +164,17 @@ function EditPeminjam() {
required required
/> />
<TextInput <Box>
value={formData.buku?.judul || ''} <Text fw="bold" fz="sm" mb={6}>Buku</Text>
label={<Text fw="bold" fz="sm">Buku</Text>} <Select
placeholder="Buku" placeholder="Pilih buku"
required data={perpustakaanDigitalState.dataPerpustakaan.findManyAll.data?.map(p => ({ value: p.id, label: p.judul })) || []}
readOnly value={formData.bukuId}
onChange={(value) => value && setFormData({ ...formData, bukuId: value })}
searchable
clearable
/> />
</Box>
<DateInput <DateInput
value={formData.tanggalPinjam} value={formData.tanggalPinjam}

View File

@@ -66,6 +66,10 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
background: "linear-gradient(135deg, #e7ebf7, #f9faff)", background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem", borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)", boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}} }}
> >
{tabs.map((tab, i) => ( {tabs.map((tab, i) => (

View File

@@ -103,18 +103,7 @@ function ListPegawaiPPID({ search }: { search: string }) {
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{(() => { {filteredData.map((item) => (
console.log('Rendering table with items:', stateOrganisasi.findMany.data);
return null;
})()}
{([...filteredData]
.sort((a, b) => {
if (a.isActive === b.isActive) {
return a.namaLengkap.localeCompare(b.namaLengkap); // kalau status sama, urut nama
}
return Number(b.isActive) - Number(a.isActive); // aktif duluan
}) // Aktif di atas
).map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Box w={150}> <Box w={150}>

View File

@@ -21,10 +21,10 @@ function ListStrukturOrganisasiPPID() {
const stateOrganisasi = useProxy(stateStrukturPPID.pegawai); const stateOrganisasi = useProxy(stateStrukturPPID.pegawai);
useEffect(() => { useEffect(() => {
stateOrganisasi.findMany.load(); stateOrganisasi.findManyAll.load();
}, []); }, []);
if (stateOrganisasi.findMany.loading) { if (stateOrganisasi.findManyAll.loading) {
return ( return (
<Center py={40}> <Center py={40}>
<Loader size="lg" /> <Loader size="lg" />
@@ -32,7 +32,7 @@ function ListStrukturOrganisasiPPID() {
); );
} }
if (!stateOrganisasi.findMany.data || stateOrganisasi.findMany.data.length === 0) { if (!stateOrganisasi.findManyAll.data || stateOrganisasi.findManyAll.data.length === 0) {
return ( return (
<Stack align="center" py={60} gap="sm"> <Stack align="center" py={60} gap="sm">
<IconUsers size={60} stroke={1.5} color="var(--mantine-color-gray-6)" /> <IconUsers size={60} stroke={1.5} color="var(--mantine-color-gray-6)" />
@@ -43,7 +43,7 @@ function ListStrukturOrganisasiPPID() {
const posisiMap = new Map<string, any>(); const posisiMap = new Map<string, any>();
const aktifPegawai = stateOrganisasi.findMany.data.filter(p => p.isActive); const aktifPegawai = stateOrganisasi.findManyAll.data?.filter(p => p.isActive);
for (const pegawai of aktifPegawai) { for (const pegawai of aktifPegawai) {
const posisiId = pegawai.posisi.id; const posisiId = pegawai.posisi.id;

View File

@@ -377,22 +377,5 @@ export const navBar = [
path: "/admin/pendidikan/data-pendidikan" path: "/admin/pendidikan/data-pendidikan"
} }
] ]
}, }
{
id: "User & Role",
name: "User & Role",
path: "",
children: [
{
id: "User",
name: "User",
path: "/admin/user&role/user"
},
{
id: "Role",
name: "Role",
path: "/admin/user&role/role"
},
]
},
] ]

View File

@@ -0,0 +1,48 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function dataPerpustakaanFindManyAll(context: Context) {
const search = (context.query.search as string) || "";
const isActiveParam = context.query.isActive;
// Buat where clause dinamis
const where: any = {};
if (isActiveParam !== undefined) {
where.isActive = isActiveParam === "true";
}
if (search) {
where.OR = [
{ judul: { contains: search, mode: "insensitive" } },
{ deskripsi: { contains: search, mode: "insensitive" } },
];
}
try {
const data = await prisma.dataPerpustakaan.findMany({
where,
include: {
kategori: true,
image: true,
},
orderBy: { createdAt: "desc" },
});
return {
success: true,
message: "Success fetch all data perpustakaan (non-paginated)",
total: data.length,
data,
};
} catch (error) {
console.error("Find many all error:", error);
return {
success: false,
message: "Failed fetch all data perpustakaan",
total: 0,
data: [],
};
}
}

View File

@@ -4,6 +4,7 @@ import dataPerpustakaanDelete from "./del";
import dataPerpustakaanFindMany from "./findMany"; import dataPerpustakaanFindMany from "./findMany";
import dataPerpustakaanFindUnique from "./findUnique"; import dataPerpustakaanFindUnique from "./findUnique";
import dataPerpustakaanUpdate from "./updt"; import dataPerpustakaanUpdate from "./updt";
import dataPerpustakaanFindManyAll from "./findManyAll";
const DataPerpustakaan = new Elysia({ const DataPerpustakaan = new Elysia({
prefix: "/dataperpustakaan", prefix: "/dataperpustakaan",
@@ -18,7 +19,7 @@ const DataPerpustakaan = new Elysia({
kategoriId: t.String(), kategoriId: t.String(),
}), }),
}) })
.get("/findManyAll", dataPerpustakaanFindManyAll)
.get("/findMany", dataPerpustakaanFindMany) .get("/findMany", dataPerpustakaanFindMany)
.get("/:id", async (context) => { .get("/:id", async (context) => {
const response = await dataPerpustakaanFindUnique( const response = await dataPerpustakaanFindUnique(

View File

@@ -2,61 +2,67 @@
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { Context } from "elysia"; import { Context } from "elysia";
// Di findMany.ts
export default async function pegawaiFindMany(context: Context) { export default async function pegawaiFindMany(context: Context) {
const page = Number(context.query.page) || 1; const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10; const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || ""; const search = (context.query.search as string) || "";
const skip = (page - 1) * limit; const skip = (page - 1) * limit;
// Buat where clause
const isActiveParam = context.query.isActive; const isActiveParam = context.query.isActive;
// where clause dinamis
const where: any = {}; const where: any = {};
if (isActiveParam !== undefined) { if (isActiveParam !== undefined) {
where.isActive = isActiveParam === "true"; where.isActive = isActiveParam === "true";
} }
// Tambahkan pencarian (jika ada)
if (search) { if (search) {
where.OR = [ where.OR = [
{ namaLengkap: { contains: search, mode: "insensitive" } }, { namaLengkap: { contains: search, mode: "insensitive" } },
{ alamat: { contains: search, mode: "insensitive" } }, { alamat: { contains: search, mode: "insensitive" } },
{ posisi: { nama: { contains: search, mode: "insensitive" } } },
]; ];
} }
try { try {
const [data, total] = await Promise.all([ // Ambil semua data terlebih dahulu (tanpa pagination)
const [allData, total] = await Promise.all([
prisma.pegawaiPPID.findMany({ prisma.pegawaiPPID.findMany({
where, where,
include: { include: {
posisi: true, posisi: true,
image: true, image: true,
}, },
skip,
take: limit,
orderBy: { posisi: { hierarki: "asc" } },
}),
prisma.pegawaiPPID.count({
where,
}), }),
prisma.pegawaiPPID.count({ where }),
]); ]);
// Sort manual berdasarkan hierarki posisi
const sortedData = allData.sort((a, b) => {
// Sort berdasarkan hierarki terlebih dahulu
if (a.posisi.hierarki !== b.posisi.hierarki) {
return a.posisi.hierarki - b.posisi.hierarki;
}
// Jika hierarki sama, sort berdasarkan nama posisi
return a.posisi.nama.localeCompare(b.posisi.nama);
});
// Lakukan pagination manual setelah sorting
const paginatedData = sortedData.slice(skip, skip + limit);
const totalPages = Math.ceil(total / limit); const totalPages = Math.ceil(total / limit);
return { return {
success: true, success: true,
message: "Success fetch pegawai with pagination", message: "Success fetch pegawai with hierarchy order",
data, data: paginatedData,
page, page,
totalPages, totalPages,
total, total,
}; };
} catch (e) { } catch (error) {
console.error("Find many paginated error:", e); console.error("Find many pegawai error:", error);
return { return {
success: false, success: false,
message: "Failed fetch pegawai with pagination", message: "Failed fetch pegawai",
data: [], data: [],
page: 1, page: 1,
totalPages: 1, totalPages: 1,

View File

@@ -0,0 +1,48 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function pegawaiFindManyAll(context: Context) {
const search = (context.query.search as string) || "";
const isActiveParam = context.query.isActive;
// Buat where clause dinamis
const where: any = {};
if (isActiveParam !== undefined) {
where.isActive = isActiveParam === "true";
}
if (search) {
where.OR = [
{ namaLengkap: { contains: search, mode: "insensitive" } },
{ alamat: { contains: search, mode: "insensitive" } },
];
}
try {
const data = await prisma.pegawaiPPID.findMany({
where,
include: {
posisi: true,
image: true,
},
orderBy: { posisi: { hierarki: "asc" } },
});
return {
success: true,
message: "Success fetch all pegawai (non-paginated)",
total: data.length,
data,
};
} catch (error) {
console.error("Find many all error:", error);
return {
success: false,
message: "Failed fetch all pegawai",
total: 0,
data: [],
};
}
}

View File

@@ -5,6 +5,7 @@ import pegawaiCreate from "./create";
import pegawaiNonActive from "./nonActive"; import pegawaiNonActive from "./nonActive";
import pegawaiUpdate from "./updt"; import pegawaiUpdate from "./updt";
import pegawaiDelete from "./del"; import pegawaiDelete from "./del";
import pegawaiFindManyAll from "./findManyAll";
const Pegawai = new Elysia({ const Pegawai = new Elysia({
@@ -15,6 +16,9 @@ const Pegawai = new Elysia({
// ✅ Find all // ✅ Find all
.get("/find-many", pegawaiFindMany) .get("/find-many", pegawaiFindMany)
// ✅ Find all (non-paginated)
.get("/find-many-all", pegawaiFindManyAll)
// ✅ Find by ID // ✅ Find by ID
.get("/:id", async (context) => { .get("/:id", async (context) => {
const response = await pegawaiFindUnique(context); const response = await pegawaiFindUnique(context);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,10 @@
import Elysia from "elysia";
import searchFindMany from "./findMany";
const Search = new Elysia({
prefix: "/api/search",
tags: ["Search"],
})
.get("/findMany", searchFindMany);
export default Search;

View File

@@ -0,0 +1,148 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client';
import { proxy } from 'valtio';
import { debounce } from 'lodash';
import ApiFetch from '@/lib/api-fetch';
interface SearchResult {
type?: string;
id: string | number;
title?: string;
[key: string]: any;
}
const searchState = proxy({
query: '',
page: 1,
limit: 10,
type: '', // kosong = global search
results: [] as SearchResult[],
nextPage: null as number | null,
loading: false,
async fetch() {
if (!searchState.query) {
searchState.results = [];
return;
}
searchState.loading = true;
const res = await ApiFetch.api.search.findMany.get({
query: {
query: searchState.query,
page: searchState.page,
limit: searchState.limit,
type: searchState.type,
},
});
if (searchState.page === 1) {
searchState.results = res.data?.data || [];
} else {
searchState.results.push(...(res.data?.data || []));
}
searchState.nextPage = res.data?.nextPage || null;
searchState.loading = false;
},
async next() {
if (!searchState.nextPage || searchState.loading) return;
searchState.page = searchState.nextPage;
await searchState.fetch();
},
});
// 🕒 debounce-nya tetap kita export biar bisa dipanggil manual
export const debouncedFetch = debounce(() => {
searchState.page = 1;
searchState.fetch();
}, 500);
export default searchState;
// 'use client';
// import { proxy, subscribe } from 'valtio';
// import { debounce } from 'lodash';
// import ApiFetch from '@/lib/api-fetch';
// interface SearchResult {
// type?: string;
// id: string | number;
// title?: string;
// [key: string]: any;
// }
// const searchState = proxy({
// query: '',
// page: 1,
// limit: 10,
// type: '', // kosong = global search
// results: [] as SearchResult[],
// nextPage: null as number | null,
// loading: false,
// // --- fetch utama ---
// async fetch() {
// if (!searchState.query.trim()) {
// // 🧹 kalau query kosong, kosongin data dan stop
// searchState.results = [];
// searchState.nextPage = null;
// searchState.loading = false;
// return;
// }
// searchState.loading = true;
// try {
// const res = await ApiFetch.api.search.findMany.get({
// query: {
// query: searchState.query,
// page: searchState.page,
// limit: searchState.limit,
// type: searchState.type,
// },
// });
// const newData = res.data?.data || [];
// // Kalau ini page pertama, replace data
// if (searchState.page === 1) {
// searchState.results = newData;
// } else {
// // Kalau page berikutnya, append data
// searchState.results = [...searchState.results, ...newData];
// }
// searchState.nextPage = res.data?.nextPage || null;
// } catch (err) {
// console.error('Search fetch error:', err);
// } finally {
// searchState.loading = false;
// }
// },
// // --- load next page (infinite scroll) ---
// async next() {
// if (!searchState.nextPage || searchState.loading) return;
// searchState.page = searchState.nextPage;
// await searchState.fetch();
// },
// });
// // --- debounce agar gak fetch tiap ketik ---
// const debouncedFetch = debounce(() => {
// // reset pagination setiap query berubah
// searchState.page = 1;
// searchState.fetch();
// }, 500);
// // --- auto trigger setiap query berubah ---
// subscribe(searchState, () => {
// // kalau query berubah, jalankan debounce fetch
// debouncedFetch();
// });
// export default searchState;

View File

@@ -25,6 +25,7 @@ import LandingPage from "./_lib/landing_page";
import Pendidikan from "./_lib/pendidikan"; import Pendidikan from "./_lib/pendidikan";
import User from "./_lib/user"; import User from "./_lib/user";
import Role from "./_lib/user/role"; import Role from "./_lib/user/role";
import Search from "./_lib/search";
const ROOT = process.cwd(); const ROOT = process.cwd();
@@ -95,6 +96,7 @@ const ApiServer = new Elysia()
.use(Pendidikan) .use(Pendidikan)
.use(User) .use(User)
.use(Role) .use(Role)
.use(Search)
.onError(({ code }) => { .onError(({ code }) => {
if (code === "NOT_FOUND") { if (code === "NOT_FOUND") {

View File

@@ -22,21 +22,19 @@ interface FileItem {
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [totalPages, setTotalPages] = useState(1); const [totalPages, setTotalPages] = useState(1);
const limit = 9; // ✅ ambil 12 data per page
// Handle search and pagination changes const loadData = useCallback(async (pageNum: number, searchTerm: string) => {
const loadData = useCallback((pageNum: number, searchTerm: string) => {
setLoading(true); setLoading(true);
// Using the load function from the component's scope
const loadFn = async () => {
try { try {
const response = await ApiFetch.api.fileStorage.findMany.get({ const query: Record<string, string> = {
query: {
category: 'image', category: 'image',
page: pageNum.toString(), page: pageNum.toString(),
limit: '10', limit: limit.toString(),
...(searchTerm && { search: searchTerm }) };
} if (searchTerm) query.search = searchTerm;
});
const response = await ApiFetch.api.fileStorage.findMany.get({ query });
if (response.status === 200 && response.data) { if (response.status === 200 && response.data) {
setFiles(response.data.data || []); setFiles(response.data.data || []);
@@ -50,78 +48,41 @@ interface FileItem {
} finally { } finally {
setLoading(false); setLoading(false);
} }
};
loadFn();
}, []); }, []);
// Initial load and URL change handler // Initial load + update when URL/search changes
useEffect(() => { useEffect(() => {
const handleRouteChange = () => { const handleRouteChange = () => {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const urlSearch = urlParams.get('search') || ''; const urlSearch = urlParams.get('search') || '';
const urlPage = parseInt(urlParams.get('page') || '1'); const urlPage = parseInt(urlParams.get('page') || '1');
setSearch(urlSearch); setSearch(urlSearch);
setPage(urlPage); setPage(urlPage);
loadData(urlPage, urlSearch); loadData(urlPage, urlSearch);
}; };
// Handle search updates from the search bar
const handleSearchUpdate = (e: Event) => { const handleSearchUpdate = (e: Event) => {
const { search } = (e as CustomEvent).detail; const { search } = (e as CustomEvent).detail;
setSearch(search); setSearch(search);
setPage(1); // Reset to first page on new search setPage(1);
loadData(1, search); loadData(1, search);
}; };
// Initial load
handleRouteChange(); handleRouteChange();
// Set up event listeners
window.addEventListener('popstate', handleRouteChange); window.addEventListener('popstate', handleRouteChange);
window.addEventListener('searchUpdate', handleSearchUpdate as EventListener); window.addEventListener('searchUpdate', handleSearchUpdate as EventListener);
// Cleanup
return () => { return () => {
window.removeEventListener('popstate', handleRouteChange); window.removeEventListener('popstate', handleRouteChange);
window.removeEventListener('searchUpdate', handleSearchUpdate as EventListener); window.removeEventListener('searchUpdate', handleSearchUpdate as EventListener);
}; };
}, [loadData]); }, [loadData]);
// ✅ Fetch data // ✅ Update when page/search changes
useEffect(() => { useEffect(() => {
const fetchFiles = async () => { loadData(page, search);
setLoading(true); }, [page, search, loadData]);
try {
const query: Record<string, string> = {
category: 'image',
page: page.toString(),
limit: '10',
};
if (search) query.search = search;
const response = await ApiFetch.api.fileStorage.findMany.get({ query });
if (response.status === 200 && response.data) {
setFiles(response.data.data || []);
setTotalPages(response.data.meta?.totalPages || 1);
} else {
setFiles([]);
}
} catch (err) {
console.error('Fetch error:', err);
setFiles([]);
} finally {
setLoading(false);
}
};
if (page > 0) fetchFiles(); // jangan fetch jika page belum valid
}, [search, page]);
// ✅ Update URL
const updateURL = (newSearch: string, newPage: number) => { const updateURL = (newSearch: string, newPage: number) => {
const url = new URL(window.location.href); const url = new URL(window.location.href);
if (newSearch) url.searchParams.set('search', newSearch); if (newSearch) url.searchParams.set('search', newSearch);
@@ -148,7 +109,14 @@ interface FileItem {
<Box pt={20} px={{ base: 'md', md: 100 }}> <Box pt={20} px={{ base: 'md', md: 100 }}>
<SimpleGrid cols={{ base: 1, md: 3 }}> <SimpleGrid cols={{ base: 1, md: 3 }}>
{files.map((file) => ( {files.map((file) => (
<Paper key={file.id} mb={50} p="md" radius={26} bg={colors['white-trans-1']} style={{ height: '100%' }}> <Paper
key={file.id}
mb={50}
p="md"
radius={26}
bg={colors['white-trans-1']}
style={{ height: '100%' }}
>
<Box style={{ height: '250px', overflow: 'hidden', borderRadius: '12px' }}> <Box style={{ height: '250px', overflow: 'hidden', borderRadius: '12px' }}>
<Image <Image
src={file.link} src={file.link}
@@ -159,7 +127,6 @@ interface FileItem {
loading="lazy" loading="lazy"
/> />
</Box> </Box>
<Box>
<Stack gap="sm" py={10}> <Stack gap="sm" py={10}>
<Text fw="bold" fz={{ base: 'h4', md: 'h3' }}> <Text fw="bold" fz={{ base: 'h4', md: 'h3' }}>
{file.realName || file.name} {file.realName || file.name}
@@ -172,7 +139,6 @@ interface FileItem {
})} })}
</Text> </Text>
</Stack> </Stack>
</Box>
</Paper> </Paper>
))} ))}
</SimpleGrid> </SimpleGrid>

View File

@@ -146,24 +146,24 @@ function Page() {
<Title order={3}>Ajukan Permohonan</Title> <Title order={3}>Ajukan Permohonan</Title>
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Nama</Text>} label={<Text fz="sm" fw="bold">Nama</Text>}
placeholder="masukkan nama" placeholder="Masukkan nama"
onChange={(val) => (stateCreate.create.form.nama = val.target.value)} onChange={(val) => (stateCreate.create.form.nama = val.target.value)}
/> />
<TextInput <TextInput
type="number" type="number"
label={<Text fz="sm" fw="bold">NIK</Text>} label={<Text fz="sm" fw="bold">NIK</Text>}
placeholder="masukkan NIK" placeholder="Masukkan NIK"
onChange={(val) => (stateCreate.create.form.nik = val.target.value)} onChange={(val) => (stateCreate.create.form.nik = val.target.value)}
/> />
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Alamat</Text>} label={<Text fz="sm" fw="bold">Alamat</Text>}
placeholder="masukkan alamat" placeholder="Masukkan alamat"
onChange={(val) => (stateCreate.create.form.alamat = val.target.value)} onChange={(val) => (stateCreate.create.form.alamat = val.target.value)}
/> />
<TextInput <TextInput
type="number" type="number"
label={<Text fz="sm" fw="bold">Nomor KK</Text>} label={<Text fz="sm" fw="bold">Nomor KK</Text>}
placeholder="masukkan Nomor KK" placeholder="Masukkan Nomor KK"
onChange={(val) => (stateCreate.create.form.nomorKk = val.target.value)} onChange={(val) => (stateCreate.create.form.nomorKk = val.target.value)}
/> />
<Select <Select

View File

@@ -16,7 +16,7 @@ function PelayananPendudukNonPermanent() {
const loadData = async () => { const loadData = async () => {
try { try {
setLoading(true); setLoading(true);
await state.pelayananPendudukNonPermanen.findById.load('1'); await state.pelayananPendudukNonPermanen.findById.load('edit');
} catch (error) { } catch (error) {
console.error('Gagal memuat data:', error); console.error('Gagal memuat data:', error);
} finally { } finally {

View File

@@ -17,7 +17,7 @@ function PelayananPerizinanBerusaha() {
const loadData = async () => { const loadData = async () => {
try { try {
setLoading(true); setLoading(true);
await state.pelayananPerizinanBerusaha.findById.load('1') await state.pelayananPerizinanBerusaha.findById.load('edit')
} catch (error) { } catch (error) {
console.error('Gagal memuat data:', error); console.error('Gagal memuat data:', error);
} finally { } finally {

View File

@@ -77,9 +77,7 @@ function Page() {
fallbackSrc="https://placehold.co/800x400?text=Gambar+tidak+tersedia" fallbackSrc="https://placehold.co/800x400?text=Gambar+tidak+tersedia"
loading="lazy" loading="lazy"
/> />
<Text py="md" fz={{ base: "sm", md: "md" }} ta="justify" lh={1.8}> <Text py="md" fz={{ base: "sm", md: "md" }} ta="justify" lh={1.8} dangerouslySetInnerHTML={{ __html: state.findUnique.data?.deskripsi || 'Belum ada deskripsi untuk potensi desa ini.' }} />
{state.findUnique.data?.deskripsi || 'Belum ada deskripsi untuk potensi desa ini.'}
</Text>
</Stack> </Stack>
</Paper> </Paper>
</Container> </Container>

View File

@@ -10,9 +10,11 @@ import ProfilPerbekel from './ui/profilPerbekel';
// import LembagaDesa from './ui/lembagaDesa'; // import LembagaDesa from './ui/lembagaDesa';
import MotoDesa from './ui/motoDesa'; import MotoDesa from './ui/motoDesa';
import SemuaPerbekel from './ui/semuaPerbekel'; import SemuaPerbekel from './ui/semuaPerbekel';
import ScrollToTopButton from '@/app/darmasaba/_com/scrollToTopButton';
function Page() { function Page() {
return ( return (
<Box>
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}> <Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
<BackButton /> <BackButton />
@@ -35,6 +37,9 @@ function Page() {
<SemuaPerbekel /> <SemuaPerbekel />
</Box> </Box>
</Stack> </Stack>
{/* Tombol Scroll ke Atas */}
<ScrollToTopButton />
</Box>
); );
} }

View File

@@ -72,21 +72,21 @@ function Page() {
) )
} }
return ( return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}> <Stack pos="relative" bg={colors.Bg} py="xl" gap="22" style={{ overflow: 'auto' }}>
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 50, lg: 100 }}>
<BackButton /> <BackButton />
</Box> </Box>
<Box px={{ base: 'md', md: 100 }} > <Box px={{ base: 'md', md: 50, lg: 100 }} >
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}> <Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
Jumlah Penduduk Usia Kerja Yang Menganggur Jumlah Penduduk Usia Kerja Yang Menganggur
</Text> </Text>
</Box> </Box>
<Box px={{ base: "md", md: 100 }}> <Box px={{ base: "md", md: 50, lg: 100 }}>
<Stack gap={'lg'} justify='center'> <Stack gap={'lg'} justify='center'>
<Paper p={'lg'}> <Paper p={'lg'}>
<Text fw={'bold'} fz={'h3'}>Pengangguran Berdasarkan Usia</Text> <Text fw={'bold'} fz={'h3'}>Pengangguran Berdasarkan Usia</Text>
{mounted && donutGrafikNganggurData.length > 0 ? (<Box style={{ width: '100%', height: 'auto', minHeight: 200 }}> {mounted && donutGrafikNganggurData.length > 0 ? (<Box style={{ width: '100%', height: 'auto', minHeight: 200 }}>
<Box w="100%" style={{ maxWidth: 400, margin: "0 auto" }}> <Box w="100%" maw={{ base: '100%', md: 400 }} mx="auto">
<PieChart <PieChart
w="100%" w="100%"
h={250} // lebih kecil biar aman di mobile h={250} // lebih kecil biar aman di mobile
@@ -133,7 +133,7 @@ function Page() {
<Box w="100%" style={{ maxWidth: 400, margin: "0 auto" }}> <Box w="100%" style={{ maxWidth: 400, margin: "0 auto" }}>
<PieChart <PieChart
w="100%" w="100%"
h={250} // lebih kecil biar aman di mobile h="min(250px, 50vh)" // lebih kecil biar aman di mobile
withLabelsLine withLabelsLine
labelsPosition="outside" labelsPosition="outside"
labelsType="percent" labelsType="percent"

View File

@@ -199,7 +199,7 @@ function Page() {
<TableTd ta={'center'}>{item.totalUnemployment}</TableTd> <TableTd ta={'center'}>{item.totalUnemployment}</TableTd>
<TableTd ta={'center'}>{item.educatedUnemployment}</TableTd> <TableTd ta={'center'}>{item.educatedUnemployment}</TableTd>
<TableTd ta={'center'}>{item.uneducatedUnemployment}</TableTd> <TableTd ta={'center'}>{item.uneducatedUnemployment}</TableTd>
<TableTd ta={'center'}>{item.percentageChange}</TableTd> <TableTd ta={'center'}>{item.percentageChange}%</TableTd>
</TableTr> </TableTr>
))} ))}
</TableTbody> </TableTbody>

View File

@@ -28,6 +28,32 @@ function Page() {
) )
} }
// Add this check before the return statement
if (data.length === 0) {
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap={22}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
<Text fz={{ base: 'h1', md: '2.5rem' }} c={colors['blue-button']} fw="bold">
Sektor Unggulan Desa Darmasaba
</Text>
<Text c="dimmed" mt="md">
Data sektor unggulan belum tersedia
</Text>
</Box>
</Stack>
);
}
const chartData = data
.filter(item => item?.name && typeof item.value === 'number')
.map((item) => ({
id: item.id,
sektor: item.name,
Ton: item.value,
}));
return ( return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}> <Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
@@ -49,23 +75,30 @@ function Page() {
</Paper> </Paper>
) )
})} })}
<Paper p={'xl'}> <Box style={{ width: '100%', overflowX: 'auto' }}>
<Text pb={10} fw={'bold'} fz={'h4'}>Statistik Sektor Unggulan Darmasaba</Text> <Paper p="xl">
<Text pb={10} fw="bold" fz="h4">Statistik Sektor Unggulan Darmasaba</Text>
<Box style={{ width: '100%', minWidth: '600px' }}>
<BarChart <BarChart
p={10} p={10}
h={300} h={300}
data={data.map((item) => ({ data={chartData}
id: item.id,
sektor: item.name,
Ton: item.value,
}))}
dataKey="sektor" dataKey="sektor"
series={[ series={[
{ name: 'Ton', color: colors['blue-button'] }, { name: 'Ton', color: colors['blue-button'] },
]} ]}
tickLine="y" tickLine="y"
tooltipAnimationDuration={200}
withTooltip
style={{
fontFamily: 'inherit',
}}
xAxisLabel="Sektor"
yAxisLabel="Ton"
/> />
</Box>
</Paper> </Paper>
</Box>
</Stack> </Stack>
</Box> </Box>
</Stack> </Stack>

View File

@@ -50,10 +50,12 @@ function Page() {
</Text> </Text>
</Box> </Box>
<TextInput <TextInput
placeholder='Cari kontak darurat, nama, atau nomor...' radius={"lg"}
leftSection={<IconSearch size={20} />} placeholder='Cari Kontak Darurat'
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.target.value)}
leftSection={<IconSearch size={20} />}
w={{ base: "100%", md: "25%" }}
/> />
</Group> </Group>
<Box px={{ base: "md", md: 100 }}> <Box px={{ base: "md", md: 100 }}>
@@ -95,10 +97,12 @@ function Page() {
</Text> </Text>
</Box> </Box>
<TextInput <TextInput
placeholder='Cari kontak darurat, nama, atau nomor...' radius={"lg"}
leftSection={<IconSearch size={20} />} placeholder='Cari Kontak Darurat'
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.target.value)}
leftSection={<IconSearch size={20} />}
w={{ base: "50%", md: "100%" }}
/> />
</Group> </Group>
<Box px={{ base: "md", md: 100 }}> <Box px={{ base: "md", md: 100 }}>

View File

@@ -4,7 +4,7 @@ import colors from '@/con/colors';
import { Box, Button, Center, ColorSwatch, Flex, Group, Modal, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Center, ColorSwatch, Flex, Group, Modal, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from '@mantine/core';
import { DateTimePicker } from '@mantine/dates'; import { DateTimePicker } from '@mantine/dates';
import { useDebouncedValue, useDisclosure, useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useDisclosure, useShallowEffect } from '@mantine/hooks';
import { IconArrowRight, IconPlus } from '@tabler/icons-react'; import { IconArrowRight, IconPlus, IconSearch } from '@tabler/icons-react';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto'; import BackButton from '../../desa/layanan/_com/BackButto';
@@ -56,9 +56,12 @@ function Page() {
<Flex justify="space-between" align="center"> <Flex justify="space-between" align="center">
<BackButton /> <BackButton />
<TextInput <TextInput
placeholder="Cari laporan" radius={"lg"}
placeholder='Cari Laporan Publik'
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.target.value)}
leftSection={<IconSearch size={20} />}
w={{ base: "50%", md: "100%" }}
/> />
</Flex> </Flex>
</Box> </Box>

View File

@@ -41,6 +41,37 @@ function Page() {
) )
} }
if (data.length === 0) {
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap={22}>
<Box px={{ base: 'md', md: 100 }}>
<Text fz={{ base: 'h1', md: '2.5rem' }} c={colors['blue-button']} fw="bold">
Pencegahan Kriminalitas
</Text>
<Text c={colors['blue-button']} fz={{ base: 'h4', md: 'h3' }}>
Keamanan Komunitas & Pencegahan Kriminal
</Text>
</Box>
<SimpleGrid
px={{ base: 20, md: 100 }}
cols={{ base: 1, md: 2 }}
spacing="xl"
>
<Paper p="xl" radius="xl" shadow="lg" >
<Text fz={{ base: 'h3', md: 'h2' }} c={colors['blue-button']} fw="bold">
Program Keamanan Berjalan
</Text>
<Stack pt={30} gap="lg">
<Text c="dimmed">
Tidak ada data pencegahan kriminalitas yang cocok
</Text>
</Stack>
</Paper>
</SimpleGrid>
</Stack>
)
}
return ( return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap={22}> <Stack pos="relative" bg={colors.Bg} py="xl" gap={22}>
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>

View File

@@ -2,7 +2,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import colors from '@/con/colors'; import colors from '@/con/colors';
import { BarChart as MantineBarChart } from '@mantine/charts'; import { BarChart as MantineBarChart } from '@mantine/charts';
import { Box, Center, ColorSwatch, Flex, Paper, SimpleGrid, Skeleton, Stack, Text } from '@mantine/core'; import { Box, Center, ColorSwatch, Flex, Paper, SimpleGrid, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import BackButton from '../../desa/layanan/_com/BackButto'; import BackButton from '../../desa/layanan/_com/BackButto';
@@ -107,20 +107,7 @@ function Page() {
<Box> <Box>
<Paper p={"xl"} bg={colors['white-trans-1']}> <Paper p={"xl"} bg={colors['white-trans-1']}>
<Box pb={30}> <Box pb={30}>
<Flex pb={30} justify={'flex-end'} gap={'xl'} align={'center'}> <Title order={2} mb="md">Data Kematian dan Kelahiran</Title>
<Box>
<Flex gap={{ base: 0, md: 5 }} align={'center'}>
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Angka Kematian</Text>
<ColorSwatch color="#EF3E3E" size={30} />
</Flex>
</Box>
<Box>
<Flex gap={{ base: 0, md: 5 }} align={'center'}>
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Angka Kelahiran</Text>
<ColorSwatch color="#3290CA" size={30} />
</Flex>
</Box>
</Flex>
{chartData.length === 0 ? ( {chartData.length === 0 ? (
<Text c="dimmed" ta="center" py="xl"> <Text c="dimmed" ta="center" py="xl">
Belum ada data yang tersedia untuk ditampilkan Belum ada data yang tersedia untuk ditampilkan
@@ -150,6 +137,20 @@ function Page() {
</Center> </Center>
</> </>
)} )}
<Flex pb={30} justify={'center'} gap={'xl'} align={'center'}>
<Box>
<Flex gap={{ base: 0, md: 5 }} align={'center'}>
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Angka Kematian</Text>
<ColorSwatch color="#EF3E3E" size={30} />
</Flex>
</Box>
<Box>
<Flex gap={{ base: 0, md: 5 }} align={'center'}>
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Angka Kelahiran</Text>
<ColorSwatch color="#3290CA" size={30} />
</Flex>
</Box>
</Flex>
</Box> </Box>
</Paper> </Paper>
</Box> </Box>

View File

@@ -121,13 +121,13 @@ function Page() {
</Badge> </Badge>
</Group> </Group>
<Text fz="sm" c="dimmed"> <Text fz="sm" c="dimmed">
Diposting: 12 Februari 2025 · Dinas Kesehatan Diposting: {v.createdAt.toLocaleDateString()}
</Text> </Text>
<Divider /> <Divider />
<Text fz="sm" lh={1.5}> <Text fz="sm" lh={1.5} lineClamp={3} truncate="end">
{v.deskripsiSingkat} {v.deskripsiSingkat}
</Text> </Text>
<Button variant="light" radius="md" size="md" onClick={() => router.push(`/admin/kesehatan/info-wabah-penyakit/${v.id}`)}> <Button variant="light" radius="md" size="md" onClick={() => router.push(`/darmasaba/kesehatan/info-wabah-penyakit/${v.id}`)}>
Selengkapnya Selengkapnya
</Button> </Button>
</Stack> </Stack>

View File

@@ -101,15 +101,30 @@ function Page() {
}} }}
> >
<Stack align="center" gap="sm"> <Stack align="center" gap="sm">
<Box
style={{
width: '100%',
aspectRatio: '16/9',
borderRadius: '12px',
overflow: 'hidden',
position: 'relative',
}}
>
<Image <Image
src={v.image.link} src={v.image.link}
alt={v.name} alt={v.name}
w={140} fit="cover"
h={140}
fit="contain"
radius="md"
loading="lazy" loading="lazy"
style={{
width: '100%',
height: '100%',
transition: 'transform 0.4s ease',
}}
onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.05)')}
onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')}
/> />
</Box>
<Text ta="center" fw={700} fz="lg" c={colors['blue-button']}> <Text ta="center" fw={700} fz="lg" c={colors['blue-button']}>
{v.name} {v.name}
</Text> </Text>

View File

@@ -2,7 +2,6 @@
import penangananDarurat from '@/app/admin/(dashboard)/_state/kesehatan/penanganan-darurat/penangananDarurat' import penangananDarurat from '@/app/admin/(dashboard)/_state/kesehatan/penanganan-darurat/penangananDarurat'
import colors from '@/con/colors' import colors from '@/con/colors'
import { import {
Badge,
Box, Box,
Center, Center,
Grid, Grid,
@@ -97,19 +96,39 @@ function Page() {
shadow="sm" shadow="sm"
withBorder withBorder
bg={colors['white-trans-1']} bg={colors['white-trans-1']}
style={{ transition: 'all 0.3s ease' }} style={{
transition: 'all 0.3s ease',
transform: 'translateY(0)',
}}
onMouseEnter={(e) => (e.currentTarget.style.transform = 'translateY(-5px)')}
onMouseLeave={(e) => (e.currentTarget.style.transform = 'translateY(0)')}
> >
<Stack align="center" gap="md"> <Stack align="center" gap="md">
<Center> <Center>
<Box
style={{
width: '100%',
aspectRatio: '16/9',
borderRadius: '12px',
overflow: 'hidden',
position: 'relative',
}}
>
<Image <Image
src={v.image.link} src={v.image.link}
alt={v.name} alt={v.name}
w={160} fit="cover"
h={160}
fit="contain"
radius="md"
loading="lazy" loading="lazy"
style={{
width: '100%',
height: '100%',
transition: 'transform 0.4s ease',
}}
onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.05)')}
onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')}
/> />
</Box>
</Center> </Center>
<Stack gap={4} w="100%"> <Stack gap={4} w="100%">
<Text <Text
@@ -131,9 +150,6 @@ function Page() {
/> />
</Box> </Box>
</Stack> </Stack>
<Badge radius="md" color="blue" variant="light" mt="sm">
Darurat
</Badge>
</Stack> </Stack>
</Paper> </Paper>
))} ))}
@@ -151,8 +167,11 @@ function Page() {
styles={{ styles={{
control: { control: {
border: `1px solid ${colors['blue-button']}`, border: `1px solid ${colors['blue-button']}`,
transition: 'all 0.3s ease',
'&:hover': { backgroundColor: colors['blue-button'], color: 'white' },
}, },
}} }}
/> />
</Center> </Center>

View File

@@ -7,16 +7,18 @@ import { IconCalendar, IconInfoCircle, IconPhone, IconSearch } from "@tabler/ico
import { useState } from "react"; import { useState } from "react";
import { useProxy } from "valtio/utils"; import { useProxy } from "valtio/utils";
import BackButton from "../../desa/layanan/_com/BackButto"; import BackButton from "../../desa/layanan/_com/BackButto";
import { useDebouncedValue } from "@mantine/hooks";
export default function Page() { export default function Page() {
const state = useProxy(posyandustate); const state = useProxy(posyandustate);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const { data, page, totalPages, loading, load } = state.findMany; const { data, page, totalPages, loading, load } = state.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 6, search); load(page, 6, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
if (loading || !data) { if (loading || !data) {
return ( return (
@@ -28,11 +30,31 @@ export default function Page() {
if (data.length === 0) { if (data.length === 0) {
return ( return (
<Box py="xl" px={{ base: "md", md: 100 }}> <Stack pos="relative" bg={colors.Bg} py="xl" gap="xl">
<Text fz="lg" fw="bold" c={colors["blue-button"]}> <Box px={{ base: "md", md: 100 }}>
Tidak ada posyandu yang ditemukan <BackButton />
<Flex mt="md" justify="space-between" align="center" wrap="wrap" gap="md">
<Text
ta="left"
fz={{ base: "1.8rem", md: "2.5rem" }}
c={colors["blue-button"]}
fw="bold"
>
Posyandu Desa Darmasaba
</Text> </Text>
<TextInput
placeholder="Cari posyandu berdasarkan nama..."
aria-label="Pencarian Posyandu"
radius="xl"
size="md"
leftSection={<IconSearch size={20} />}
w={{ base: "100%", md: "35%" }}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
</Flex>
</Box> </Box>
</Stack>
); );
} }

View File

@@ -78,9 +78,7 @@ function Page() {
<Tooltip label={materi.data?.judul} position="top" withArrow> <Tooltip label={materi.data?.judul} position="top" withArrow>
<Stack gap={4} align="center"> <Stack gap={4} align="center">
<IconRecycle size={28} color={colors['blue-button']} /> <IconRecycle size={28} color={colors['blue-button']} />
<Text fz="h3" fw="bold" c={colors['blue-button']} ta="center"> <Text fz="h3" fw="bold" c={colors['blue-button']} ta="center" dangerouslySetInnerHTML={{ __html: materi.data?.judul || '' }} />
{materi.data?.judul}
</Text>
</Stack> </Stack>
</Tooltip> </Tooltip>
</Box> </Box>

View File

@@ -1,5 +1,9 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import { motion, AnimatePresence } from 'framer-motion';
import { Transition } from '@mantine/core';
import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong'; import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong';
import { import {
Badge, Badge,
@@ -23,12 +27,11 @@ import {
} from '@mantine/core'; } from '@mantine/core';
import { IconArrowRight, IconCalendar } from '@tabler/icons-react'; import { IconArrowRight, IconCalendar } from '@tabler/icons-react';
import { useTransitionRouter } from 'next-view-transitions'; import { useTransitionRouter } from 'next-view-transitions';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
export default function Content({ kategori }: { kategori: string }) { export default function Content({ kategori }: { kategori: string }) {
const router = useTransitionRouter(); const router = useTransitionRouter();
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [animateKey, setAnimateKey] = useState(0);
const state = useProxy(gotongRoyongState.kegiatanDesa); const state = useProxy(gotongRoyongState.kegiatanDesa);
const featuredState = useProxy(gotongRoyongState.kegiatanDesa.findFirst); const featuredState = useProxy(gotongRoyongState.kegiatanDesa.findFirst);
@@ -37,29 +40,44 @@ export default function Content({ kategori }: { kategori: string }) {
const paginatedNews = state.findMany.data || []; const paginatedNews = state.findMany.data || [];
const totalPages = state.findMany.totalPages || 1; const totalPages = state.findMany.totalPages || 1;
// Load data // Load data awal
useEffect(() => { useEffect(() => {
gotongRoyongState.kegiatanDesa.findFirst.load(kategori); gotongRoyongState.kegiatanDesa.findFirst.load(kategori);
}, [kategori]); }, [kategori]);
// Load daftar berita
useEffect(() => { useEffect(() => {
state.findMany.load(page, 3, '', kategori); state.findMany.load(page, 3, '', kategori);
setAnimateKey((prev) => prev + 1); // trigger animasi halus saat page berubah
}, [page, kategori]); }, [page, kategori]);
// Tampilan kosong
if (!featuredState.loading && !featured) {
return (
<Center py={100}>
<Stack align="center" gap="sm">
<Title order={3}>Belum Ada Data Gotong Royong</Title>
<Text c="dimmed">Tidak ada data gotong royong yang tersedia saat ini.</Text>
</Stack>
</Center>
);
}
return ( return (
<Box py={20}> <Box py={20}>
<Container size="xl" px={{ base: 'md', md: 'xl' }}> <Container size="xl" px={{ base: 'md', md: 'xl' }}>
{/* === Gotong Royong Utama === */} {/* === Gotong Royong Utama === */}
{featuredState.loading ? ( <Transition mounted={!featuredState.loading} transition="fade" duration={250} timingFunction="ease">
<Center><Skeleton h={400} /></Center> {(styles) => (
) : featured ? ( <div style={styles}>
{featured ? (
<Box mb={50}> <Box mb={50}>
<Text fz="h2" fw={700} mb="md">Gotong Royong Utama</Text> <Text fz="h2" fw={700} mb="md">Gotong Royong Utama</Text>
<Paper shadow="md" radius="md" withBorder> <Paper shadow="md" radius="md" withBorder>
<Grid gutter={0}> <Grid gutter={0}>
<GridCol span={{ base: 12, md: 6 }}> <GridCol span={{ base: 12, md: 6 }}>
<Image <Image
src={featured.image?.link} src={featured.image?.link || '/images/placeholder.jpg'}
alt={featured.judul || 'Berita Utama'} alt={featured.judul || 'Berita Utama'}
height={400} height={400}
fit="cover" fit="cover"
@@ -75,7 +93,12 @@ export default function Content({ kategori }: { kategori: string }) {
{featured.kategoriKegiatan?.nama || kategori} {featured.kategoriKegiatan?.nama || kategori}
</Badge> </Badge>
<Title order={2} mb="md">{featured.judul}</Title> <Title order={2} mb="md">{featured.judul}</Title>
<Text c="dimmed" lineClamp={3} mb="md" dangerouslySetInnerHTML={{ __html: featured.deskripsiLengkap }} /> <Text
c="dimmed"
lineClamp={3}
mb="md"
dangerouslySetInnerHTML={{ __html: featured.deskripsiLengkap }}
/>
</div> </div>
<Group justify="apart" mt="auto"> <Group justify="apart" mt="auto">
<Group gap="xs"> <Group gap="xs">
@@ -91,7 +114,9 @@ export default function Content({ kategori }: { kategori: string }) {
<Button <Button
variant="light" variant="light"
rightSection={<IconArrowRight size={16} />} rightSection={<IconArrowRight size={16} />}
onClick={() => router.push(`/darmasaba/lingkungan/gotong-royong/${kategori}/${featured.id}`)} onClick={() =>
router.push(`/darmasaba/lingkungan/gotong-royong/${kategori}/${featured.id}`)
}
> >
Baca Selengkapnya Baca Selengkapnya
</Button> </Button>
@@ -101,21 +126,41 @@ export default function Content({ kategori }: { kategori: string }) {
</Grid> </Grid>
</Paper> </Paper>
</Box> </Box>
) : null} ) : (
<Skeleton h={400} radius="md" />
)}
</div>
)}
</Transition>
{/* === Daftar Gotong Royong === */} {/* === Daftar Gotong Royong (Pagination + Fade-in Halus) === */}
<Box mt={50}> <Box mt={50}>
<Title order={2} mb="md">Daftar Gotong Royong</Title> <Title order={2} mb="md">Daftar Gotong Royong</Title>
<Divider mb="xl" /> <Divider mb="xl" />
<AnimatePresence mode="wait">
<motion.div
key={animateKey}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.25, ease: 'easeInOut' }}
>
{state.findMany.loading ? ( {state.findMany.loading ? (
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl"> <SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl">
{Array(3).fill(0).map((_, i) => ( {Array(3)
.fill(0)
.map((_, i) => (
<Skeleton key={i} h={300} radius="md" /> <Skeleton key={i} h={300} radius="md" />
))} ))}
</SimpleGrid> </SimpleGrid>
) : paginatedNews.length === 0 ? ( ) : paginatedNews.length === 0 ? (
<Text c="dimmed" ta="center">Belum ada gotong royong di kategori &quot;{kategori}&quot;.</Text> <Center py={50}>
<Stack align="center" gap="sm">
<Title order={3}>Tidak Ada Data</Title>
<Text c="dimmed">Belum ada data gotong royong yang tersedia.</Text>
</Stack>
</Center>
) : ( ) : (
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl" verticalSpacing="xl"> <SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl" verticalSpacing="xl">
{paginatedNews.map((item) => ( {paginatedNews.map((item) => (
@@ -129,13 +174,28 @@ export default function Content({ kategori }: { kategori: string }) {
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
> >
<Card.Section> <Card.Section>
<Image src={item.image?.link} height={200} alt={item.judul} fit="cover" loading="lazy"/> <Image
src={item.image?.link || '/images/placeholder-small.jpg'}
height={200}
alt={item.judul}
fit="cover"
loading="lazy"
/>
</Card.Section> </Card.Section>
<Badge color="blue" variant="light" mt="md"> <Badge color="blue" variant="light" mt="md">
{item.kategoriKegiatan?.nama || kategori} {item.kategoriKegiatan?.nama || kategori}
</Badge> </Badge>
<Text fw={600} size="lg" mt="sm" lineClamp={2}>{item.judul}</Text> <Text fw={600} size="lg" mt="sm" lineClamp={2}>
<Text size="sm" c="dimmed" lineClamp={3} style={{wordBreak: "break-word", whiteSpace: "normal"}} mt="xs" dangerouslySetInnerHTML={{ __html: item.deskripsiLengkap }} /> {item.judul}
</Text>
<Text
size="sm"
c="dimmed"
lineClamp={3}
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
mt="xs"
dangerouslySetInnerHTML={{ __html: item.deskripsiLengkap }}
/>
<Group justify="apart" mt="md" gap="xs"> <Group justify="apart" mt="md" gap="xs">
<Text size="xs" c="dimmed"> <Text size="xs" c="dimmed">
{new Date(item.createdAt).toLocaleDateString('id-ID', { {new Date(item.createdAt).toLocaleDateString('id-ID', {
@@ -150,6 +210,8 @@ export default function Content({ kategori }: { kategori: string }) {
))} ))}
</SimpleGrid> </SimpleGrid>
)} )}
</motion.div>
</AnimatePresence>
{/* Pagination */} {/* Pagination */}
<Center mt="xl"> <Center mt="xl">

View File

@@ -1,47 +1,64 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client';
import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong'; import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong';
import { Badge, Box, Button, Card, Center, Container, Divider, Flex, Grid, GridCol, Group, Image, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, Title } from '@mantine/core'; import {
Badge,
Box,
Button,
Card,
Center,
Container,
Divider,
Flex,
Grid,
GridCol,
Group,
Image,
Pagination,
Paper,
SimpleGrid,
Skeleton,
Stack,
Text,
Title,
Transition,
} from '@mantine/core';
import { IconArrowRight, IconCalendar } from '@tabler/icons-react'; import { IconArrowRight, IconCalendar } from '@tabler/icons-react';
import { useTransitionRouter } from 'next-view-transitions'; import { motion } from 'framer-motion';
import { useSearchParams } from 'next/navigation'; import { useRouter, useSearchParams } from 'next/navigation';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function Page() { export default function Page() {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const router = useTransitionRouter(); const router = useRouter();
// Parameter URL
const search = searchParams.get('search') || ''; const search = searchParams.get('search') || '';
const page = parseInt(searchParams.get('page') || '1'); const page = parseInt(searchParams.get('page') || '1');
// Gunakan proxy untuk state
const state = useProxy(gotongRoyongState.kegiatanDesa); const state = useProxy(gotongRoyongState.kegiatanDesa);
const featured = useProxy(gotongRoyongState.kegiatanDesa.findFirst); // ✅ Berita utama const featured = useProxy(gotongRoyongState.kegiatanDesa.findFirst);
const loadingGrid = state.findMany.loading; const loadingGrid = state.findMany.loading;
const loadingFeatured = featured.loading; const loadingFeatured = featured.loading;
// Load berita utama (hanya sekali)
useEffect(() => { useEffect(() => {
if (!featured.data && !loadingFeatured) { if (!featured.data && !loadingFeatured) {
gotongRoyongState.kegiatanDesa.findFirst.load(); gotongRoyongState.kegiatanDesa.findFirst.load();
} }
}, [featured.data, loadingFeatured]); }, [featured.data, loadingFeatured]);
// Load berita terbaru (untuk grid) saat page/search berubah
useEffect(() => { useEffect(() => {
const limit = 3; // Sesuaikan dengan tampilan grid const limit = 3;
state.findMany.load(page, limit, search); state.findMany.load(page, limit, search);
}, [page, search]); }, [page, search]);
// Update URL saat page berubah
const handlePageChange = (newPage: number) => { const handlePageChange = (newPage: number) => {
const url = new URLSearchParams(searchParams.toString()); const url = new URLSearchParams(searchParams.toString());
if (search) url.set('search', search); if (search) url.set('search', search);
if (newPage > 1) url.set('page', newPage.toString()); if (newPage > 1) url.set('page', newPage.toString());
else url.delete('page'); // biar page=1 ga muncul di URL else url.delete('page');
router.replace(`?${url.toString()}`); router.replace(`?${url.toString()}`);
}; };
@@ -49,14 +66,34 @@ function Page() {
const paginatedNews = state.findMany.data || []; const paginatedNews = state.findMany.data || [];
const totalPages = state.findMany.totalPages || 1; const totalPages = state.findMany.totalPages || 1;
// Animasi transisi halus tapi tetap instant load
const MotionBox = motion(Box as any);
// fallback kosong
if (!loadingGrid && !loadingFeatured && paginatedNews.length === 0) {
return ( return (
<Box py={20}> <Container size="xl" py={80} ta="center">
<Container size="xl" px={{ base: "md", md: "xl" }}> <Title order={2} mb="md">Belum Ada Data Gotong Royong</Title>
{/* === Gotong royong Utama (Tetap) === */} <Text c="dimmed">Tidak ada data gotong royong yang tersedia saat ini.</Text>
{loadingFeatured ? ( </Container>
<Center><Skeleton h={400} /></Center> );
) : featuredData ? ( }
<Box mb={50}>
return (
<MotionBox
key={`${page}-${search}`}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2, ease: 'easeOut' }}
py={20}
>
<Container size="xl" px={{ base: 'md', md: 'xl' }}>
{/* === Gotong Royong Utama === */}
<Transition mounted={!loadingFeatured} transition="fade" duration={200} timingFunction="ease-out">
{(styles) =>
featuredData ? (
<Box mb={50} style={styles}>
<Text fz="h2" fw={700} mb="md">Gotong royong Utama</Text> <Text fz="h2" fw={700} mb="md">Gotong royong Utama</Text>
<Paper shadow="md" radius="md" withBorder> <Paper shadow="md" radius="md" withBorder>
<Grid gutter={0}> <Grid gutter={0}>
@@ -78,7 +115,12 @@ function Page() {
{featuredData.kategoriKegiatan?.nama || 'Gotong royong'} {featuredData.kategoriKegiatan?.nama || 'Gotong royong'}
</Badge> </Badge>
<Title order={2} mb="md">{featuredData.judul}</Title> <Title order={2} mb="md">{featuredData.judul}</Title>
<Text c="dimmed" lineClamp={3} mb="md" dangerouslySetInnerHTML={{ __html: featuredData.deskripsiSingkat }} /> <Text
c="dimmed"
lineClamp={3}
mb="md"
dangerouslySetInnerHTML={{ __html: featuredData.deskripsiSingkat }}
/>
</div> </div>
<Group justify="apart" mt="auto"> <Group justify="apart" mt="auto">
<Group gap="xs"> <Group gap="xs">
@@ -87,14 +129,18 @@ function Page() {
{new Date(featuredData.createdAt).toLocaleDateString('id-ID', { {new Date(featuredData.createdAt).toLocaleDateString('id-ID', {
day: 'numeric', day: 'numeric',
month: 'long', month: 'long',
year: 'numeric' year: 'numeric',
})} })}
</Text> </Text>
</Group> </Group>
<Button <Button
variant="light" variant="light"
rightSection={<IconArrowRight size={16} />} rightSection={<IconArrowRight size={16} />}
onClick={() => router.push(`/darmasaba/lingkungan/gotong-royong/${featuredData.kategoriKegiatan?.nama}/${featuredData.id}`)} onClick={() =>
router.push(
`/darmasaba/lingkungan/gotong-royong/${featuredData.kategoriKegiatan?.nama}/${featuredData.id}`
)
}
> >
Baca Selengkapnya Baca Selengkapnya
</Button> </Button>
@@ -104,31 +150,36 @@ function Page() {
</Grid> </Grid>
</Paper> </Paper>
</Box> </Box>
) : null} ) : (
<Skeleton h={400} radius="md" mb="xl" />
)
}
</Transition>
{/* === Gotong royong Terbaru (Berubah Saat Pagination) === */} {/* === Gotong royong Terbaru === */}
<Box mt={50}> <Box mt={50}>
<Title order={2} mb="md">Gotong royong Terbaru</Title> <Title order={2} mb="md">Gotong royong Terbaru</Title>
<Divider mb="xl" /> <Divider mb="xl" />
{loadingGrid ? ( <Transition mounted={!loadingGrid} transition="fade" duration={200} timingFunction="ease-out">
{(styles) =>
loadingGrid ? (
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl"> <SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl">
{Array(3).fill(0).map((_, i) => ( {Array(3)
.fill(0)
.map((_, i) => (
<Skeleton key={i} h={300} radius="md" /> <Skeleton key={i} h={300} radius="md" />
))} ))}
</SimpleGrid> </SimpleGrid>
) : paginatedNews.length === 0 ? ( ) : paginatedNews.length === 0 ? (
<Text c="dimmed" ta="center">Tidak ada gotong royong ditemukan.</Text> <Text c="dimmed" ta="center">
Tidak ada gotong royong ditemukan.
</Text>
) : ( ) : (
<Box style={styles}>
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl" verticalSpacing="xl"> <SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl" verticalSpacing="xl">
{paginatedNews.map((item) => ( {paginatedNews.map((item) => (
<Card <Card key={item.id} shadow="sm" p="lg" radius="md" withBorder>
key={item.id}
shadow="sm"
p="lg"
radius="md"
withBorder
>
<Card.Section> <Card.Section>
<Image <Image
src={item.image?.link || '/images/placeholder-small.jpg'} src={item.image?.link || '/images/placeholder-small.jpg'}
@@ -143,27 +194,49 @@ function Page() {
{item.kategoriKegiatan?.nama || 'Gotong royong'} {item.kategoriKegiatan?.nama || 'Gotong royong'}
</Badge> </Badge>
<Text fw={600} size="lg" mt="sm" lineClamp={2}>{item.judul}</Text> <Text fw={600} size="lg" mt="sm" lineClamp={2}>
{item.judul}
</Text>
<Text size="sm" c="dimmed" lineClamp={3} mt="xs" dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat }} /> <Text
size="sm"
c="dimmed"
lineClamp={3}
mt="xs"
dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat }}
/>
<Flex align="center" justify="apart" mt="md" gap="xs"> <Flex align="center" justify="apart" mt="md" gap="xs">
<Text size="xs" c="dimmed"> <Text size="xs" c="dimmed">
{new Date(item.createdAt).toLocaleDateString('id-ID', { {new Date(item.createdAt).toLocaleDateString('id-ID', {
day: 'numeric', day: 'numeric',
month: 'short', month: 'short',
year: 'numeric' year: 'numeric',
})} })}
</Text> </Text>
<Button p="xs" variant="light" rightSection={<IconArrowRight size={16} />} onClick={() => router.push(`/darmasaba/lingkungan/gotong-royong/${item.kategoriKegiatan?.nama}/${item.id}`)}>Baca Selengkapnya</Button> <Button
p="xs"
variant="light"
rightSection={<IconArrowRight size={16} />}
onClick={() =>
router.push(
`/darmasaba/lingkungan/gotong-royong/${item.kategoriKegiatan?.nama}/${item.id}`
)
}
>
Baca Selengkapnya
</Button>
</Flex> </Flex>
</Card> </Card>
))} ))}
</SimpleGrid> </SimpleGrid>
)} </Box>
)
}
</Transition>
{/* Pagination hanya untuk berita terbaru */} {/* Pagination */}
<Center mt="xl"> <Center mt="xl">
<Pagination <Pagination
total={totalPages} total={totalPages}
@@ -176,9 +249,6 @@ function Page() {
</Center> </Center>
</Box> </Box>
</Container> </Container>
</Box> </MotionBox>
); );
} }
export default Page;

View File

@@ -1,7 +1,7 @@
'use client' 'use client'
import stateBimbinganBelajarDesa from '@/app/admin/(dashboard)/_state/pendidikan/bimbingan-belajar-desa'; import stateBimbinganBelajarDesa from '@/app/admin/(dashboard)/_state/pendidikan/bimbingan-belajar-desa';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Paper, SimpleGrid, Skeleton, Stack, Text, Title, Tooltip, Divider, Badge } from '@mantine/core'; import { Box, Paper, SimpleGrid, Skeleton, Stack, Text, Title, Tooltip, Divider, Badge, Group } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { IconMapPin, IconCalendarTime, IconBook2 } from '@tabler/icons-react'; import { IconMapPin, IconCalendarTime, IconBook2 } from '@tabler/icons-react';
@@ -49,46 +49,46 @@ function Page() {
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="xl"> <SimpleGrid cols={{ base: 1, md: 3 }} spacing="xl">
<Paper p="xl" radius="lg" shadow="md" withBorder bg={colors['white-trans-1']}> <Paper p="xl" radius="lg" shadow="md" withBorder bg={colors['white-trans-1']}>
<Stack gap="sm"> <Stack gap="sm">
<Box> <Group>
<Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
{stateTujuanProgram.findById.data?.judul}
</Badge>
<Tooltip label="Gambaran manfaat utama program" position="top-start" withArrow> <Tooltip label="Gambaran manfaat utama program" position="top-start" withArrow>
<Box> <Box>
<IconBook2 size={36} stroke={1.5} color={colors['blue-button']} /> <IconBook2 size={36} stroke={1.5} color={colors['blue-button']} />
</Box> </Box>
</Tooltip> </Tooltip>
</Box> <Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
{stateTujuanProgram.findById.data?.judul}
</Badge>
</Group>
<Text fz="md" style={{wordBreak: "break-word", whiteSpace: "normal"}} lh={1.6} dangerouslySetInnerHTML={{ __html: stateTujuanProgram.findById.data?.deskripsi }} /> <Text fz="md" style={{wordBreak: "break-word", whiteSpace: "normal"}} lh={1.6} dangerouslySetInnerHTML={{ __html: stateTujuanProgram.findById.data?.deskripsi }} />
</Stack> </Stack>
</Paper> </Paper>
<Paper p="xl" radius="lg" shadow="md" withBorder bg={colors['white-trans-1']}> <Paper p="xl" radius="lg" shadow="md" withBorder bg={colors['white-trans-1']}>
<Stack gap="sm"> <Stack gap="sm">
<Box> <Group>
<Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
{stateLokasiDanJadwal.findById.data?.judul}
</Badge>
<Tooltip label="Tempat dan waktu pelaksanaan" position="top-start" withArrow> <Tooltip label="Tempat dan waktu pelaksanaan" position="top-start" withArrow>
<Box> <Box>
<IconMapPin size={36} stroke={1.5} color={colors['blue-button']} /> <IconMapPin size={36} stroke={1.5} color={colors['blue-button']} />
</Box> </Box>
</Tooltip> </Tooltip>
</Box> <Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
{stateLokasiDanJadwal.findById.data?.judul}
</Badge>
</Group>
<Text fz="md" lh={1.6} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: stateLokasiDanJadwal.findById.data?.deskripsi }} /> <Text fz="md" lh={1.6} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: stateLokasiDanJadwal.findById.data?.deskripsi }} />
</Stack> </Stack>
</Paper> </Paper>
<Paper p="xl" radius="lg" shadow="md" withBorder bg={colors['white-trans-1']}> <Paper p="xl" radius="lg" shadow="md" withBorder bg={colors['white-trans-1']}>
<Stack gap="sm"> <Stack gap="sm">
<Box> <Group>
<Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
{stateFasilitas.findById.data?.judul}
</Badge>
<Tooltip label="Sarana yang disediakan untuk peserta" position="top-start" withArrow> <Tooltip label="Sarana yang disediakan untuk peserta" position="top-start" withArrow>
<Box> <Box>
<IconCalendarTime size={36} stroke={1.5} color={colors['blue-button']} /> <IconCalendarTime size={36} stroke={1.5} color={colors['blue-button']} />
</Box> </Box>
</Tooltip> </Tooltip>
</Box> <Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
{stateFasilitas.findById.data?.judul}
</Badge>
</Group>
<Text fz="md" lh={1.6} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: stateFasilitas.findById.data?.deskripsi }} /> <Text fz="md" lh={1.6} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: stateFasilitas.findById.data?.deskripsi }} />
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -56,7 +56,7 @@ export default function Content() {
try { try {
await state.dataPerpustakaan.findMany.load( await state.dataPerpustakaan.findMany.load(
currentPage, currentPage,
10, 3,
searchQuery, searchQuery,
'' ''
); );

View File

@@ -12,10 +12,9 @@ import {
SimpleGrid, SimpleGrid,
Stack, Stack,
Text, Text,
TextInput, TextInput
Tooltip,
} from '@mantine/core'; } from '@mantine/core';
import { IconDownload, IconSend2 } from '@tabler/icons-react'; import { IconSend2 } 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 BackButton from '../../desa/layanan/_com/BackButto'; import BackButton from '../../desa/layanan/_com/BackButto';
@@ -150,23 +149,6 @@ function Page() {
</Paper> </Paper>
))} ))}
</SimpleGrid> </SimpleGrid>
<Center pb={30}>
<Tooltip label="Unduh dokumen tata cara permohonan" withArrow>
<Button
fz="sm"
size="md"
radius="md"
bg={colors['blue-button']}
leftSection={
<IconDownload size={20} color={colors['white-1']} />
}
>
Unduh Tata Cara
</Button>
</Tooltip>
</Center>
<Group justify="center"> <Group justify="center">
<Paper <Paper
p="xl" p="xl"

View File

@@ -6,6 +6,7 @@ import { useShallowEffect } from '@mantine/hooks';
import { IconBuildingCommunity, IconTargetArrow, IconTimeline, IconUser } from '@tabler/icons-react'; import { IconBuildingCommunity, IconTargetArrow, IconTimeline, IconUser } from '@tabler/icons-react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto'; import BackButton from '../../desa/layanan/_com/BackButto';
import ScrollToTopButton from '@/app/darmasaba/_com/scrollToTopButton';
function Page() { function Page() {
const allList = useProxy(stateProfilePPID) const allList = useProxy(stateProfilePPID)
@@ -36,6 +37,7 @@ function Page() {
: [allList.profile.data] : [allList.profile.data]
return ( return (
<Box>
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22"> <Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
<BackButton /> <BackButton />
@@ -129,6 +131,9 @@ function Page() {
</Box> </Box>
))} ))}
</Stack> </Stack>
{/* Tombol Scroll ke Atas */}
<ScrollToTopButton />
</Box>
) )
} }

View File

@@ -0,0 +1,157 @@
'use client';
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID';
import colors from '@/con/colors';
import {
Box,
Divider,
Group,
Image,
Paper,
Skeleton,
Stack,
Text,
Title,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
function DetailPegawaiUser() {
const statePegawai = useProxy(stateStrukturPPID.pegawai);
const params = useParams();
const router = useRouter();
useShallowEffect(() => {
stateStrukturPPID.posisiOrganisasi.findMany.load();
statePegawai.findUnique.load(params?.id as string);
}, []);
if (!statePegawai.findUnique.data) {
return (
<Stack py="lg">
<Skeleton height={500} radius="md" />
</Stack>
);
}
const data = statePegawai.findUnique.data;
return (
<Box px={{ base: 'sm', md: 'lg' }} py="xl">
{/* Back button */}
<Group mb="lg">
<Box
onClick={() => router.back()}
style={{
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: 8,
}}
>
<IconArrowBack size={22} color={colors['blue-button']} />
<Text c={colors['blue-button']} fw={500}>
Kembali
</Text>
</Box>
</Group>
<Paper
w={{ base: '100%', md: '70%' }}
mx="auto"
p="xl"
radius="lg"
shadow="sm"
bg="white"
style={{
border: '1px solid #eaeaea',
}}
>
<Stack align="center" gap="md">
{/* Foto Profil */}
<Image
src={data.image?.link || '/placeholder-profile.png'}
alt={data.namaLengkap || 'Foto Profil'}
w={160}
h={160}
radius={100}
fit="cover"
style={{ border: `2px solid ${colors['blue-button']}` }}
loading="lazy"
/>
{/* Nama & Jabatan */}
<Stack align="center" gap={2}>
<Title order={3} fw={700} c={colors['blue-button']}>
{data.namaLengkap || '-'} {data.gelarAkademik || ''}
</Title>
<Text fz="sm" c="dimmed">
{data.posisi?.nama || 'Posisi tidak tersedia'}
</Text>
</Stack>
</Stack>
<Divider my="lg" />
{/* Informasi Detail */}
<Stack gap="md">
<InfoRow label="Email" value={data.email} />
<InfoRow label="Telepon" value={data.telepon} />
<InfoRow label="Alamat" value={data.alamat} multiline />
<InfoRow
label="Tanggal Masuk"
value={
data.tanggalMasuk
? new Date(data.tanggalMasuk).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric',
})
: '-'
}
/>
<InfoRow
label="Status"
value={data.isActive ? 'Aktif' : 'Tidak Aktif'}
valueColor={data.isActive ? 'green' : 'red'}
/>
</Stack>
</Paper>
</Box>
);
}
/* Komponen kecil untuk menampilkan baris informasi */
function InfoRow({
label,
value,
valueColor,
multiline = false,
}: {
label: string;
value?: string | null;
valueColor?: string;
multiline?: boolean;
}) {
return (
<Box>
<Text fz="sm" fw={600} c="dark">
{label}
</Text>
<Text
fz="sm"
c={valueColor || 'dimmed'}
style={{
whiteSpace: multiline ? 'normal' : 'nowrap',
wordBreak: 'break-word',
}}
>
{value || '-'}
</Text>
</Box>
);
}
export default DetailPegawaiUser;

View File

@@ -1,129 +1,9 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
// /* eslint-disable react-hooks/exhaustive-deps */
// /* eslint-disable @typescript-eslint/no-explicit-any */
// 'use client'
// import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID';
// import colors from '@/con/colors';
// import { Box, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
// import { OrganizationChart } from 'primereact/organizationchart';
// import { useEffect } from 'react';
// import { useProxy } from 'valtio/utils';
// import BackButton from '../../desa/layanan/_com/BackButto';
// function Page() {
// return (
// <Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
// <Box px={{ base: 'md', md: 100 }}>
// <BackButton />
// </Box>
// <Title ta={"center"} fz={{ base: "2rem", md: "2.5rem", lg: "3rem", xl: "3.5rem" }} c={colors["blue-button"]} fw={"bold"}>Struktur PPID</Title>
// <StrukturOrganisasiPPID />
// </Stack>
// );
// }
// function StrukturOrganisasiPPID() {
// const stateOrganisasi = useProxy(stateStrukturPPID.pegawai)
// useEffect(() => {
// stateOrganisasi.findMany.load()
// }, [])
// if (!stateOrganisasi.findMany.data || stateOrganisasi.findMany.data.length === 0) {
// return (
// <Stack py={10}>
// <Skeleton h={500} />
// </Stack>
// );
// }
// // Step 1: Group pegawai berdasarkan posisiId
// const posisiMap = new Map<string, any>();
// for (const pegawai of stateOrganisasi.findMany.data) {
// const posisiId = pegawai.posisi.id;
// if (!posisiMap.has(posisiId)) {
// posisiMap.set(posisiId, {
// ...pegawai.posisi,
// pegawaiList: [],
// children: []
// });
// }
// posisiMap.get(posisiId)!.pegawaiList.push(pegawai);
// }
// // Step 2: Buat struktur pohon berdasarkan parentId
// const root: any[] = [];
// posisiMap.forEach((posisi) => {
// if (posisi.parentId) {
// const parent = posisiMap.get(posisi.parentId);
// if (parent) {
// parent.children.push(posisi);
// }
// } else {
// root.push(posisi);
// }
// });
// // Step 3: Ubah struktur ke format OrganizationChart
// function toOrgChartFormat(node: any): any {
// return {
// expanded: true,
// type: 'person',
// styleClass: 'p-person',
// data: {
// name: node.pegawaiList?.[0]?.namaLengkap || 'Tidak ada pegawai',
// status: node.nama,
// image: node.pegawaiList?.[0]?.image?.link || '/img/default.png'
// },
// children: node.children.map(toOrgChartFormat)
// };
// }
// const chartData = root.map(toOrgChartFormat);
// return (
// <Box py={10}>
// <Paper bg={colors.grey} p="md" style={{ overflowX: 'auto' }}>
// <OrganizationChart style={{ color: colors['blue-button'] }} value={chartData} nodeTemplate={nodeTemplate} />
// </Paper>
// </Box>
// );
// }
// function nodeTemplate(node: any) {
// const imageSrc = node?.data?.image || '/img/default.png';
// const name = node?.data?.name || 'Tanpa Nama';
// const status = node?.data?.status || 'Tidak ada deskripsi';
// return (
// <Stack pos={"relative"} py={"xl"} gap={"22"}>
// <Stack align="center" gap={4}>
// <Image
// src={imageSrc}
// alt={name}
// radius="xl"
// w={120}
// h={120}
// fit="cover"
// />
// <Text fw={600} ta="center">{name}</Text>
// <Text size="sm" c="dimmed" ta="center">{status}</Text>
// </Stack>
// </Stack>
// );
// }
// export default Page;
'use client' 'use client'
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID' import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID'
import colors from '@/con/colors'
import { import {
Box, Box,
Button, Button,
@@ -136,16 +16,27 @@ import {
Paper, Paper,
Stack, Stack,
Text, Text,
TextInput,
Title, Title,
Tooltip, Tooltip,
Transition, Transition,
} from '@mantine/core' } from '@mantine/core'
import { IconRefresh, IconSearch, IconUsers } from '@tabler/icons-react' import {
IconRefresh,
IconSearch,
IconUsers,
IconZoomIn,
IconZoomOut,
IconArrowsMaximize,
IconArrowsMinimize,
} from '@tabler/icons-react'
import { OrganizationChart } from 'primereact/organizationchart' import { OrganizationChart } from 'primereact/organizationchart'
import { useEffect } from 'react' import { useEffect, useRef, useState } from 'react'
import { useProxy } from 'valtio/utils' import { useProxy } from 'valtio/utils'
import BackButton from '../../desa/layanan/_com/BackButto' import BackButton from '../../desa/layanan/_com/BackButto'
import colors from '@/con/colors' import { useTransitionRouter } from 'next-view-transitions'
import ScrollToTopButton from '@/app/darmasaba/_com/scrollToTopButton'
import { debounce } from 'lodash'
export default function Page() { export default function Page() {
return ( return (
@@ -167,7 +58,6 @@ export default function Page() {
ta="center" ta="center"
c={colors['blue-button']} c={colors['blue-button']}
fz={{ base: 28, md: 36, lg: 44 }} fz={{ base: 28, md: 36, lg: 44 }}
> >
Struktur Organisasi PPID Struktur Organisasi PPID
</Title> </Title>
@@ -180,20 +70,34 @@ export default function Page() {
<StrukturOrganisasiPPID /> <StrukturOrganisasiPPID />
</Box> </Box>
</Container> </Container>
{/* Tombol Scroll ke Atas */}
<ScrollToTopButton />
</Box> </Box>
) )
} }
function StrukturOrganisasiPPID() { function StrukturOrganisasiPPID() {
const stateOrganisasi: any = useProxy(stateStrukturPPID.pegawai) const stateOrganisasi: any = useProxy(stateStrukturPPID.pegawai)
const router = useTransitionRouter()
const chartContainerRef = useRef<HTMLDivElement>(null)
const [scale, setScale] = useState(1)
const [isFullscreen, setFullscreen] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
// debounce untuk pencarian
const debouncedSearch = useRef(
debounce((value: string) => {
setSearchQuery(value)
}, 400)
).current
useEffect(() => { useEffect(() => {
void stateOrganisasi.findMany.load() void stateOrganisasi.findMany.load()
}, []) }, [])
const isLoading = const isLoading =
!stateOrganisasi.findMany.data && !stateOrganisasi.findMany.data && stateOrganisasi.findMany.loading !== false
stateOrganisasi.findMany.loading !== false
if (isLoading) { if (isLoading) {
return ( return (
@@ -209,10 +113,7 @@ function StrukturOrganisasiPPID() {
) )
} }
if ( if (!stateOrganisasi.findMany.data || stateOrganisasi.findMany.data.length === 0) {
!stateOrganisasi.findMany.data ||
stateOrganisasi.findMany.data.length === 0
) {
return ( return (
<Center py={40}> <Center py={40}>
<Stack align="center" gap="md"> <Stack align="center" gap="md">
@@ -233,8 +134,7 @@ function StrukturOrganisasiPPID() {
Data pegawai belum tersedia Data pegawai belum tersedia
</Title> </Title>
<Text c="dimmed" mt="xs"> <Text c="dimmed" mt="xs">
Belum ada data pegawai yang tercatat untuk PPID. Silakan coba Belum ada data pegawai yang tercatat untuk PPID.
muat ulang atau periksa sumber data.
</Text> </Text>
<Group justify="center" mt="lg"> <Group justify="center" mt="lg">
<Button <Button
@@ -245,15 +145,6 @@ function StrukturOrganisasiPPID() {
> >
Muat Ulang Muat Ulang
</Button> </Button>
<Button
leftSection={<IconSearch size={16} />}
variant="subtle"
onClick={() =>
stateOrganisasi.findMany.load({ query: { q: '' } })
}
>
Cari Pegawai
</Button>
</Group> </Group>
</Paper> </Paper>
</Stack> </Stack>
@@ -261,45 +152,42 @@ function StrukturOrganisasiPPID() {
) )
} }
// Buat struktur organisasi
const posisiMap = new Map<string, any>() const posisiMap = new Map<string, any>()
const aktifPegawai = stateOrganisasi.findMany.data.filter((p: any) => p.isActive)
const aktifPegawai = stateOrganisasi.findMany.data.filter((p: any) => p.isActive);
for (const pegawai of aktifPegawai) { for (const pegawai of aktifPegawai) {
const posisiId = pegawai.posisi.id; const posisiId = pegawai.posisi.id
if (!posisiMap.has(posisiId)) { if (!posisiMap.has(posisiId)) {
posisiMap.set(posisiId, { posisiMap.set(posisiId, {
...pegawai.posisi, ...pegawai.posisi,
pegawaiList: [], pegawaiList: [],
children: [], children: [],
}); })
} }
posisiMap.get(posisiId)!.pegawaiList.push(pegawai); posisiMap.get(posisiId)!.pegawaiList.push(pegawai)
} }
const root: any[] = [] const root: any[] = []
posisiMap.forEach((posisi) => { posisiMap.forEach((posisi) => {
if (posisi.parentId) { if (posisi.parentId) {
const parent = posisiMap.get(posisi.parentId) const parent = posisiMap.get(posisi.parentId)
if (parent) { if (parent) parent.children.push(posisi)
parent.children.push(posisi) else root.push(posisi)
} else { } else root.push(posisi)
root.push(posisi)
}
} else {
root.push(posisi)
}
}) })
function toOrgChartFormat(node: any): any { function toOrgChartFormat(node: any): any {
const pegawai = node.pegawaiList?.[0]
return { return {
expanded: true, expanded: true,
type: 'person', type: 'person',
styleClass: 'p-person', styleClass: 'p-person',
data: { data: {
name: node.pegawaiList?.[0]?.namaLengkap || 'Belum ditugaskan', id: pegawai?.id || null,
name: pegawai?.namaLengkap || 'Belum ditugaskan',
title: node.nama || 'Tanpa jabatan', title: node.nama || 'Tanpa jabatan',
image: node.pegawaiList?.[0]?.image?.link || '/img/default.png', image: pegawai?.image?.link || '/img/default.png',
description: node.deskripsi || '', description: node.deskripsi || '',
positionId: node.id || null, positionId: node.id || null,
}, },
@@ -307,29 +195,91 @@ function StrukturOrganisasiPPID() {
} }
} }
const chartData = root.map(toOrgChartFormat) let chartData = root.map(toOrgChartFormat)
// 🔍 filter by search
if (searchQuery) {
const filterNodes = (nodes: any[]): any[] =>
nodes
.map((n) => ({
...n,
children: filterNodes(n.children || []),
}))
.filter(
(n) =>
n.data.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
n.data.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
n.children.length > 0
)
chartData = filterNodes(chartData)
}
// 🧭 fungsi fullscreen
const toggleFullscreen = () => {
if (!document.fullscreenElement) {
chartContainerRef.current?.requestFullscreen()
setFullscreen(true)
} else {
document.exitFullscreen()
setFullscreen(false)
}
}
// 🧭 fungsi zoom
const handleZoomIn = () => setScale((prev) => Math.min(prev + 0.1, 2))
const handleZoomOut = () => setScale((prev) => Math.max(prev - 0.1, 0.5))
const resetZoom = () => setScale(1)
return ( return (
<Box py={16} > <Stack align="center" mt="xl">
<Paper {/* 🔍 Search + Zoom + Fullscreen controls */}
radius="md" <Group mb="md" justify="center" gap="sm">
p="md" <TextInput
placeholder="Cari nama atau jabatan..."
leftSection={<IconSearch size={16} />}
onChange={(e) => debouncedSearch(e.target.value)}
/>
<Button variant="light" size="sm" onClick={handleZoomOut}>
<IconZoomOut size={16} />
</Button>
<Button variant="light" size="sm" onClick={resetZoom}>
100%
</Button>
<Button variant="light" size="sm" onClick={handleZoomIn}>
<IconZoomIn size={16} />
</Button>
<Button
variant="light"
size="sm"
onClick={toggleFullscreen}
leftSection={
isFullscreen ? <IconArrowsMinimize size={16} /> : <IconArrowsMaximize size={16} />
}
>
{isFullscreen ? 'Keluar' : 'Fullscreen'}
</Button>
</Group>
{/* Chart Container */}
<Box
ref={chartContainerRef}
style={{ style={{
background: 'rgba(28,110,164,0.2)', overflow: 'auto',
border: `1px solid rgba(255,255,255,0.1)`, transform: `scale(${scale})`,
overflowX: 'auto', transformOrigin: 'center top',
transition: 'transform 0.25s ease',
}} }}
> >
<OrganizationChart <OrganizationChart
value={chartData} value={chartData}
nodeTemplate={nodeTemplate} nodeTemplate={(node) => nodeTemplate(node, router)}
/> />
</Paper>
</Box> </Box>
</Stack>
) )
} }
function nodeTemplate(node: any) { function nodeTemplate(node: any, router: ReturnType<typeof useTransitionRouter>) {
const imageSrc = node?.data?.image || '/img/default.png' const imageSrc = node?.data?.image || '/img/default.png'
const name = node?.data?.name || 'Tanpa Nama' const name = node?.data?.name || 'Tanpa Nama'
const title = node?.data?.title || 'Tanpa Jabatan' const title = node?.data?.title || 'Tanpa Jabatan'
@@ -357,15 +307,15 @@ function nodeTemplate(node: any) {
src={imageSrc} src={imageSrc}
alt={name} alt={name}
radius="md" radius="md"
width={120} width={60}
height={120} height={60}
fit="cover" fit="cover"
style={{ style={{
objectFit: 'cover', objectFit: 'cover',
border: '2px solid rgba(255,255,255,0.2)', border: '2px solid rgba(255,255,255,0.2)',
marginBottom: 12, marginBottom: 12,
}} }}
loading='lazy' loading="lazy"
/> />
<Text fw={700}>{name}</Text> <Text fw={700}>{name}</Text>
<Text size="sm" c="dimmed" mt={4}> <Text size="sm" c="dimmed" mt={4}>
@@ -374,19 +324,17 @@ function nodeTemplate(node: any) {
<Text size="xs" c="dimmed" mt={8} lineClamp={3}> <Text size="xs" c="dimmed" mt={8} lineClamp={3}>
{description || 'Belum ada deskripsi.'} {description || 'Belum ada deskripsi.'}
</Text> </Text>
<Tooltip label="Kembali ke struktur organisasi" withArrow position="bottom"> <Tooltip label="Lihat Detail" withArrow position="bottom">
<Button <Button
variant="light" variant="light"
size="xs" size="xs"
mt="md" mt="md"
onClick={() => { onClick={() => {
const id = node?.data?.positionId const id = node?.data?.id
if (id && (window as any).scrollTo) { router.push(`/darmasaba/ppid/struktur-ppid/${id}`)
;(window as any).scrollTo({ top: 0, behavior: 'smooth' })
}
}} }}
> >
Kembali Lihat Detail
</Button> </Button>
</Tooltip> </Tooltip>
</Card> </Card>
@@ -394,6 +342,3 @@ function nodeTemplate(node: any) {
</Transition> </Transition>
) )
} }

View File

@@ -73,7 +73,7 @@ function Page() {
<Text <Text
fz={{ base: 'md', md: 'lg' }} fz={{ base: 'md', md: 'lg' }}
lh={1.7} lh={1.7}
ta="justify" ta="center"
dangerouslySetInnerHTML={{ __html: item.visi }} dangerouslySetInnerHTML={{ __html: item.visi }}
style={{wordBreak: "break-word", whiteSpace: "normal"}} style={{wordBreak: "break-word", whiteSpace: "normal"}}
/> />
@@ -88,7 +88,7 @@ function Page() {
<Text <Text
fz={{ base: 'md', md: 'lg' }} fz={{ base: 'md', md: 'lg' }}
lh={1.7} lh={1.7}
ta="justify" ta="center"
dangerouslySetInnerHTML={{ __html: item.misi }} dangerouslySetInnerHTML={{ __html: item.misi }}
style={{wordBreak: "break-word", whiteSpace: "normal"}} style={{wordBreak: "break-word", whiteSpace: "normal"}}
/> />

View File

@@ -1,6 +1,63 @@
'use client' 'use client'
import { ActionIcon, Anchor, Box, Button, Center, Container, Divider, Flex, Group, Image, Paper, SimpleGrid, Stack, Text, TextInput, Title } from '@mantine/core'; import { ActionIcon, Anchor, Box, Button, Center, Container, Divider, Flex, Group, Image, Paper, SimpleGrid, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconAt, IconBrandFacebook, IconBrandInstagram, IconBrandTwitter, IconBrandWhatsapp } from '@tabler/icons-react'; import { IconAt, IconBrandFacebook, IconBrandInstagram, IconBrandTiktok, IconBrandYoutube } from '@tabler/icons-react';
const sosialMedia = [
{
title: "Facebook",
link: "https://www.facebook.com/DarmasabaDesaku",
icon: IconBrandFacebook,
},
{
title: "Instagram",
link: "https://www.instagram.com/ddarmasaba/",
icon: IconBrandInstagram,
},
{
title: "Youtube",
link: "https://www.youtube.com/channel/UCtPw9WOQO7d2HIKzKgel4Xg",
icon: IconBrandYoutube,
},
{
title: "Tiktok",
link: "https://www.tiktok.com/@desa.darmasaba?is_from_webapp=1&sender_device=pc",
icon: IconBrandTiktok,
},
]
const layanandesa = [
{
title: "Administrasi Kependudukan",
link: "/darmasaba/desa/layanan/",
},
{
title: "Layanan Sosial",
link: "/darmasaba/ekonomi/program-kemiskinan",
},
{
title: "Pengaduan Masyarakat",
link: "/darmasaba/keamanan/laporan-publik",
},
{
title: "Informasi Publik",
link: "/darmasaba/ppid/daftar-informasi-publik-desa-darmasaba",
},
]
const tautanPenting = [
{
title: "Portal Badung",
link: "/darmasaba/desa/berita/semua",
},
{
title: "E-Government",
link: "/darmasaba/inovasi/desa-digital-smart-village",
},
{
title: "Transparansi",
link: "/darmasaba/ppid/daftar-informasi-publik-desa-darmasaba",
}
]
function Footer() { function Footer() {
return ( return (
@@ -64,31 +121,39 @@ function Footer() {
Darmasaba adalah desa budaya yang kaya akan tradisi dan nilai-nilai warisan Bali. Darmasaba adalah desa budaya yang kaya akan tradisi dan nilai-nilai warisan Bali.
</Text> </Text>
<Flex gap="md" mt="sm" c="#F3F2EC"> <Flex gap="md" mt="sm" c="#F3F2EC">
<ActionIcon variant="subtle" color="white"><IconBrandFacebook size={22} /></ActionIcon> {sosialMedia.map((item) => (
<ActionIcon variant="subtle" color="white"><IconBrandInstagram size={22} /></ActionIcon> <ActionIcon
<ActionIcon variant="subtle" color="white"><IconBrandTwitter size={22} /></ActionIcon> key={item.title}
<ActionIcon variant="subtle" color="white"><IconBrandWhatsapp size={22} /></ActionIcon> component="a"
href={item.link}
target="_blank"
rel="noopener noreferrer"
variant="subtle"
color="white"
>
<item.icon size={22} />
</ActionIcon>
))}
</Flex> </Flex>
</Stack> </Stack>
</Box> </Box>
<Box> <Box>
<Stack gap="xs"> <Stack gap="xs">
<Text c="white" fz="md" fw={700}>Layanan Desa</Text> <Text c="white" fz="md" fw={700}>Layanan Desa</Text>
<Anchor c="#F3F2EC" fz="xs">Administrasi Kependudukan</Anchor> {layanandesa.map((item) => (
<Anchor c="#F3F2EC" fz="xs">Layanan Sosial</Anchor> <Anchor key={item.title} c="#F3F2EC" fz="xs" href={item.link}>{item.title}</Anchor>
<Anchor c="#F3F2EC" fz="xs">Pengaduan Masyarakat</Anchor> ))}
<Anchor c="#F3F2EC" fz="xs">Informasi Publik</Anchor>
</Stack> </Stack>
</Box> </Box>
<Box> <Box>
<Stack gap="xs"> <Stack gap="xs">
<Text c="white" fz="md" fw={700}>Tautan Penting</Text> <Text c="white" fz="md" fw={700}>Tautan Penting</Text>
<Anchor c="#F3F2EC" fz="xs">Portal Badung</Anchor> {tautanPenting.map((item) => (
<Anchor c="#F3F2EC" fz="xs">E-Government</Anchor> <Anchor key={item.title} c="#F3F2EC" fz="xs" href={item.link}>{item.title}</Anchor>
<Anchor c="#F3F2EC" fz="xs">Transparansi</Anchor> ))}
<Anchor c="#F3F2EC" fz="xs">Unduhan</Anchor>
</Stack> </Stack>
</Box> </Box>

View File

@@ -1,27 +1,67 @@
import { useRef, useState, useEffect } from 'react';
import stateNav from "@/state/state-nav"; import stateNav from "@/state/state-nav";
import { Container, Stack, TextInput, Tooltip } from "@mantine/core"; import { Container, Stack, ActionIcon, Box } from "@mantine/core";
import { IconSearch } from "@tabler/icons-react"; import { IconX } from '@tabler/icons-react';
import GlobalSearch from "./globalSearch";
export function NavbarSearch() { export function NavbarSearch() {
const [isOpen, setIsOpen] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
// Close when clicking outside
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
const target = event.target as HTMLElement;
// Only close if clicking outside both the search input and results
if (
containerRef.current &&
!containerRef.current.contains(target) &&
!target.closest('.search-result-item') // Add a class to your search result items
) {
setIsOpen(false);
stateNav.clear();
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
return ( return (
<Box
ref={containerRef}
style={{ position: 'relative' }}
>
<Container <Container
w={{ base: "100%", md: "80%" }} w={{ base: "100%", md: "80%" }}
fluid fluid
py="xl" py="xl"
onMouseLeave={stateNav.clear}
> >
<Stack pt="xl"> <Stack pt="xl">
<Tooltip label="Type to search across the site" position="bottom-start" withArrow> <Box style={{ position: 'relative' }}>
<TextInput <GlobalSearch />
autoFocus {isOpen && (
size="lg" <ActionIcon
variant="filled" onClick={() => {
radius="xl" setIsOpen(false);
placeholder="Search anything..." stateNav.clear();
leftSection={<IconSearch size={20} />} }}
/> style={{
</Tooltip> position: 'absolute',
right: 10,
top: '50%',
transform: 'translateY(-50%)',
zIndex: 1000
}}
>
<IconX size={16} />
</ActionIcon>
)}
</Box>
</Stack> </Stack>
</Container> </Container>
</Box>
); );
} }

View File

@@ -86,10 +86,15 @@ function NavbarMobile({ listNavbar }: { listNavbar: MenuItem[] }) {
align="center" align="center"
p="xs" p="xs"
onClick={() => { onClick={() => {
if (item.href) {
router.push(item.href); router.push(item.href);
stateNav.mobileOpen = false; stateNav.mobileOpen = false;
}
}}
style={{
cursor: item.href ? "pointer" : "default",
opacity: item.href ? 1 : 0.8
}} }}
style={{ cursor: "pointer" }}
> >
<Text c="dark.9" fw={600} fz="md"> <Text c="dark.9" fw={600} fz="md">
{item.name} {item.name}

View File

@@ -2,15 +2,14 @@
import colors from "@/con/colors" import colors from "@/con/colors"
import stateNav from "@/state/state-nav" import stateNav from "@/state/state-nav"
import { ActionIcon, Button, Container, Flex, Image, Stack, Tooltip } from "@mantine/core" import { ActionIcon, Button, Container, Flex, Image, Menu, MenuTarget, Stack, Tooltip } from "@mantine/core"
import { useHover } from "@mantine/hooks"
import { IconSearch, IconUser } from "@tabler/icons-react" import { IconSearch, IconUser } from "@tabler/icons-react"
import { useTransitionRouter } from 'next-view-transitions' import { useTransitionRouter } from 'next-view-transitions'
import { usePathname, useRouter } from "next/navigation"
import { useSnapshot } from "valtio" import { useSnapshot } from "valtio"
import { MenuItem } from "../../../../types/menu-item" import { MenuItem } from "../../../../types/menu-item"
import { NavbarSearch } from "./NavBarSearch" import { NavbarSearch } from "./NavBarSearch"
import { NavbarSubMenu } from "./NavbarSubMenu" import { NavbarSubMenu } from "./NavbarSubMenu"
import { useRouter } from "next/navigation"
// contoh state auth (dummy aja dulu, bisa diganti sesuai sistem auth kamu) // contoh state auth (dummy aja dulu, bisa diganti sesuai sistem auth kamu)
const stateAuth = { const stateAuth = {
@@ -21,6 +20,7 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
const { item, isSearch } = useSnapshot(stateNav) const { item, isSearch } = useSnapshot(stateNav)
const router = useTransitionRouter() const router = useTransitionRouter()
const next = useRouter() const next = useRouter()
const pathname = usePathname();
return ( return (
<Stack gap={0} visibleFrom="sm" bg={colors["white-trans-1"]}> <Stack gap={0} visibleFrom="sm" bg={colors["white-trans-1"]}>
@@ -47,7 +47,12 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
</Tooltip> </Tooltip>
{listNavbar.map((item, k) => ( {listNavbar.map((item, k) => (
<MenuItemCom key={k} item={item} /> <MenuItemCom
key={k}
item={item}
isActive={pathname === item.href ||
(item.children?.some(child => child.href === pathname))}
/>
))} ))}
<Tooltip label="Search content" position="bottom" withArrow> <Tooltip label="Search content" position="bottom" withArrow>
@@ -88,27 +93,45 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
) )
} }
function MenuItemCom({ item }: { item: MenuItem }) { function MenuItemCom({ item, isActive = false }: { item: MenuItem, isActive?: boolean }) {
const { ref, hovered } = useHover()
const router = useTransitionRouter() const router = useTransitionRouter()
return ( return (
<Menu
trigger="hover"
position="bottom-start"
offset={20}
width={300}
shadow="md"
withinPortal
onOpen={() => {
stateNav.item = item.children || null;
stateNav.isSearch = false;
}}
>
<MenuTarget>
<Button <Button
ref={ref}
color={hovered ? "gray" : colors["blue-button"]}
onMouseEnter={() => {
stateNav.item = item.children || null
stateNav.isSearch = false
}}
variant="subtle" variant="subtle"
radius="xl" color={isActive ? 'blue' : 'gray'}
onClick={() => { onClick={() => {
router.push(item.href) if (item.href) {
stateNav.clear() router.push(item.href);
stateNav.clear();
}
}}
styles={{
root: {
fontWeight: isActive ? 600 : 400,
borderBottom: isActive ? `2px solid ${colors['blue-button']}` : 'none',
'&:hover': {
backgroundColor: 'transparent',
}
}
}} }}
fw={500}
> >
{item.name} {item.name}
</Button> </Button>
</MenuTarget>
</Menu>
) )
} }

View File

@@ -7,10 +7,11 @@ import { IconArrowRight } from "@tabler/icons-react";
import { MenuItem } from "../../../../types/menu-item"; import { MenuItem } from "../../../../types/menu-item";
import { useTransitionRouter } from "next-view-transitions"; import { useTransitionRouter } from "next-view-transitions";
import colors from "@/con/colors"; import colors from "@/con/colors";
import { usePathname } from "next/navigation";
export function NavbarSubMenu({ item }: { item: MenuItem[] | null }) { export function NavbarSubMenu({ item }: { item: MenuItem[] | null }) {
const router = useTransitionRouter(); const router = useTransitionRouter();
const pathname = usePathname();
return ( return (
<motion.div <motion.div
key={Math.random().toString(36).slice(2)} key={Math.random().toString(36).slice(2)}
@@ -37,23 +38,24 @@ export function NavbarSubMenu({ item }: { item: MenuItem[] | null }) {
justify="space-between" justify="space-between"
size="lg" size="lg"
radius="md" radius="md"
color="gray.0" color={pathname === link.href ? 'blue' : 'gray'}
onClick={() => { onClick={() => {
if (link.href) {
router.push(link.href); router.push(link.href);
stateNav.item = null; stateNav.item = null;
stateNav.isSearch = false; stateNav.isSearch = false;
}
}} }}
rightSection={<IconArrowRight size={18} />} rightSection={<IconArrowRight size={18} />}
styles={(theme) => ({ styles={(theme) => ({
root: { root: {
background: "transparent", background: pathname === link.href ? theme.colors.blue[0] : 'transparent',
color: colors['blue-button'], color: pathname === link.href ? theme.colors.blue[7] : colors['blue-button'],
fontWeight: 500, fontWeight: pathname === link.href ? 600 : 500,
transition: "all 0.2s ease", transition: "all 0.2s ease",
"&:hover": { "&:hover": {
background: theme.colors.gray[8], background: pathname === link.href ? theme.colors.blue[1] : theme.colors.gray[0],
boxShadow: `0 0 12px ${theme.colors.blue[6]}55`, }
},
}, },
})} })}
> >

View File

@@ -0,0 +1,144 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client';
import searchState, { debouncedFetch } from '@/app/api/[[...slugs]]/_lib/search/searchState';
import { Box, Center, Loader, Popover, Text, TextInput } from '@mantine/core';
import { IconX } from '@tabler/icons-react';
import { useEffect, useState } from 'react';
import { useSnapshot } from 'valtio';
import getDetailUrl from './searchUrl';
export default function GlobalSearch() {
const snap = useSnapshot(searchState);
const [opened, setOpened] = useState(false);
// buka popover saat ada query
useEffect(() => {
setOpened(!!snap.query);
}, [snap.query]);
// infinite scroll
useEffect(() => {
const handleScroll = () => {
const bottom = window.innerHeight + window.scrollY >= document.body.offsetHeight - 200;
if (bottom && !snap.loading) searchState.next();
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [snap.loading]);
const handleSelect = async (e: React.MouseEvent, item: any) => {
e.stopPropagation();
e.preventDefault();
const url = getDetailUrl(item);
if (!url) return;
// Immediately close the search dropdown
setOpened(false);
searchState.results = []; // Clear results immediately
searchState.loading = false;
// Use window.location for navigation to ensure full page reload
window.location.href = url;
};
return (
<Box pos="relative">
<Popover
opened={opened && !!snap.query}
onChange={(isOpen) => {
if (!isOpen) {
// Clear search state when popover is closed
searchState.query = '';
searchState.results = [];
searchState.page = 1;
searchState.nextPage = null;
}
setOpened(isOpen);
}}
width="target"
position="bottom"
shadow="md"
withinPortal
radius="md"
zIndex={1000} // Add this line to ensure it appears above other elements
styles={{
dropdown: {
zIndex: 1000, // Add this to ensure the dropdown appears above other elements
},
}}
>
<Popover.Target>
<TextInput
placeholder="Cari apapun..."
value={snap.query}
onChange={(e) => {
searchState.query = e.currentTarget.value;
debouncedFetch();
}}
radius="xl"
size="md"
rightSection={
snap.query ? (
<IconX
size={16}
style={{ cursor: 'pointer' }}
onClick={() => {
searchState.query = '';
searchState.results = [];
searchState.page = 1;
searchState.nextPage = null;
setOpened(false);
}}
/>
) : undefined
}
/>
</Popover.Target>
<Popover.Dropdown
p={0}
style={{
maxHeight: 350,
overflowY: 'auto',
borderRadius: 12,
zIndex: 1000, // Add this line to ensure dropdown stays above other elements
position: 'relative', // Add this to contain child elements
}}
>
{snap.results.length > 0 ? (
snap.results.map((item, i) => (
<Box
key={i}
p="sm"
className="search-result-item" // Add this class
style={{
borderBottom: '1px solid #eee',
cursor: 'pointer',
transition: 'background 0.2s',
position: 'relative', // Add this
zIndex: 1, // Add this to ensure proper stacking context
backgroundColor: 'white', // Ensure background is set
}}
onMouseEnter={(e) => (e.currentTarget.style.background = '#f7f7f7')}
onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')}
onClick={(e) => handleSelect(e, item)} // Pass the event here
>
<Text size="sm" fw={500}>
{item.judul || item.namaPasar || item.nama || item.name}
</Text>
<Text size="xs" c="dimmed">
dari modul: {item.type}
</Text>
</Box>
))
) : (
<Center py="md">
{snap.loading ? <Loader size="sm" /> : <Text fz="sm">Tidak ada hasil</Text>}
</Center>
)}
</Popover.Dropdown>
</Popover>
</Box>
);
}

View File

@@ -332,7 +332,7 @@ function Kepuasan() {
<TextInput <TextInput
label="Nama" label="Nama"
type='text' type='text'
placeholder="masukkan nama" placeholder="Masukkan nama"
defaultValue={state.create.form.name} defaultValue={state.create.form.name}
onChange={(val) => { onChange={(val) => {
state.create.form.name = val.currentTarget.value; state.create.form.name = val.currentTarget.value;

View File

@@ -6,20 +6,19 @@ import {
Center, Center,
Image, Image,
Paper, Paper,
ScrollArea,
SimpleGrid, SimpleGrid,
Skeleton,
Stack, Stack,
Text, Text,
Tooltip, useMantineColorScheme
Skeleton,
useMantineColorScheme,
ScrollArea,
} from "@mantine/core"; } from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks"; import { useShallowEffect } from "@mantine/hooks";
import { Prisma } from "@prisma/client";
import { IconPhotoOff } from "@tabler/icons-react";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { useTransitionRouter } from "next-view-transitions"; import { useTransitionRouter } from "next-view-transitions";
import { useProxy } from "valtio/utils"; import { useProxy } from "valtio/utils";
import { Prisma } from "@prisma/client";
import { IconPhotoOff } from "@tabler/icons-react";
type ProgramInovasiItem = Prisma.ProgramInovasiGetPayload<{ include: { image: true } }>; type ProgramInovasiItem = Prisma.ProgramInovasiGetPayload<{ include: { image: true } }>;
@@ -30,7 +29,6 @@ function ModuleItem({ data }: { data: ProgramInovasiItem }) {
return ( return (
<motion.div whileHover={{ scale: 1.03 }}> <motion.div whileHover={{ scale: 1.03 }}>
<Tooltip label={`Lihat ${data.name}`} withArrow>
<Paper <Paper
onClick={() => router.push(`/darmasaba/program-inovasi/${data.id}`)} onClick={() => router.push(`/darmasaba/program-inovasi/${data.id}`)}
p="lg" p="lg"
@@ -67,7 +65,6 @@ function ModuleItem({ data }: { data: ProgramInovasiItem }) {
</Text> </Text>
</Box> </Box>
</Paper> </Paper>
</Tooltip>
</motion.div> </motion.div>
); );
} }

View File

@@ -37,9 +37,12 @@ export default function ProfileView({ data }: ProfileViewProps) {
<Image <Image
src={data.image.link} src={data.image.link}
alt={data.name || 'Foto profil'} alt={data.name || 'Foto profil'}
fit="cover" fit="contain"
radius="lg" radius="lg"
loading="lazy" loading="lazy"
style={{
objectPosition: 'bottom center',
}}
/> />
) : ( ) : (
<Stack align="center" gap="xs" w="100%" py="xl"> <Stack align="center" gap="xs" w="100%" py="xl">
@@ -49,13 +52,26 @@ export default function ProfileView({ data }: ProfileViewProps) {
</Text> </Text>
</Stack> </Stack>
)} )}
<Box pos="absolute" bottom={0} w="100%" p={{ base: 'xs', md: 'md' }}>
{/* Box nama dan jabatan - sedikit overlap dengan gambar */}
<Box
pos="absolute"
bottom={-20} // bikin naik sedikit ke gambar
right={0}
w="100%"
p={{ base: 'xs', md: 'md' }}
style={{ pointerEvents: 'none' }} // biar ga ganggu klik di gambar
>
<Card <Card
px="lg" px="lg"
radius="2xl" py="sm"
radius="lg"
withBorder withBorder
className="glass3" style={{
style={{ border: '1px solid rgba(255,255,255,0.15)' }} boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
backdropFilter: 'blur(6px)',
pointerEvents: 'auto',
}}
> >
<Tooltip label="Jabatan Resmi" withArrow> <Tooltip label="Jabatan Resmi" withArrow>
<Text fz="sm" c="dimmed"> <Text fz="sm" c="dimmed">

View File

@@ -68,7 +68,7 @@ function Penghargaan() {
variant="gradient" variant="gradient"
gradient={{ from: "cyan", to: "blue", deg: 60 }} gradient={{ from: "cyan", to: "blue", deg: 60 }}
> >
Penghargaan & Prestasi Desa Penghargaan Desa
</Text> </Text>
{loading ? ( {loading ? (

View File

@@ -51,7 +51,7 @@ export default function SDGS() {
</Title> </Title>
</Center> </Center>
<Text fz={{ base: "1rem", md: "1.2rem" }} ta="center" c="dimmed" mt="md" maw={820} mx="auto"> <Text fz={{ base: "1rem", md: "1.2rem" }} ta="center" c="dimmed" mt="md" maw={820} mx="auto">
SDGs Desa merupakan langkah nyata untuk mewujudkan desa yang maju, inklusif, dan berkelanjutan melalui 17 tujuan pembangunan: dari pengentasan kemiskinan, pendidikan, kesehatan, kesetaraan gender, hingga pelestarian lingkungan. SDGs Desa merupakan langkah nyata untuk mewujudkan desa yang maju, inklusif, dan berkelanjutan melalui 17 tujuan pembangunan dari pengentasan kemiskinan, pendidikan, kesehatan, kesetaraan gender, hingga pelestarian lingkungan.
</Text> </Text>
<Box py={50}> <Box py={50}>
@@ -78,6 +78,9 @@ export default function SDGS() {
background: "linear-gradient(180deg, #FFFFFF, #F6F8FA)", background: "linear-gradient(180deg, #FFFFFF, #F6F8FA)",
border: "1px solid rgba(0,0,0,0.05)", border: "1px solid rgba(0,0,0,0.05)",
transition: "all 0.3s ease", transition: "all 0.3s ease",
height: "100%", // biar tinggi antar card konsisten
display: "flex",
flexDirection: "column",
}} }}
> >
<Center mb="lg"> <Center mb="lg">
@@ -105,11 +108,21 @@ export default function SDGS() {
/> />
</Box> </Box>
</Center> </Center>
{/* Stack isi teks & angka */}
<Stack justify="space-between" align="center" gap="xs" h="100%">
<Tooltip label="Nama tujuan SDGs Desa" position="top" withArrow> <Tooltip label="Nama tujuan SDGs Desa" position="top" withArrow>
<Text ta="center" fz={{ base: "lg", md: "xl" }} fw={700} mb="xs"> <Text
ta="center"
fz={{ base: "lg", md: "xl" }}
fw={700}
mb="xs"
style={{ minHeight: mobile ? 60 : 70 }} // biar judulnya punya tinggi tetap
>
{item.name} {item.name}
</Text> </Text>
</Tooltip> </Tooltip>
<Title <Title
order={2} order={2}
ta="center" ta="center"
@@ -122,9 +135,11 @@ export default function SDGS() {
> >
{item.jumlah} {item.jumlah}
</Title> </Title>
</Stack>
</Paper> </Paper>
))} ))}
</SimpleGrid> </SimpleGrid>
) : ( ) : (
<Center mih={200} style={{ flexDirection: "column" }}> <Center mih={200} style={{ flexDirection: "column" }}>
<IconMoodSad size={48} stroke={1.5} style={{ marginBottom: "1rem" }} /> <IconMoodSad size={48} stroke={1.5} style={{ marginBottom: "1rem" }} />

View File

@@ -0,0 +1,36 @@
'use client'
import { useWindowScroll } from '@mantine/hooks';
import { ActionIcon, Transition } from '@mantine/core';
import { IconArrowUp } from '@tabler/icons-react';
import colors from '@/con/colors';
function ScrollToTopButton() {
const [scroll, scrollTo] = useWindowScroll();
return (
<Transition
mounted={scroll.y > 300}
transition="slide-up"
duration={300}
timingFunction="ease"
>
{(styles) => (
<ActionIcon
style={styles}
size="xl"
radius="xl"
variant="filled"
color={colors['blue-button']}
onClick={() => scrollTo({ y: 0 })}
pos="fixed"
bottom={24}
right={24}
aria-label="Kembali ke atas"
>
<IconArrowUp size={20} />
</ActionIcon>
)}
</Transition>
);
}
export default ScrollToTopButton

View File

@@ -0,0 +1,91 @@
const getDetailUrl = (item: { type?: string; id: string | number; [key: string]: unknown }) => {
const { type, id, kategori } = item;
const typeUrlMap: Record<string, string> = {
programinovasi: `/darmasaba/program-inovasi/${id}`,
desaantikorupsi: '/darmasaba/desa-anti-korupsi',
sdgsdesa: '/darmasaba/sdgs-desa',
apbdes: '/darmasaba/apbdes',
prestasidesa: '/darmasaba/prestasi-desa',
pejabatdesa: '/darmasaba/profile/pejabat-desa',
strukturppid: '/darmasaba/ppid/struktur-ppid',
visimisippid: '/darmasaba/ppid/visi-misi',
dasarhukumppid: '/darmasaba/ppid/dasar-hukum',
profileppid: '/darmasaba/ppid/profile',
daftarinformasipublik: '/darmasaba/ppid/daftar-informasi-publik',
perbekeldarmasaba: '/darmasaba/desa/profile',
berita: `/darmasaba/desa/berita/${kategori}/${id}`,
pengumuman: `/darmasaba/desa/pengumuman/${kategori}/${id}`,
sejarahdesa: '/darmasaba/desa/profile',
visimisidesa: '/darmasaba/desa/profile',
lambangdesa: '/darmasaba/desa/profile',
maskotdesa: '/darmasaba/desa/profile',
profilperbekel: '/darmasaba/desa/profile',
potensi: '/darmasaba/desa/potensi-desa',
galleryFoto: '/darmasaba/desa/gallery/foto',
galleryVideo: '/darmasaba/desa/gallery/video',
pelayananSuratKeterangan: '/darmasaba/desa/layanan',
pelayananPerizinanBerusaha: '/darmasaba/desa/layanan',
pelayananTelunjukSaktiDesa: '/darmasaba/desa/layanan',
pelayananPendudukNonPermanent: '/darmasaba/desa/layanan',
penghargaan: '/darmasaba/desa/penghargaan',
posyandu: '/darmasaba/kesehatan/posyandu',
fasilitasKesehatan: '/darmasaba/kesehatan/data-kesehatan-warga',
jadwalKegiatan: '/darmasaba/kesehatan/data-kesehatan-warga',
artikelKesehatan: '/darmasaba/kesehatan/data-kesehatan-warga',
puskesmas: '/darmasaba/kesehatan/puskesmas',
programKesehatan: '/darmasaba/kesehatan/program-kesehatan',
penangananDarurat: '/darmasaba/kesehatan/penanganan-darurat',
kontakDarurat: '/darmasaba/kesehatan/kontak-darurat',
infoWabahPenyakit: '/darmasaba/kesehatan/info-wabah-penyakit',
keamananLingkungan: '/darmasaba/keamanan/keamanan-lingkungan-pecalang-patwal',
polsekTerdekat: '/darmasaba/keamanan/polsek-terdekat',
kontakDaruratKeamanan: '/darmasaba/keamanan/kontak-darurat',
pencegahanKriminalitas: '/darmasaba/keamanan/pencegahan-kriminalitas',
laporanPublik: '/darmasaba/keamanan/laporan-publik',
tipsKeamanan: '/darmasaba/keamanan/tips-keamanan',
pasarDesa: '/darmasaba/ekonomi/pasar-desa',
lowonganKerjaLokal: '/darmasaba/ekonomi/lowongan-kerja-lokal',
strukturOrganisasi: '/darmasaba/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa',
jumlahPendudukUsiaKerjaYangMenganggurUsia: '/darmasaba/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur',
jumlahPendudukUsiaKerjaYangMenganggurPendidikan: '/darmasaba/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur',
jumlahPendudukMiskin: '/darmasaba/ekonomi/jumlah-penduduk-miskin',
programKemiskinan: '/darmasaba/ekonomi/program-kemiskinan',
sektorUnggulanDesa: '/darmasaba/ekonomi/sektor-unggulan-desa',
demografiPekerjaan: '/darmasaba/ekonomi/demografi-pekerjaan',
desaDigital: '/darmasaba/inovasi/desa-digital-smart-village',
programKreatif: '/darmasaba/inovasi/program-kreatif-desa',
kolaborasiInovasi: '/darmasaba/inovasi/kolaborasi-inovasi',
mitraKolaborasi: '/darmasaba/inovasi/kolaborasi-inovasi',
infoTekno: '/darmasaba/inovasi/info-teknologi-tepat-guna',
pengelolaanSampah: '/darmasaba/lingkungan/pengelolaan-sampah-bank-sampah',
keteranganBankSampahTerdekat: '/darmasaba/lingkungan/pengelolaan-sampah-bank-sampah',
programPenghijauan: '/darmasaba/lingkungan/program-penghijauan',
dataLingkunganDesa: '/darmasaba/lingkungan/data-lingkungan-desa',
gotongRoyong: '/darmasaba/lingkungan/gotong-royong',
tujuanEdukasiLingkungan: '/darmasaba/lingkungan/edukasi-lingkungan',
materiEdukasiLingkungan: '/darmasaba/lingkungan/edukasi-lingkungan',
contohEdukasiLingkungan: '/darmasaba/lingkungan/edukasi-lingkungan',
filosofiTriHita: '/darmasaba/lingkungan/konservasi-adat-bali',
bentukKonservasiBerdasarkanAdat: '/darmasaba/lingkungan/konservasi-adat-bali',
nilaiKonservasiAdat: '/darmasaba/lingkungan/konservasi-adat-bali',
jenjangPendidikan: '/darmasaba/pendidikan/info-sekolah/semua',
lembaga: '/darmasaba/pendidikan/info-sekolah/semua/lembaga',
siswa: '/darmasaba/pendidikan/info-sekolah/semua/siswa',
pengajar: '/darmasaba/pendidikan/info-sekolah/semua/pengajar',
keunggulanProgram: '/darmasaba/pendidikan/beasiswa-desa',
tujuanProgram: '/darmasaba/pendidikan/program-pendidikan-anak',
programUnggulan: '/darmasaba/pendidikan/program-pendidikan-anak',
lokasiJadwalBimbinganBelajarDesa: '/darmasaba/pendidikan/bimbingan-belajar-desa',
fasilitasBimbinganBelajarDesa: '/darmasaba/pendidikan/bimbingan-belajar-desa',
tujuanPendidikanNonFormal: '/darmasaba/pendidikan/pendidikan-non-formal',
tempatKegiatan: '/darmasaba/pendidikan/pendidikan-non-formal',
jenisProgramYangDiselenggarakan: '/darmasaba/pendidikan/pendidikan-non-formal',
dataPerpustakaan: '/darmasaba/pendidikan/perpustakaan-digital/semua',
dataPendidikan: '/darmasaba/pendidikan/data-pendidikan',
};
return typeUrlMap[type || ''] || '/darmasaba';
};
export default getDetailUrl;

View File

@@ -8,13 +8,15 @@ import colors from "@/con/colors";
import SDGS from "./_com/main-page/sdgs"; import SDGS from "./_com/main-page/sdgs";
// import ApiFetch from "@/lib/api-fetch"; // import ApiFetch from "@/lib/api-fetch";
import { Stack } from "@mantine/core"; import { Box, Stack } from "@mantine/core";
import Apbdes from "./_com/main-page/apbdes"; import Apbdes from "./_com/main-page/apbdes";
import Prestasi from "./_com/main-page/prestasi"; import Prestasi from "./_com/main-page/prestasi";
import ScrollToTopButton from "./_com/scrollToTopButton";
export default function Page() { export default function Page() {
return ( return (
<Box>
<Stack bg={colors.grey[1]} gap={"4rem"}> <Stack bg={colors.grey[1]} gap={"4rem"}>
<LandingPage /> <LandingPage />
<Penghargaan /> <Penghargaan />
@@ -26,5 +28,8 @@ export default function Page() {
<Apbdes /> <Apbdes />
<Prestasi /> <Prestasi />
</Stack> </Stack>
{/* Tombol Scroll ke Atas */}
<ScrollToTopButton />
</Box>
); );
} }

View File

@@ -11,7 +11,7 @@
-webkit-backdrop-filter: blur(40px); -webkit-backdrop-filter: blur(40px);
backdrop-filter: blur(40px); backdrop-filter: blur(40px);
position: fixed; position: fixed;
z-index: 1; z-index: 50;
width: 100%; width: 100%;
height: 100vh; height: 100vh;
} }

View File

@@ -2,7 +2,6 @@ const navbarListMenu = [
{ {
id: "1", id: "1",
name: "PPID", name: "PPID",
href: "/darmasaba/ppid/profile-ppid",
children: [ children: [
{ {
id: "1.1", id: "1.1",
@@ -51,7 +50,6 @@ const navbarListMenu = [
{ {
id: "2", id: "2",
name: "Desa", name: "Desa",
href: "/darmasaba/desa/profile",
children: [ children: [
{ {
id: "2.1", id: "2.1",
@@ -94,7 +92,6 @@ const navbarListMenu = [
{ {
id: "3", id: "3",
name: "Kesehatan", name: "Kesehatan",
href: "/darmasaba/kesehatan/posyandu",
children: [ children: [
{ {
id: "3.1", id: "3.1",
@@ -136,7 +133,6 @@ const navbarListMenu = [
{ {
id: "4", id: "4",
name: "Keamanan", name: "Keamanan",
href: "/darmasaba/keamanan/keamanan-lingkungan-pecalang-patwal",
children: [ children: [
{ {
id: "4.1", id: "4.1",
@@ -173,7 +169,6 @@ const navbarListMenu = [
{ {
id: "5", id: "5",
name: "Ekonomi", name: "Ekonomi",
href: "/darmasaba/ekonomi/pasar-desa",
children: [ children: [
{ {
id: "5.1", id: "5.1",
@@ -229,7 +224,6 @@ const navbarListMenu = [
}, { }, {
id: "6", id: "6",
name: "Inovasi", name: "Inovasi",
href: "/darmasaba/inovasi/desa-digital-smart-village",
children: [ children: [
{ {
id: "6.1", id: "6.1",
@@ -266,7 +260,6 @@ const navbarListMenu = [
}, { }, {
id: "7", id: "7",
name: "Lingkungan", name: "Lingkungan",
href: "/darmasaba/lingkungan/pengelolaan-sampah-bank-sampah",
children: [ children: [
{ {
id: "7.1", id: "7.1",
@@ -302,7 +295,6 @@ const navbarListMenu = [
}, { }, {
id: "8", id: "8",
name: "Pendidikan", name: "Pendidikan",
href: "/darmasaba/pendidikan/info-sekolah",
children: [ children: [
{ {
id: "8.1", id: "8.1",

View File

@@ -1,6 +1,9 @@
export type MenuItem = { export type MenuItem = {
id: string, id: string;
name: string, name: string;
href: string, href?: string;
children?: MenuItem[] children?: MenuItem[];
} } & (
{ href: string; children?: MenuItem[] } |
{ children: MenuItem[] }
)