Fix Admin Menu PPID, Submenu IKM

This commit is contained in:
2025-08-13 00:07:57 +08:00
parent c1583c21b1
commit a1d55e2b0a
64 changed files with 2865 additions and 1829 deletions

View File

@@ -124,7 +124,7 @@ export default function Content({ kategori }: { kategori: string }) {
p="lg"
radius="md"
withBorder
onClick={() => router.push(`/desa/berita/${item.id}`)}
onClick={() => router.push(`/darmasaba/desa/berita/${item.id}`)}
style={{ cursor: 'pointer' }}
>
<Card.Section>

View File

@@ -94,7 +94,7 @@ function Semua() {
<Button
variant="light"
rightSection={<IconArrowRight size={16} />}
onClick={() => router.push(`/desa/berita/${featuredData.id}`)}
onClick={() => router.push(`/darmasaba/desa/berita/${featuredData.kategoriBerita?.name}/${featuredData.id}`)}
>
Baca Selengkapnya
</Button>
@@ -155,7 +155,7 @@ function Semua() {
})}
</Text>
<Button p="xs" variant="light" rightSection={<IconArrowRight size={16} />} onClick={() => router.push(`/desa/berita/${item.id}`)}>Baca Selengkapnya</Button>
<Button p="xs" variant="light" rightSection={<IconArrowRight size={16} />} onClick={() => router.push(`/darmasaba/desa/berita/${item.kategoriBerita?.name}/${item.id}`)}>Baca Selengkapnya</Button>
</Flex>
</Card>
))}

View File

