Sinkronisasi UI Admin & User Menu Landing Page, Submenu Profile, SDGSDesa

This commit is contained in:
2025-08-04 10:29:13 +08:00
parent 54312e9486
commit 1cdff53c56
17 changed files with 490 additions and 72 deletions

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -52,19 +53,38 @@ const sdgsDesa = proxy({
},
},
findMany: {
data: null as Array<
Prisma.SDGSDesaGetPayload<{
include: {
image: true;
};
}>
> | null,
async load() {
const res = await ApiFetch.api.landingpage.sdgsdesa[
"find-many"
].get();
if (res.status === 200) {
sdgsDesa.findMany.data = res.data?.data ?? [];
data: null as any[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
load: async (page = 1, limit = 10) => { // Change to arrow function
sdgsDesa.findMany.loading = true; // Use the full path to access the property
sdgsDesa.findMany.page = page;
try {
const res = await ApiFetch.api.landingpage.sdgsdesa[
"findMany"
].get({
query: { page, limit },
});
if (res.status === 200 && res.data?.success) {
sdgsDesa.findMany.data = res.data.data || [];
sdgsDesa.findMany.total = res.data.total || 0;
sdgsDesa.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error("Failed to load media sosial:", res.data?.message);
sdgsDesa.findMany.data = [];
sdgsDesa.findMany.total = 0;
sdgsDesa.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading media sosial:", error);
sdgsDesa.findMany.data = [];
sdgsDesa.findMany.total = 0;
sdgsDesa.findMany.totalPages = 1;
} finally {
sdgsDesa.findMany.loading = false;
}
},
},

View File

@@ -68,7 +68,7 @@ function ListKategoriKegiatan({ search }: { search: string }) {
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={300} />
<Skeleton height={550} />
</Stack>
);
}

View File

@@ -59,7 +59,7 @@ function ListDesaAntiKorupsi({ search }: { search: string }) {
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={300} />
<Skeleton height={550} />
</Stack>
);
}

View File

@@ -57,7 +57,7 @@ function ListMediaSosial({ search }: { search: string }) {
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={300} />
<Skeleton height={550} />
</Stack>
);
}

View File

@@ -57,7 +57,7 @@ function ListProgramInovasi({ search }: { search: string }) {
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={300} />
<Skeleton height={550} />
</Stack>
);
}

View File

@@ -163,7 +163,7 @@ function EditKolaborasiInovasi() {
<iframe
src={previewImage}
width="100%"
height="500px"
height="250px"
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
/>
) : (

View File

@@ -110,7 +110,7 @@ function CreateSDGsDesa() {
<iframe
src={previewImage}
width="100%"
height="500px"
height="250px"
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
/>
) : (

View File

@@ -1,10 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import sdgsDesa from '../../_state/landing-page/sdgs-desa';
@@ -30,24 +30,61 @@ function SdgsDesa() {
function ListSdgsDesa({ search }: { search: string }) {
const listState = useProxy(sdgsDesa)
const router = useRouter();
const {
data,
page,
totalPages,
loading,
load,
} = listState.findMany;
useEffect(() => {
listState.findMany.load()
load(page, 10)
}, [])
const filteredData = (listState.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.jumlah.toLowerCase().includes(keyword)
)
});
const filteredData = useMemo(() => {
if (!data) return [];
return data.filter(item => {
const keyword = search.toLowerCase();
return (
item.name?.toLowerCase().includes(keyword) ||
item.jumlah?.toLowerCase().includes(keyword)
);
})
}, [data, search]);
if (!listState.findMany.data) {
// Handle loading state
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton h={500} />
<Skeleton height={550} />
</Stack>
)
);
}
if (data.length === 0) {
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Desa Anti Korupsi'
href='/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/create'
/>
<Box style={{ overflowX: "auto" }}>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama SDGs Desa</TableTh>
<TableTh>Jumlah SDGs Desa</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
</Table>
</Box>
</Paper>
</Box>
);
}
return (
@@ -90,6 +127,18 @@ function ListSdgsDesa({ search }: { search: string }) {
</Box>
</Stack>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={totalPages}
mt="md"
mb="md"
/>
</Center>
</Box>
)
}

View File

