Compare commits

..

3 Commits

Author SHA1 Message Date
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
47 changed files with 2239 additions and 422 deletions

View File

@@ -143,7 +143,7 @@ model MediaSosial {
isActive Boolean @default(true)
}
//========================================= PROFILE ========================================= //
//========================================= DESA ANTI KORUPSI ========================================= //
model DesaAntiKorupsi {
id String @id @default(cuid())
name String @unique

View File

@@ -95,6 +95,41 @@ const dataPerpustakaan = proxy({
}
},
},
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;
}
},
},
findUnique: {
data: null as Prisma.DataPerpustakaanGetPayload<{
include: {

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: {
data: null as
| (Prisma.PegawaiPPIDGetPayload<{

View File

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

View File

@@ -43,7 +43,7 @@ function PerizinanBerusaha() {
try {
setLoading(true);
// 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);
} catch (err) {
setError('Gagal memuat data');

View File

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

View File

@@ -223,7 +223,7 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
{/* Chart */}
<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>
{mounted && diseaseChartData.length > 0 ? (
<Center>

View File

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

View File

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

View File

@@ -65,7 +65,11 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
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) => (
<Tooltip

View File

@@ -72,7 +72,11 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
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) => (
<Tooltip

View File

@@ -79,7 +79,11 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
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) => (
<Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}>

View File

@@ -73,7 +73,11 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
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) => (
<Tooltip

View File

@@ -72,7 +72,11 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
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) => (
<Tooltip

View File

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

View File

@@ -66,7 +66,11 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
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) => (
<Tooltip

View File

@@ -103,18 +103,7 @@ function ListPegawaiPPID({ search }: { search: string }) {
</TableTr>
</TableThead>
<TableTbody>
{(() => {
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) => (
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={150}>

View File

@@ -21,10 +21,10 @@ function ListStrukturOrganisasiPPID() {
const stateOrganisasi = useProxy(stateStrukturPPID.pegawai);
useEffect(() => {
stateOrganisasi.findMany.load();
stateOrganisasi.findManyAll.load();
}, []);
if (stateOrganisasi.findMany.loading) {
if (stateOrganisasi.findManyAll.loading) {
return (
<Center py={40}>
<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 (
<Stack align="center" py={60} gap="sm">
<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 aktifPegawai = stateOrganisasi.findMany.data.filter(p => p.isActive);
const aktifPegawai = stateOrganisasi.findManyAll.data?.filter(p => p.isActive);
for (const pegawai of aktifPegawai) {
const posisiId = pegawai.posisi.id;

View File

@@ -377,22 +377,5 @@ export const navBar = [
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 dataPerpustakaanFindUnique from "./findUnique";
import dataPerpustakaanUpdate from "./updt";
import dataPerpustakaanFindManyAll from "./findManyAll";
const DataPerpustakaan = new Elysia({
prefix: "/dataperpustakaan",
@@ -18,7 +19,7 @@ const DataPerpustakaan = new Elysia({
kategoriId: t.String(),
}),
})
.get("/findManyAll", dataPerpustakaanFindManyAll)
.get("/findMany", dataPerpustakaanFindMany)
.get("/:id", async (context) => {
const response = await dataPerpustakaanFindUnique(

View File

@@ -2,61 +2,67 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
// Di findMany.ts
export default async function pegawaiFindMany(context: Context) {
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || "";
const skip = (page - 1) * limit;
// Buat where clause
const isActiveParam = context.query.isActive;
// where clause dinamis
const where: any = {};
if (isActiveParam !== undefined) {
where.isActive = isActiveParam === "true";
}
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ namaLengkap: { contains: search, mode: "insensitive" } },
{ alamat: { contains: search, mode: "insensitive" } },
{ posisi: { nama: { contains: search, mode: "insensitive" } } },
];
}
try {
const [data, total] = await Promise.all([
// Ambil semua data terlebih dahulu (tanpa pagination)
const [allData, total] = await Promise.all([
prisma.pegawaiPPID.findMany({
where,
include: {
posisi: 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);
return {
success: true,
message: "Success fetch pegawai with pagination",
data,
message: "Success fetch pegawai with hierarchy order",
data: paginatedData,
page,
totalPages,
total,
};
} catch (e) {
console.error("Find many paginated error:", e);
} catch (error) {
console.error("Find many pegawai error:", error);
return {
success: false,
message: "Failed fetch pegawai with pagination",
message: "Failed fetch pegawai",
data: [],
page: 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 pegawaiUpdate from "./updt";
import pegawaiDelete from "./del";
import pegawaiFindManyAll from "./findManyAll";
const Pegawai = new Elysia({
@@ -15,6 +16,9 @@ const Pegawai = new Elysia({
// ✅ Find all
.get("/find-many", pegawaiFindMany)
// ✅ Find all (non-paginated)
.get("/find-many-all", pegawaiFindManyAll)
// ✅ Find by ID
.get("/:id", async (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 User from "./_lib/user";
import Role from "./_lib/user/role";
import Search from "./_lib/search";
const ROOT = process.cwd();
@@ -95,6 +96,7 @@ const ApiServer = new Elysia()
.use(Pendidikan)
.use(User)
.use(Role)
.use(Search)
.onError(({ code }) => {
if (code === "NOT_FOUND") {

View File

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

View File

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

View File

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

View File

@@ -10,31 +10,36 @@ import ProfilPerbekel from './ui/profilPerbekel';
// import LembagaDesa from './ui/lembagaDesa';
import MotoDesa from './ui/motoDesa';
import SemuaPerbekel from './ui/semuaPerbekel';
import ScrollToTopButton from '@/app/darmasaba/_com/scrollToTopButton';
function Page() {
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Container w={{ base: "100%", md: "50%" }}>
<Stack align='center' gap={0}>
<Text fz={{base: "h1", md: "2.5rem"}} c={colors["blue-button"]} fw={"bold"}>
Profile Desa
</Text>
</Stack>
</Container>
<Box px={{ base: "md", md: 100 }}>
<ProfileDesa />
<SejarahDesa />
<VisimisiDesa />
<LambangDesa />
<MaskotDesa />
<ProfilPerbekel />
<MotoDesa />
<SemuaPerbekel/>
</Box>
</Stack>
<Box>
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Container w={{ base: "100%", md: "50%" }}>
<Stack align='center' gap={0}>
<Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
Profile Desa
</Text>
</Stack>
</Container>
<Box px={{ base: "md", md: 100 }}>
<ProfileDesa />
<SejarahDesa />
<VisimisiDesa />
<LambangDesa />
<MaskotDesa />
<ProfilPerbekel />
<MotoDesa />
<SemuaPerbekel />
</Box>
</Stack>
{/* Tombol Scroll ke Atas */}
<ScrollToTopButton />
</Box>
);
}

View File

@@ -2,7 +2,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import colors from '@/con/colors';
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 BackButton from '../../desa/layanan/_com/BackButto';
@@ -107,20 +107,7 @@ function Page() {
<Box>
<Paper p={"xl"} bg={colors['white-trans-1']}>
<Box pb={30}>
<Flex pb={30} justify={'flex-end'} 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>
<Title order={2} mb="md">Data Kematian dan Kelahiran</Title>
{chartData.length === 0 ? (
<Text c="dimmed" ta="center" py="xl">
Belum ada data yang tersedia untuk ditampilkan
@@ -150,6 +137,20 @@ function Page() {
</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>
</Paper>
</Box>
@@ -163,11 +164,11 @@ function Page() {
}}
>
{/* Fasilitas Kesehatan */}
<FasilitasKesehatan/>
<FasilitasKesehatan />
{/* Jadwal Kegiatan */}
<JadwalKegiatan/>
<JadwalKegiatan />
{/* Artikel Kesehatan */}
<ArtikelKesehatanPage/>
<ArtikelKesehatanPage />
</SimpleGrid>
</Box>
</Stack>

View File

@@ -121,13 +121,13 @@ function Page() {
</Badge>
</Group>
<Text fz="sm" c="dimmed">
Diposting: 12 Februari 2025 · Dinas Kesehatan
Diposting: {v.createdAt.toLocaleDateString()}
</Text>
<Divider />
<Text fz="sm" lh={1.5}>
<Text fz="sm" lh={1.5} lineClamp={3} truncate="end">
{v.deskripsiSingkat}
</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
</Button>
</Stack>

View File

@@ -97,18 +97,23 @@ function Page() {
shadow="sm"
withBorder
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">
<Center>
<Image
src={v.image.link}
alt={v.name}
w={160}
h={160}
fit="contain"
h={180}
w="100%"
radius="md"
loading="lazy"
fit="cover"
style={{ aspectRatio: '4/3' }}
/>
</Center>
<Stack gap={4} w="100%">
@@ -151,8 +156,11 @@ function Page() {
styles={{
control: {
border: `1px solid ${colors['blue-button']}`,
transition: 'all 0.3s ease',
'&:hover': { backgroundColor: colors['blue-button'], color: 'white' },
},
}}
/>
</Center>

View File

@@ -28,11 +28,31 @@ export default function Page() {
if (data.length === 0) {
return (
<Box py="xl" px={{ base: "md", md: 100 }}>
<Text fz="lg" fw="bold" c={colors["blue-button"]}>
Tidak ada posyandu yang ditemukan
</Text>
</Box>
<Stack pos="relative" bg={colors.Bg} py="xl" gap="xl">
<Box px={{ base: "md", md: 100 }}>
<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>
<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>
</Stack>
);
}

View File

@@ -12,10 +12,9 @@ import {
SimpleGrid,
Stack,
Text,
TextInput,
Tooltip,
TextInput
} from '@mantine/core';
import { IconDownload, IconSend2 } from '@tabler/icons-react';
import { IconSend2 } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto';
@@ -150,23 +149,6 @@ function Page() {
</Paper>
))}
</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">
<Paper
p="xl"

View File

@@ -6,6 +6,7 @@ import { useShallowEffect } from '@mantine/hooks';
import { IconBuildingCommunity, IconTargetArrow, IconTimeline, IconUser } from '@tabler/icons-react';
import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto';
import ScrollToTopButton from '@/app/darmasaba/_com/scrollToTopButton';
function Page() {
const allList = useProxy(stateProfilePPID)
@@ -36,99 +37,103 @@ function Page() {
: [allList.profile.data]
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }}>
<Text ta="center" fz={{ base: "2rem", md: "2.5rem", lg: "3rem", xl: "3.4rem" }} c={colors["blue-button"]} fw="bold">
Profil PPID Desa Darmasaba
</Text>
</Box>
{dataArray.map((item) => (
<Box key={item.id} px={{ base: "md", md: 100 }}>
<Paper p="xl" bg={colors['white-trans-1']} radius="lg" shadow="xl">
<Box px={{ base: "md", md: 100 }}>
<Flex align="center" gap={40} justify="center">
<Image loading='lazy' src="/darmasaba-icon.png" h={{ base: 70, md: 120 }} alt="Logo Desa" />
<Text fz={{ base: "1.5rem", md: "2rem", lg: "2.5rem", xl: "3rem" }} fw="bold">
Pejabat Pengelola Informasi Publik
</Text>
</Flex>
</Box>
<Divider my="lg" />
<Box px={{ base: 0, md: 50 }} pb={40}>
<SimpleGrid cols={{ base: 1, xl: 2 }} spacing="xl">
<Box px={{ base: 0, md: 50 }}>
<Paper bg={colors['white-trans-1']} radius="lg" shadow="sm">
<Stack gap="md">
<Center>
<Image
loading='lazy'
src={item.image?.link ? `${item.image.link}?t=${Date.now()}` : "/perbekel.png"}
w={{ base: 220, md: 330 }}
alt="Foto Pimpinan"
radius="md"
/>
</Center>
<Paper bg={colors['blue-button']} py={25} radius="lg" className="glass3">
<Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "1.5rem", md: "2rem" }}>
{item.name}
</Text>
</Paper>
</Stack>
</Paper>
</Box>
<Box>
<Stack gap="xl">
<Box>
<Flex align="center" gap="sm" mb="sm">
<IconUser size={28} />
<Text fz={{ base: "1.25rem", md: "1.5rem" }} fw="bold">Biografi</Text>
</Flex>
<Text fz={{ base: "1rem", md: "1.125rem", lg: "1.25rem" }} ta="justify" dangerouslySetInnerHTML={{ __html: item.biodata }} style={{wordBreak: "break-word", whiteSpace: "normal"}} />
</Box>
<Box>
<Flex align="center" gap="sm" mb="sm">
<IconTimeline size={28} />
<Text fz={{ base: "1.25rem", md: "1.5rem" }} fw="bold">Riwayat Karir</Text>
</Flex>
<Text fz={{ base: "1rem", md: "1.125rem", lg: "1.25rem" }} dangerouslySetInnerHTML={{ __html: item.riwayat }} style={{wordBreak: "break-word", whiteSpace: "normal"}} />
</Box>
</Stack>
</Box>
</SimpleGrid>
</Box>
<Box pb={40}>
<Flex align="center" gap="sm" mb="sm">
<IconBuildingCommunity size={28} />
<Text fz={{ base: "1.25rem", md: "1.5rem" }} fw="bold">Pengalaman Organisasi</Text>
</Flex>
<List spacing="xs" size="sm">
<Box px={20}>
<Text fz={{ base: "1rem", md: "1.125rem" }} ta="justify" dangerouslySetInnerHTML={{ __html: item.pengalaman }} style={{wordBreak: "break-word", whiteSpace: "normal"}} />
</Box>
</List>
</Box>
<Box>
<Flex align="center" gap="sm" mb="sm">
<IconTargetArrow size={28} />
<Text fz={{ base: "1.25rem", md: "1.5rem" }} fw="bold">Program Unggulan</Text>
</Flex>
<List spacing="xs" size="sm">
<Box px={20}>
<Text fz={{ base: "1rem", md: "1.125rem" }} ta="justify" dangerouslySetInnerHTML={{ __html: item.unggulan }} style={{wordBreak: "break-word", whiteSpace: "normal"}} />
</Box>
</List>
</Box>
</Paper>
<Box>
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
))}
</Stack>
<Box px={{ base: 'md', md: 100 }}>
<Text ta="center" fz={{ base: "2rem", md: "2.5rem", lg: "3rem", xl: "3.4rem" }} c={colors["blue-button"]} fw="bold">
Profil PPID Desa Darmasaba
</Text>
</Box>
{dataArray.map((item) => (
<Box key={item.id} px={{ base: "md", md: 100 }}>
<Paper p="xl" bg={colors['white-trans-1']} radius="lg" shadow="xl">
<Box px={{ base: "md", md: 100 }}>
<Flex align="center" gap={40} justify="center">
<Image loading='lazy' src="/darmasaba-icon.png" h={{ base: 70, md: 120 }} alt="Logo Desa" />
<Text fz={{ base: "1.5rem", md: "2rem", lg: "2.5rem", xl: "3rem" }} fw="bold">
Pejabat Pengelola Informasi Publik
</Text>
</Flex>
</Box>
<Divider my="lg" />
<Box px={{ base: 0, md: 50 }} pb={40}>
<SimpleGrid cols={{ base: 1, xl: 2 }} spacing="xl">
<Box px={{ base: 0, md: 50 }}>
<Paper bg={colors['white-trans-1']} radius="lg" shadow="sm">
<Stack gap="md">
<Center>
<Image
loading='lazy'
src={item.image?.link ? `${item.image.link}?t=${Date.now()}` : "/perbekel.png"}
w={{ base: 220, md: 330 }}
alt="Foto Pimpinan"
radius="md"
/>
</Center>
<Paper bg={colors['blue-button']} py={25} radius="lg" className="glass3">
<Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "1.5rem", md: "2rem" }}>
{item.name}
</Text>
</Paper>
</Stack>
</Paper>
</Box>
<Box>
<Stack gap="xl">
<Box>
<Flex align="center" gap="sm" mb="sm">
<IconUser size={28} />
<Text fz={{ base: "1.25rem", md: "1.5rem" }} fw="bold">Biografi</Text>
</Flex>
<Text fz={{ base: "1rem", md: "1.125rem", lg: "1.25rem" }} ta="justify" dangerouslySetInnerHTML={{ __html: item.biodata }} style={{ wordBreak: "break-word", whiteSpace: "normal" }} />
</Box>
<Box>
<Flex align="center" gap="sm" mb="sm">
<IconTimeline size={28} />
<Text fz={{ base: "1.25rem", md: "1.5rem" }} fw="bold">Riwayat Karir</Text>
</Flex>
<Text fz={{ base: "1rem", md: "1.125rem", lg: "1.25rem" }} dangerouslySetInnerHTML={{ __html: item.riwayat }} style={{ wordBreak: "break-word", whiteSpace: "normal" }} />
</Box>
</Stack>
</Box>
</SimpleGrid>
</Box>
<Box pb={40}>
<Flex align="center" gap="sm" mb="sm">
<IconBuildingCommunity size={28} />
<Text fz={{ base: "1.25rem", md: "1.5rem" }} fw="bold">Pengalaman Organisasi</Text>
</Flex>
<List spacing="xs" size="sm">
<Box px={20}>
<Text fz={{ base: "1rem", md: "1.125rem" }} ta="justify" dangerouslySetInnerHTML={{ __html: item.pengalaman }} style={{ wordBreak: "break-word", whiteSpace: "normal" }} />
</Box>
</List>
</Box>
<Box>
<Flex align="center" gap="sm" mb="sm">
<IconTargetArrow size={28} />
<Text fz={{ base: "1.25rem", md: "1.5rem" }} fw="bold">Program Unggulan</Text>
</Flex>
<List spacing="xs" size="sm">
<Box px={20}>
<Text fz={{ base: "1rem", md: "1.125rem" }} ta="justify" dangerouslySetInnerHTML={{ __html: item.unggulan }} style={{ wordBreak: "break-word", whiteSpace: "normal" }} />
</Box>
</List>
</Box>
</Paper>
</Box>
))}
</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,8 @@
/* eslint-disable react-hooks/exhaustive-deps */
/* 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'
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID'
import colors from '@/con/colors'
import {
Box,
Button,
@@ -145,7 +24,8 @@ import { OrganizationChart } from 'primereact/organizationchart'
import { useEffect } from 'react'
import { useProxy } from 'valtio/utils'
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'
export default function Page() {
return (
@@ -180,12 +60,16 @@ export default function Page() {
<StrukturOrganisasiPPID />
</Box>
</Container>
{/* Tombol Scroll ke Atas */}
<ScrollToTopButton />
</Box>
)
}
function StrukturOrganisasiPPID() {
const stateOrganisasi: any = useProxy(stateStrukturPPID.pegawai)
const router = useTransitionRouter()
useEffect(() => {
void stateOrganisasi.findMany.load()
@@ -292,19 +176,21 @@ function StrukturOrganisasiPPID() {
})
function toOrgChartFormat(node: any): any {
const pegawai = node.pegawaiList?.[0];
return {
expanded: true,
type: 'person',
styleClass: 'p-person',
data: {
name: node.pegawaiList?.[0]?.namaLengkap || 'Belum ditugaskan',
id: pegawai?.id || null, // tambahin ini bro
name: pegawai?.namaLengkap || 'Belum ditugaskan',
title: node.nama || 'Tanpa jabatan',
image: node.pegawaiList?.[0]?.image?.link || '/img/default.png',
image: pegawai?.image?.link || '/img/default.png',
description: node.deskripsi || '',
positionId: node.id || null,
},
children: node.children?.map(toOrgChartFormat) || [],
}
};
}
const chartData = root.map(toOrgChartFormat)
@@ -322,21 +208,22 @@ function StrukturOrganisasiPPID() {
>
<OrganizationChart
value={chartData}
nodeTemplate={nodeTemplate}
nodeTemplate={(node) => nodeTemplate(node, router)}
/>
</Paper>
</Box>
)
}
function nodeTemplate(node: any) {
function nodeTemplate(node: any, router: ReturnType<typeof useTransitionRouter>) {
const imageSrc = node?.data?.image || '/img/default.png'
const name = node?.data?.name || 'Tanpa Nama'
const title = node?.data?.title || 'Tanpa Jabatan'
const description = node?.data?.description || ''
return (
<Transition mounted transition="pop" duration={240}>
<Transition mounted transition="pop" duration={240}>
{(styles) => (
<Card
radius="lg"
@@ -374,19 +261,17 @@ function nodeTemplate(node: any) {
<Text size="xs" c="dimmed" mt={8} lineClamp={3}>
{description || 'Belum ada deskripsi.'}
</Text>
<Tooltip label="Kembali ke struktur organisasi" withArrow position="bottom">
<Tooltip label="Lihat Detail" withArrow position="bottom">
<Button
variant="light"
size="xs"
mt="md"
onClick={() => {
const id = node?.data?.positionId
if (id && (window as any).scrollTo) {
;(window as any).scrollTo({ top: 0, behavior: 'smooth' })
}
const id = node?.data?.id
router.push(`/darmasaba/ppid/struktur-ppid/${id}`)
}}
>
Kembali
Lihat Detail
</Button>
</Tooltip>
</Card>
@@ -394,6 +279,3 @@ function nodeTemplate(node: any) {
</Transition>
)
}

View File

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

View File

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

View File

@@ -0,0 +1,91 @@
'use client';
import searchState, { debouncedFetch } from '@/app/api/[[...slugs]]/_lib/search/searchState';
import { Box, Center, Loader, Stack, Text, TextInput } from '@mantine/core';
import { IconX } from '@tabler/icons-react';
import { useEffect } from 'react';
import { useSnapshot } from 'valtio';
import getDetailUrl from './searchUrl';
export default function GlobalSearch() {
const snap = useSnapshot(searchState);
// 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]);
return (
<Stack maw={800} mx="auto">
{/* 🔍 Search input */}
<TextInput
placeholder="Cari apapun..."
value={snap.query}
onChange={(e) => (
searchState.query = e.currentTarget.value,
debouncedFetch()
)}
radius="xl"
rightSection={
snap.query ? (
<IconX
size={16}
style={{ cursor: 'pointer' }}
onClick={() => {
searchState.query = '';
searchState.results = [];
}}
/>
) : undefined
}
/>
{/* 📄 Hasil pencarian */}
<div style={{ maxHeight: '400px', overflowY: 'auto' }}>
{snap.results.map((item, i) => (
<Box
key={i}
p="sm"
style={{
borderBottom: '1px solid #eee',
cursor: 'pointer',
transition: 'background 0.2s',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
maxWidth: '100%'
}}
onMouseEnter={(e) => (e.currentTarget.style.background = '#f5f5f5')}
onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')}
onClick={() => {
const url = getDetailUrl(item);
window.location.href = url;
}}
>
<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>
))}
</div>
{/* ⏳ Loader di bawah hasil */}
{snap.loading && (
<Center py="md">
<Loader size="sm" />
</Center>
)}
</Stack>
);
}

View File

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

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,60 @@
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',
};
return typeUrlMap[type || ''] || '/darmasaba';
};
export default getDetailUrl;

View File

@@ -8,23 +8,28 @@ import colors from "@/con/colors";
import SDGS from "./_com/main-page/sdgs";
// 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 Prestasi from "./_com/main-page/prestasi";
import ScrollToTopButton from "./_com/scrollToTopButton";
export default function Page() {
return (
<Stack bg={colors.grey[1]} gap={"4rem"}>
<LandingPage />
<Penghargaan />
<Layanan />
<Potensi />
<DesaAntiKorupsi />
<Kepuasan />
<SDGS />
<Apbdes />
<Prestasi/>
</Stack>
<Box>
<Stack bg={colors.grey[1]} gap={"4rem"}>
<LandingPage />
<Penghargaan />
<Layanan />
<Potensi />
<DesaAntiKorupsi />
<Kepuasan />
<SDGS />
<Apbdes />
<Prestasi />
</Stack>
{/* Tombol Scroll ke Atas */}
<ScrollToTopButton />
</Box>
);
}