@@ -4,8 +4,8 @@
import { TextInput } from '@mantine/core';
import { IconSearch } from '@tabler/icons-react';
import { useRouter, useSearchParams } from 'next/navigation';
import React, { useState } from 'react';
import { usePathname, useSearchParams } from 'next/navigation';
import { useEffect, useRef, useState } from 'react';
export type SearchBarProps = {
placeholder?: string;
@@ -16,31 +16,54 @@ export function SearchBar({
placeholder = "pencarian",
searchIcon = <IconSearch size={20} />,
}: SearchBarProps) {
const router = useRouter();
const searchParams = useSearchParams();
// Get initial search value from URL
const [searchValue, setSearchValue] = useState(searchParams.get('search') || '');
// Handle search input change with debounce
const pathname = usePathname();
const typingTimeoutRef = useRef<number | null>(null);
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
setSearchValue(value);
// Update URL with debounce
const params = new URLSearchParams(searchParams.toString());
if (value) {
params.set('search', value);
} else {
params.delete('search');
// Clear previous timeout
if (typingTimeoutRef.current) {
window.clearTimeout(typingTimeoutRef.current);
}
// Only update URL if the search value has actually changed
if (params.toString() !== searchParams.toString()) {
router.push(`?${params.toString()}`);
}
// Set new timeout
typingTimeoutRef.current = window.setTimeout(() => {
const params = new URLSearchParams(searchParams.toString());
// Always reset to first page when search changes
params.set('page', '1');
if (value) {
params.set('search', value);
} else {
params.delete('search');
}
// Update URL without adding to history
const newUrl = `${pathname}?${params.toString()}`;
window.history.replaceState({ ...window.history.state, as: newUrl, url: newUrl }, '', newUrl);
// Dispatch a custom event that content components can listen to
window.dispatchEvent(new CustomEvent('searchUpdate', { detail: { search: value } }));
}, 500);
};
// Clean up timeout on unmount
useEffect(() => {
return () => {
if (typingTimeoutRef.current) {
window.clearTimeout(typingTimeoutRef.current);
}
};
}, []);
return (
<TextInput
radius="lg"

View File

@@ -2,7 +2,7 @@
import colors from '@/con/colors';
import { Box, Center, Image, Pagination, Paper, SimpleGrid, Stack, Text } from '@mantine/core';
import { useEffect, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import ApiFetch from '@/lib/api-fetch';
interface FileItem {
@@ -23,18 +23,20 @@ interface FileItem {
const [page, setPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
// ✅ Load data function
const load = async (pageNum: number, limit: number, searchTerm: string) => {
setLoading(true);
// Handle search and pagination changes
const loadData = useCallback((pageNum: number, searchTerm: string) => {
setLoading(true);
// Using the load function from the component's scope
const loadFn = async () => {
try {
const query: Record<string, string> = {
category: 'image',
page: pageNum.toString(),
limit: limit.toString(),
};
if (searchTerm) query.search = searchTerm;
const response = await ApiFetch.api.fileStorage.findMany.get({ query });
const response = await ApiFetch.api.fileStorage.findMany.get({
query: {
category: 'image',
page: pageNum.toString(),
limit: '10',
...(searchTerm && { search: searchTerm })
}
});
if (response.status === 200 && response.data) {
setFiles(response.data.data || []);
@@ -49,14 +51,44 @@ interface FileItem {
setLoading(false);
}
};
// ✅ Baca dari URL — AMAN karena ssr: false
useEffect(() => {
loadFn();
}, []);
// Initial load and URL change handler
useEffect(() => {
const handleRouteChange = () => {
const urlParams = new URLSearchParams(window.location.search);
const urlSearch = urlParams.get('search') || '';
const urlPage = parseInt(urlParams.get('page') || '1');
setSearch(urlSearch);
load(1, 10, urlSearch.trim());
}, []);
setPage(urlPage);
loadData(urlPage, urlSearch);
};
// Handle search updates from the search bar
const handleSearchUpdate = (e: Event) => {
const { search } = (e as CustomEvent).detail;
setSearch(search);
setPage(1); // Reset to first page on new search
loadData(1, search);
};
// Initial load
handleRouteChange();
// Set up event listeners
window.addEventListener('popstate', handleRouteChange);
window.addEventListener('searchUpdate', handleSearchUpdate as EventListener);
// Cleanup
return () => {
window.removeEventListener('popstate', handleRouteChange);
window.removeEventListener('searchUpdate', handleSearchUpdate as EventListener);
};
}, [loadData]);
// ✅ Fetch data
useEffect(() => {

View File

@@ -1,34 +1,60 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client';
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
import colors from '@/con/colors';
import { Box, Center, Pagination, Paper, SimpleGrid, Spoiler, Stack, Text } from '@mantine/core';
import { useEffect, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { useSnapshot } from 'valtio';
export default function VideoContent() {
const [expanded, setExpanded] = useState(false);
const [currentSearch, setCurrentSearch] = useState('');
const videoState = useSnapshot(stateGallery.video);
const {
data,
page,
totalPages,
loading,
load,
} = videoState.findMany;
// ✅ Baca dari URL hanya di client
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
const urlSearch = urlParams.get('search') || '';
setCurrentSearch(urlSearch);
load(1, 10, urlSearch.trim());
// Handle search and pagination changes
const loadData = useCallback((pageNum: number, searchTerm: string) => {
stateGallery.video.findMany.load(pageNum, 10, searchTerm.trim());
}, []);
// Initial load and URL change handler
useEffect(() => {
const handleRouteChange = () => {
const urlParams = new URLSearchParams(window.location.search);
const urlSearch = urlParams.get('search') || '';
const urlPage = parseInt(urlParams.get('page') || '1');
loadData(urlPage, urlSearch);
};
// Handle search updates from the search bar
const handleSearchUpdate = (e: Event) => {
const { search } = (e as CustomEvent).detail;
loadData(1, search);
};
// Initial load
handleRouteChange();
// Set up event listeners
window.addEventListener('popstate', handleRouteChange);
window.addEventListener('searchUpdate', handleSearchUpdate as EventListener);
// Cleanup
return () => {
window.removeEventListener('popstate', handleRouteChange);
window.removeEventListener('searchUpdate', handleSearchUpdate as EventListener);
};
}, [loadData]);
const handlePageChange = (newPage: number) => {
load(newPage, 10, currentSearch.trim());
const params = new URLSearchParams(window.location.search);
const search = params.get('search') || '';
loadData(newPage, search);
};
const dataVideo = data || [];

View File

@@ -7,7 +7,7 @@ import VisimisiDesa from './ui/visimisiDesa';
import LambangDesa from './ui/lambangDesa';
import MaskotDesa from './ui/maskotDesa';
import ProfilPerbekel from './ui/profilPerbekel';
import LembagaDesa from './ui/lembagaDesa';
// import LembagaDesa from './ui/lembagaDesa';
import MotoDesa from './ui/motoDesa';
import SemuaPerbekel from './ui/semuaPerbekel';
@@ -31,7 +31,7 @@ function Page() {
<LambangDesa />
<MaskotDesa />
<ProfilPerbekel />
<LembagaDesa />
{/* <LembagaDesa /> */}
<MotoDesa />
<SemuaPerbekel/>
</Box>

View File

@@ -97,7 +97,7 @@ function PengaduanMasyarakat() {
>
<Paper p="md" withBorder>
<Stack gap="xs">
<Title order={3}>Ajukan Administrasi Online</Title>
<Title order={3}>Ajukan Pengaduan Masyarakat</Title>
<TextInput
label={<Text fz="sm" fw="bold">Nama</Text>}
placeholder="masukkan nama"

View File

@@ -50,7 +50,7 @@ function Page() {
data={data}
dataKey="kategori"
series={[
{ name: 'jumlah', color: 'violet.6' },
{ name: 'jumlah', color: colors['blue-button'] },
]}
tickLine="y"
xAxisProps={{

View File

@@ -49,7 +49,7 @@ function Page() {
return (
<Stack pos={"relative"} bg={colors['BG-trans']} py={"xl"} gap={22}>
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={22}>
<Box px={{ base: "md", md: 100 }}>
<BackButton />
</Box>

View File

@@ -87,9 +87,8 @@ function Kepuasan() {
<Flex justify={"space-between"} align={"center"}>
<Text fw={"bold"}>Pelayanan Terhadap Publik Desa Darmasaba</Text>
<Box>
<Text fz={"h1"} fw={"bold"} c={colors["blue-button"]}>95.00</Text>
<Text >Sangat Baik</Text>
<Text fz={"sm"} fw={"bold"} c={colors["blue-button"]}>Total 2500 responden</Text>
<Text fz={"sm"} fw={"bold"} c={colors["blue-button"]}>Total Responden</Text>
<Text fz={"h1"} fw={"bold"} c={colors["blue-button"]}>2500</Text>
</Box>
</Flex>
<BarChart

View File

@@ -0,0 +1,60 @@
import colors from '@/con/colors';
import { Box, Card, Image, Stack, Text } from '@mantine/core';
import React from 'react';
import { Prisma } from '@prisma/client';
interface ProfileViewProps {
data: Prisma.PejabatDesaGetPayload<{ include: { image: true } }> | null;
}
function ProfileView({ data }: ProfileViewProps) {
if (!data) {
return <div>No profile data available</div>;
}
return (
<Stack
justify="end"
align="end"
pos="relative"
w={{
base: "100%",
md: "40%",
}}
px="xl"
>
{data.image?.link && (
<Image
src={data.image.link}
alt={data.name || "Profile image"}
sizes="100%"
fit="contain"
/>
)}
<Box
pos="absolute"
bottom={0}
p={{
base: "xs",
md: "md",
}}
>
<Card
px="lg"
radius="32"
className="glass3"
style={{
border: `1px solid white`,
}}
>
<Text>{data.position}</Text>
<Text c={colors["blue-button"]} fw="bolder" fz="1rem">
{data.name}
</Text>
</Card>
</Box>
</Stack>
);
}
export default ProfileView;

View File

@@ -16,6 +16,7 @@ import {
import { useEffect, useState } from "react";
import ModuleView from "./ModuleView";
import SosmedView from "./SosmedView";
import ProfileView from "./ProfileView";
const getDayOfWeek = () => {
const days = ['Minggu', 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu'];
@@ -59,6 +60,7 @@ const getWorkStatus = (day: string, currentTime: string): { status: string; mess
function LandingPage() {
const [socialMedia, setSocialMedia] = useState<Prisma.MediaSosialGetPayload<{ include: { image: true } }>[]>([]);
const [profile, setProfile] = useState<Prisma.PejabatDesaGetPayload<{ include: { image: true } }> | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
@@ -87,7 +89,21 @@ function LandingPage() {
}
};
const fetchProfile = async () => {
try {
const response = await fetch(`/api/landingpage/pejabatdesa/edit`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setProfile(result.data || null); // Handle single object response
} catch (error) {
console.error('Error fetching profile:', error);
setProfile(null);
}
};
fetchSocialMedia();
fetchProfile();
}, []);
const [workStatus, setWorkStatus] = useState<{ status: string; message: string }>
@@ -296,45 +312,13 @@ function LandingPage() {
</Card>
</Stack>
<Stack
justify={"end"}
align={"end"}
pos={"relative"}
w={{
base: "100%",
md: "40%",
}}
px={"xl"}
>
<Image
src={"/assets/images/perbekel.png"}
alt="perbekel"
sizes="100%"
fit="contain"
/>
<Box
pos={"absolute"}
bottom={0}
p={{
base: "xs",
md: "md",
}}
>
<Card
px={"lg"}
radius={"32"}
className="glass3"
style={{
border: `1px solid white`,
}}
>
<Text>Perbekel Desa Darmasaba</Text>
<Text c={colors["blue-button"]} fw={"bolder"} fz={"1rem"}>
I.B. Surya Prabhawa Manuaba, S.H.,M.H.,NL.P.
</Text>
</Card>
</Box>
</Stack>
{isLoading ? (
<Skeleton height={32} width="100%" />
) : profile ? (
<ProfileView data={profile} />
) : (
<div>No profile available</div>
)}
</Flex>
</Stack >
);