@@ -16,7 +16,7 @@ async function sdgsDesaFindMany(context: Context) {
},
skip,
take: limit,
orderBy: { createdAt: "desc" }, // opsional, kalau mau urut berdasarkan waktu
orderBy: { jumlah: "desc" }, // opsional, kalau mau urut berdasarkan waktu
}),
prisma.sDGSDesa.count({
where: { isActive: true },

View File

@@ -11,7 +11,7 @@ const SDGSDesa = new Elysia({
})
// ✅ Find all
.get("/find-many", sdgsDesaFindMany)
.get("/findMany", sdgsDesaFindMany)
// ✅ Find by ID
.get("/:id", sdgsDesaFindUnique)

View File

@@ -1,11 +1,43 @@
import { Box, Center, Container, Image, Stack, Text } from '@mantine/core';
'use client'
import { Box, Center, Container, Image, LoadingOverlay, Paper, SimpleGrid, Stack, Text, Title } from '@mantine/core';
import { Prisma } from '@prisma/client';
import { useEffect, useState } from 'react';
import BackButton from '../../(pages)/desa/layanan/_com/BackButto';
import colors from '@/con/colors';
function Page() {
return (
const [sdgsDesa, setSdgsDesa] = useState<Prisma.SDGSDesaGetPayload<{ include: { image: true } }>[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchSdgsDesa = async () => {
try {
const response = await fetch('/api/landingpage/sdgsdesa/findMany');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
// Ensure the data is an array before setting it
let data = [];
if (Array.isArray(result.data)) {
data = result.data;
} else if (Array.isArray(result)) {
// In case the API returns the array directly
data = result;
} else {
console.error('Invalid data format:', result);
}
setSdgsDesa(data);
} catch (error) {
console.error('Error fetching sdgs desa:', error);
} finally {
setLoading(false);
}
};
fetchSdgsDesa();
}, []);
return (
<Stack pos={"relative"} py={"xl"} gap={22}>
<Box px={{ base: "md", md: 100 }}><BackButton /></Box>
<Container w={{ base: "100%", md: "50%" }}>
@@ -28,12 +60,70 @@ function Page() {
>
SDGs Desa sebagaimana dijabarkan dalam Permendesa Nomor 21 tahun 2020 terdiri dari 18 tujuan yang harus dicapai pada tahun 2030. Tujuan-tujuan tersebut mencakup berbagai aspek kehidupan masyarakat desa, mulai dari pengentasan kemiskinan, peningkatan kesehatan dan pendidikan, kesetaraan gender, pertumbuhan ekonomi, infrastruktur, hingga kelestarian lingkungan. Adapun SDGs Desa terdiri dari tujuan-tujuan sebagai berikut:
</Text>
<Center>
<Image src={"/api/img/sdgsdesa-18-removebg.png"} alt='' w={800} />
</Center>
</Box >
<Box py={20}>
<Box py={20} px={{ base: "md", md: 100 }}>
<Box pos="relative" style={{ minHeight: 200 }}>
<LoadingOverlay visible={loading} overlayProps={{ blur: 2 }} />
{!loading && sdgsDesa.length > 0 ? (
<SimpleGrid
cols={{ base: 1, sm: 2, md: 3, lg: 4 }}
spacing="xl"
verticalSpacing="xl"
>
{sdgsDesa.map((item) => (
<Paper
key={item.id}
p="md"
radius="md"
shadow="sm"
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
height: '100%',
transition: 'transform 0.2s',
'&:hover': {
transform: 'translateY(-5px)',
boxShadow: '0 8px 16px rgba(0,0,0,0.1)'
}
}}
>
<Box
p="md"
style={{
backgroundColor: '#f8f9fa',
borderRadius: '8px',
width: '100%',
display: 'flex',
justifyContent: 'center',
marginBottom: '1rem',
}}
>
<Image
src={item.image?.link || '/placeholder-sdgs.png'}
alt={item.name}
width={120}
height={120}
fit="contain"
/>
</Box>
<Stack gap="xs" style={{ width: '100%' }}>
<Title order={4} ta="center" c={"dimmed"} fw={600} lineClamp={2} style={{ minHeight: '3rem' }}>
{item.name}
</Title>
<Text ta="center" fw={"bold"} c={colors['blue-button']} fz={"h2"} lineClamp={3} style={{ minHeight: '4.5rem' }}>
{item.jumlah}
</Text>
</Stack>
</Paper>
))}
</SimpleGrid>
) : !loading ? (
<Center style={{ minHeight: 200 }}>
<Text>Tidak ada data SDGs Desa yang tersedia</Text>
</Center>
) : null}
</Box>
</Box>
</Stack >
);

View File

@@ -112,7 +112,7 @@ function Footer() {
</Box>
</Paper>
</Center>
<Box py={20}>
<Box py={20} >
<SimpleGrid
p={20}
cols={{
@@ -123,7 +123,7 @@ function Footer() {
color: "white"
}}
>
<Box p={mobile ? 30 : 30}>
<Box p={mobile ? 30 : 30} style={{color: "white"}}>
<Stack justify='space-between'>
<Text fz={"md"} fw={"bold"}>Tentang Darmasaba</Text>
<Text fz={"xs"} >Desa Darmasaba adalah desa

View File

@@ -1,11 +1,14 @@
import images from "@/con/images";
import { ActionIcon, Flex, Image } from "@mantine/core";
import { Prisma } from "@prisma/client";
import { useTransitionRouter } from "next-view-transitions";
function SosmedView() {
const listSosmed = Object.values(images.sosmed);
function SosmedView({data} : {data : Prisma.MediaSosialGetPayload<{ include: { image: true } }>[]}) {
const router = useTransitionRouter();
return (
<Flex gap={"md"} justify={"center"} align={"center"}>
{listSosmed.map((item, k) => {
{data?.map((item, k) => {
return (
<ActionIcon
variant="transparent"
@@ -13,8 +16,11 @@ function SosmedView() {
w={32}
h={32}
pos={"relative"}
onClick={() => {
router.push(item.iconUrl || "");
}}
>
<Image src={item} alt="icon" loading="lazy" />
<Image src={item.image?.link || ""} alt="icon" loading="lazy" />
</ActionIcon>
);
})}

View File

@@ -1,8 +1,10 @@
"use client";
import colors from "@/con/colors";
import { Prisma } from "@prisma/client";
import {
Box,
Card,
Skeleton,
Flex,
Grid,
GridCol,
@@ -11,9 +13,9 @@ import {
Stack,
Text
} from "@mantine/core";
import { useEffect, useState } from "react";
import ModuleView from "./ModuleView";
import SosmedView from "./SosmedView";
import { useEffect, useState } from "react";
const getDayOfWeek = () => {
const days = ['Minggu', 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu'];
@@ -56,6 +58,38 @@ const getWorkStatus = (day: string, currentTime: string): { status: string; mess
function LandingPage() {
const [socialMedia, setSocialMedia] = useState<Prisma.MediaSosialGetPayload<{ include: { image: true } }>[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const fetchSocialMedia = async () => {
try {
const response = await fetch('/api/landingpage/mediasosial/findMany');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
// Ensure the data is an array before setting it
if (Array.isArray(result.data)) {
setSocialMedia(result.data);
} else if (Array.isArray(result)) {
// In case the API returns the array directly
setSocialMedia(result);
} else {
console.error('Unexpected API response format:', result);
setSocialMedia([]);
}
} catch (error) {
console.error('Error fetching social media:', error);
setSocialMedia([]); // Ensure we always have an array
} finally {
setIsLoading(false);
}
};
fetchSocialMedia();
}, []);
const [workStatus, setWorkStatus] = useState<{ status: string; message: string }>
({ status: '', message: '' });
@@ -248,7 +282,13 @@ function LandingPage() {
</Flex>
<ModuleView />
<SosmedView />
{isLoading ? (
<Skeleton height={32} width="100%" />
) : socialMedia.length > 0 ? (
<SosmedView data={socialMedia} />
) : (
<div>No social media links available</div>
)}
<Text c={colors.trans.dark[2]} style={{
textAlign: "center"
}} >Sampaikan saran dan masukan guna kemajuan dalam pembangunan. Semua lebih mudah melalui fitur interaktif</Text>

View File

@@ -1,12 +1,51 @@
'use client'
import colors from "@/con/colors";
import { Box, Button, Center, Container, Image, Paper, SimpleGrid, Stack, Text, useMantineTheme } from "@mantine/core";
import { Box, Button, Center, Container, Image, Paper, SimpleGrid, Stack, Text, Title, useMantineTheme } from "@mantine/core";
import { useMediaQuery } from "@mantine/hooks";
import { Prisma } from "@prisma/client";
import Link from "next/link";
import { useEffect, useState } from "react";
export default function SDGS() {
const theme = useMantineTheme();
const mobile = useMediaQuery(`(max-width: ${theme.breakpoints.sm})`);
const [sdgsDesa, setSdgsDesa] = useState<Prisma.SDGSDesaGetPayload<{ include: { image: true } }>[] | null>(null);
useEffect(() => {
const fetchSdgsDesa = async () => {
try {
const response = await fetch('/api/landingpage/sdgsdesa/findMany');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
// Ensure the data is an array before setting it
let data = [];
if (Array.isArray(result.data)) {
data = result.data;
} else if (Array.isArray(result)) {
// In case the API returns the array directly
data = result;
} else {
console.error('Unexpected API response format:', result);
setSdgsDesa([]);
return;
}
// Sort by jumlah in descending order and take top 3
const top3Sdgs = [...data]
.sort((a, b) => parseInt(b.jumlah) - parseInt(a.jumlah))
.slice(0, 3);
setSdgsDesa(top3Sdgs);
} catch (error) {
console.error('Error fetching sdgs desa:', error);
setSdgsDesa([]);
}
};
fetchSdgsDesa();
}, []);
return (
<Stack p={"sm"}>
<Container w={{ base: "100%", md: "80%" }} p={"xl"}>
@@ -16,22 +55,82 @@ export default function SDGS() {
<Text fz={"1.4rem"} ta={"center"}>SDGs Desa adalah upaya menerapkan 17 Tujuan Pembangunan Berkelanjutan di tingkat desa.
Dengan fokus pada pengentasan kemiskinan, pendidikan, kesehatan, kesetaraan gender, dan pelestarian lingkungan, kami berkomitmen untuk menciptakan desa yang lebih baik bagi semua</Text>
<Box py={50}>
<Paper p={"lg"} bg={colors.Bg}>
<SimpleGrid
cols={{
base: 1,
sm: 3,
}}>
<Center>
<Image src={"/api/img/sgdesa-1.png"} alt="" w={mobile ? 250 : 200} />
</Center>
<Center>
<Image src={"/api/img/sgdesa-2.png"} alt="" w={mobile ? 250 : 220} />
</Center>
<Center>
<Image src={"/api/img/sgdesa-3.png"} alt="" w={mobile ? 250 : 190} />
</Center>
</SimpleGrid>
<Paper p={{ base: 'md', md: 'xl' }} bg={colors.Bg} radius="lg" shadow="sm">
{sdgsDesa && sdgsDesa.length > 0 ? (
<SimpleGrid
cols={{ base: 1, sm: 3 }}
spacing="xl"
verticalSpacing="xl"
>
{sdgsDesa.map((item) => (
<Box
key={item.id}
p="md"
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
transition: 'transform 0.2s',
'&:hover': {
transform: 'translateY(-5px)'
}
}}
>
<Box
p="md"
style={{
backgroundColor: 'white',
width: mobile ? 150 : 180,
height: mobile ? 150 : 180,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: '1.5rem',
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)'
}}
>
<Image
src={item.image?.link ? item.image.link : '/placeholder-sdgs.png'}
alt={item.name}
width={mobile ? 100 : 120}
height={mobile ? 100 : 120}
style={{
objectFit: 'contain',
maxWidth: '100%',
maxHeight: '100%'
}}
/>
</Box>
<Text
ta="center"
fz={{ base: 'lg', md: 'xl' }}
fw={700}
mb="xs"
style={{ lineHeight: 1.2 }}
>
{item.name}
</Text>
<Title
order={2}
ta="center"
c={colors['blue-button']}
style={{
fontSize: mobile ? '2.5rem' : '3rem',
lineHeight: 1,
margin: '0.5rem 0',
fontWeight: 800
}}
>
{item.jumlah}
</Title>
</Box>
))}
</SimpleGrid>
) : (
<Text>Tidak ada data SDGs Desa</Text>
)}
</Paper>
<Center>
<Button component={Link} href={"/darmasaba/sdgs-desa"} radius={"lg"} fz={"1.2rem"} mt={20} bg={colors["blue-button"]}>Selengkapnya</Button>