Fix konsisten font, menu landing page & PPID

This commit is contained in:
2025-12-10 17:44:31 +08:00
parent 99c2c9c6d7
commit 242ea86f77
25 changed files with 1505 additions and 700 deletions

View File

@@ -168,6 +168,7 @@ export default function ModernNewsNotification({
position: "fixed",
bottom: "24px",
right: "24px",
zIndex: 1
}}
>
<ActionIcon

View File

@@ -100,6 +100,7 @@ const NewsReaderLanding = () => {
borderBottomRightRadius: '20px',
borderTopRightRadius: '20px',
transition: 'all 0.3s ease',
zIndex: 1
}}
>
{isPointerMode ? <IconMusicOff /> : <IconMusic />}

View File

@@ -5,7 +5,20 @@ import apbdes from '@/app/admin/(dashboard)/_state/landing-page/apbdes'
import APBDesProgress from '@/app/darmasaba/(tambahan)/apbdes/lib/apbDesaProgress'
import { transformAPBDesData } from '@/app/darmasaba/(tambahan)/apbdes/lib/types'
import colors from '@/con/colors'
import { ActionIcon, BackgroundImage, Box, Button, Center, Group, Loader, Select, SimpleGrid, Stack, Text } from '@mantine/core'
import {
ActionIcon,
BackgroundImage,
Box,
Button,
Center,
Group,
Loader,
Select,
SimpleGrid,
Stack,
Text,
Title,
} from '@mantine/core'
import { IconDownload } from '@tabler/icons-react'
import Link from 'next/link'
import { useEffect, useState } from 'react'
@@ -38,17 +51,15 @@ function Apbdes() {
const dataAPBDes = state.findMany.data || []
const years = Array.from(new Set(dataAPBDes.map((item: any) => item.tahun)))
.sort((a, b) => b - a) // urutkan descending
.sort((a, b) => b - a)
.map(year => ({ value: year.toString(), label: `Tahun ${year}` }))
// Pilih tahun pertama sebagai default jika belum ada yang dipilih
useEffect(() => {
if (years.length > 0 && !selectedYear) {
setSelectedYear(years[0].value)
}
}, [years, selectedYear])
// Transform and filter data based on selected year
const currentApbdes = dataAPBDes.length > 0
? transformAPBDesData(dataAPBDes.find(item => item?.tahun?.toString() === selectedYear) || dataAPBDes[0])
: null
@@ -57,17 +68,31 @@ function Apbdes() {
return (
<Stack p="sm" gap="xl" bg={colors.Bg}>
<Box mt={"xl"}>
{/* 📌 HEADING */}
<Box mt="xl">
<Stack gap="sm">
<Text c={colors["blue-button"]} ta={"center"} fw={"bold"} fz={{ base: "1.8rem", md: "3.4rem" }}>
<Title
order={1}
ta="center"
c={colors['blue-button']}
fz={{ base: '2rem', md: '3.6rem' }}
lh={{ base: 1.2, md: 1.1 }}
>
{textHeading.title}
</Text>
<Text ta={"center"} fz={{ base: "1rem", md: "1.3rem" }}>
</Title>
<Text
ta="center"
fz={{ base: '1rem', md: '1.25rem' }}
lh={{ base: 1.5, md: 1.55 }}
c="black"
>
{textHeading.des}
</Text>
</Stack>
</Box>
{/* Button Lihat Semua */}
<Group justify="center">
<Button
component={Link}
@@ -81,32 +106,39 @@ function Apbdes() {
</Button>
</Group>
{/* 🔥 COMBOBOX UNTUK PILIH TAHUN */}
{/* COMBOBOX */}
<Box px={{ base: 'md', md: 100 }}>
<Select
label="Pilih Tahun APBDes"
label={<Text fw={600} fz="sm">Pilih Tahun APBDes</Text>}
placeholder="Pilih tahun"
value={selectedYear}
onChange={setSelectedYear}
data={years}
w={{ base: '100%', sm: 200 }}
w={{ base: '100%', sm: 220 }}
searchable
clearable
nothingFoundMessage="Tidak ada tahun tersedia"
/>
</Box>
{/* Progress */}
{currentApbdes ? (
<>
<APBDesProgress apbdesData={currentApbdes} />
</>
<APBDesProgress apbdesData={currentApbdes} />
) : (
<Box px={{ base: 'md', md: 100 }} py="md">
<Text c="dimmed">Tidak ada data APBDes untuk tahun yang dipilih.</Text>
<Text fz="sm" c="dimmed" ta="center" lh={1.5}>
Tidak ada data APBDes untuk tahun yang dipilih.
</Text>
</Box>
)}
<SimpleGrid mx={{ base: 'md', md: 100 }} cols={{ base: 1, sm: 3 }} spacing="lg" pb={"xl"}>
{/* GRID */}
<SimpleGrid
mx={{ base: 'md', md: 100 }}
cols={{ base: 1, sm: 3 }}
spacing="lg"
pb="xl"
>
{loading ? (
<Center mih={200}>
<Loader size="lg" color="blue" />
@@ -114,10 +146,10 @@ function Apbdes() {
) : data.length === 0 ? (
<Center mih={200}>
<Stack align="center" gap="xs">
<Text fz="lg" c="dimmed">
<Text fz="lg" c="dimmed" lh={1.4}>
Belum ada data APBDes yang tersedia
</Text>
<Text fz="sm" c="dimmed">
<Text fz="sm" c="dimmed" lh={1.4}>
Data akan ditampilkan di sini setelah diunggah
</Text>
</Stack>
@@ -133,25 +165,30 @@ function Apbdes() {
style={{ overflow: 'hidden' }}
>
<Box pos="absolute" inset={0} bg="rgba(0,0,0,0.45)" style={{ borderRadius: 16 }} />
<Stack gap={"xs"} justify="space-between" h="100%" p="xl" pos="relative">
<Stack gap="xs" justify="space-between" h="100%" p="xl" pos="relative">
<Text
c="white"
fw={600}
fz="lg"
fz={{ base: 'lg', md: 'xl' }}
ta="center"
lh={1.35}
lineClamp={2}
>
{v.name}
</Text>
<Text
fw="bold"
fw={700}
c="white"
fz="3rem"
fz={{ base: '2.4rem', md: '3.2rem' }}
ta="center"
lh={1}
style={{ textShadow: '0 2px 8px rgba(0,0,0,0.6)' }}
>
{v.jumlah}
</Text>
<Center>
<ActionIcon
component={Link}
@@ -163,29 +200,12 @@ function Apbdes() {
>
<IconDownload size={20} color="white" />
</ActionIcon>
</Center>
{/* <Group justify="center">
<ActionIcon
component={Link}
href={v.file?.link || ''}
radius="xl"
size="lg"
variant="gradient"
gradient={{ from: '#1C6EA4', to: '#1C6EA4' }}
>
<Group align="center" gap="xs" px="md" py={6}>
<IconDownload size={25} color="white" />
</Group>
</ActionIcon>
</Group> */}
</Stack>
</BackgroundImage>
))
)}
</SimpleGrid>
</Stack>
)
}

View File

@@ -2,7 +2,16 @@
'use client'
import korupsiState from "@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi";
import colors from "@/con/colors";
import { Button, Center, Container, Flex, Paper, SimpleGrid, Stack, Text } from "@mantine/core";
import {
Button,
Center,
Container,
Flex,
Paper,
SimpleGrid,
Stack,
Text
} from "@mantine/core";
import { IconClipboardText } from "@tabler/icons-react";
import Link from "next/link";
import { useEffect, useState } from "react";
@@ -11,7 +20,6 @@ import { useProxy } from "valtio/utils";
function DesaAntiKorupsi() {
const state = useProxy(korupsiState);
const [loading, setLoading] = useState(false);
useEffect(() => {
const loadData = async () => {
@@ -19,30 +27,64 @@ function DesaAntiKorupsi() {
setLoading(true);
await state.desaAntikorupsi.findMany.load();
} catch (error) {
console.error('Error loading data:', error);
console.error("Error loading data:", error);
} finally {
setLoading(false);
}
}
};
loadData();
}, [])
}, []);
const data = (state.desaAntikorupsi.findMany.data || []).slice(0, 6);
return (
<Stack gap={"0"} bg={colors.Bg} p={"sm"} my={"xs"}>
<Container w={{ base: "100%", md: "80%" }} p={"md"} >
<Stack gap="0" bg={colors.Bg} p="sm" my="xs">
{/* ===================== HEADER ===================== */}
<Container w={{ base: "100%", md: "80%" }} p="md">
<Center>
<Text fw={"bold"} c={colors["blue-button"]} fz={{ base: "1.8rem", md: "3.4rem" }}>Desa Anti Korupsi</Text>
<Text
fw={700}
ta="center"
c={colors["blue-button"]}
fz={{ base: "1.8rem", md: "3.2rem" }}
lh={{ base: "2.2rem", md: "3.4rem" }}
>
Desa Anti Korupsi
</Text>
</Center>
<Text ta={"center"} fz={{ base: "1rem", md: "1.3rem" }}>Desa antikorupsi mendorong pemerintahan jujur dan transparan. Keuangan desa dikelola terbuka dengan melibatkan warga mengawasi anggaran, sehingga digunakan tepat sasaran sesuai kebutuhan.</Text>
<Center py={20}>
<Button radius={"lg"} fz={"h4"} bg={colors["blue-button"]} component={Link} href={"/darmasaba/desa-anti-korupsi/detail"}>Selengkapnya</Button>
<Text
ta="center"
c="black"
fz={{ base: "1rem", md: "1.25rem" }}
lh={{ base: "1.5rem", md: "1.8rem" }}
mt="sm"
>
Desa antikorupsi mendorong pemerintahan jujur dan transparan.
Keuangan desa dikelola secara terbuka dengan melibatkan warga
dalam pengawasan anggaran, sehingga digunakan tepat sasaran dan
sesuai kebutuhan masyarakat.
</Text>
<Center py={25}>
<Button
radius="lg"
fz={{ base: "md", md: "lg" }}
bg={colors["blue-button"]}
component={Link}
href="/darmasaba/desa-anti-korupsi/detail"
style={{ paddingInline: "2rem" }}
>
Selengkapnya
</Button>
</Center>
</Container>
{/* ===================== LIST ===================== */}
<Container w="100%" maw="80rem" px="md">
{loading ? (
<Center mih={200}>
<Text fz="lg">Memuat Data...</Text>
<Text fz={{ base: "md", md: "lg" }}>Memuat Data...</Text>
</Center>
) : (
<SimpleGrid
@@ -64,26 +106,35 @@ function DesaAntiKorupsi() {
<IconClipboardText
color={colors["blue-button"]}
size={40}
style={{ flexShrink: 0 }} // biar icon nggak ketekan
style={{ flexShrink: 0 }}
/>
<Stack gap={2} style={{ flex: 1, minWidth: 0 }}>
<Stack gap={6} style={{ flex: 1, minWidth: 0 }}>
{/* Title */}
<Text
fz={{ base: "sm", sm: "md", md: "lg", lg: "xl" }} // lebih besar di desktop
fw={700}
c={colors["blue-button"]}
fw={600}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
fz={{ base: "1rem", sm: "1.1rem", md: "1.25rem" }}
lh={{ base: "1.3rem", md: "1.5rem" }}
style={{
wordBreak: "break-word",
whiteSpace: "normal"
}}
>
{v.kategori?.name || "Kategori"}
</Text>
{/* Description */}
<Text
dangerouslySetInnerHTML={{
__html: v.name || "Name",
__html: v.name || "Name"
}}
fz={{ base: "sm", sm: "md", md: "lg", lg: "xl" }} // sama, scaling responsif
c="dark"
fz={{ base: "0.9rem", sm: "1rem", md: "1.15rem" }}
lh={{ base: "1.3rem", md: "1.6rem" }}
style={{
wordBreak: "break-word",
whiteSpace: "normal",
whiteSpace: "normal"
}}
/>
</Stack>
@@ -91,7 +142,6 @@ function DesaAntiKorupsi() {
</Paper>
))}
</SimpleGrid>
)}
</Container>
</Stack>

View File

@@ -15,8 +15,6 @@ interface ChartDataItem {
label?: string;
}
function Kepuasan() {
const state = useProxy(indeksKepuasanState.responden);
const { data, loading } = state.findMany;
@@ -154,66 +152,88 @@ function Kepuasan() {
if (data.length === 0) {
return (
<Stack p="sm" my={"xs"}>
<Container w={{ base: "100%", md: "80%" }} p={"sm"}>
<Stack p="sm" my="xs">
<Container w={{ base: "100%", md: "80%" }} p="sm">
<Center>
<Text
<Title
order={2}
ta="center"
fz={{ base: '2rem', md: '2.8rem' }}
lh={{ base: 1.05, md: 1.04 }}
c={colors['blue-button']}
fw={800}
style={{ letterSpacing: '-0.5px' }}
>Indeks Kepuasan Masyarakat</Text>
>
Indeks Kepuasan Masyarakat
</Title>
</Center>
<Text ta={"center"} fz={{ base: "1rem", md: "1.3rem" }}>Ukur kebahagiaan warga, tingkatkan layanan desa! Dengan partisipasi aktif masyarakat, kami berkomitmen untuk terus memperbaiki layanan agar lebih transparan, efektif, dan sesuai dengan kebutuhan warga. Kepuasan Anda adalah prioritas utama kami dalam membangun desa yang lebih baik!</Text>
<Center mt={10}>
<Text
ta="center"
fz={{ base: "0.95rem", md: "1.25rem" }}
lh={{ base: 1.45, md: 1.5 }}
c="black"
mt="sm"
>
Ukur kebahagiaan warga, tingkatkan layanan desa! Dengan partisipasi aktif masyarakat, kami berkomitmen untuk terus memperbaiki layanan agar lebih transparan, efektif, dan sesuai dengan kebutuhan warga. Kepuasan Anda adalah prioritas utama kami dalam membangun desa yang lebih baik!
</Text>
<Center mt={12}>
<Button
radius={"lg"}
radius="lg"
onClick={open}
variant="gradient"
gradient={{ from: "#26667F", to: "#124170" }}
>Ajukan Responden</Button>
style={{ paddingLeft: 20, paddingRight: 20, fontWeight: 600 }}
>
<Text fz={{ base: "0.95rem", md: "1rem" }} ta="center" c="white">Ajukan Responden</Text>
</Button>
</Center>
</Container>
<Box px={"sm"}>
<Paper p={"lg"} bg={colors.Bg}>
<Paper p={"lg"}>
<Stack gap={"xs"}>
<Box px="sm">
<Paper p="lg" bg={colors.Bg}>
<Paper p="lg">
<Stack gap="xs">
<Flex
direction={{ base: "column", sm: "row" }}
justify="space-between"
align={{ base: "flex-start", sm: "center" }}
gap={{ base: "xs", sm: "md" }} // 👈 Tambahkan gap untuk memberi ruang
gap={{ base: "xs", sm: "md" }}
>
<Text
fw="bold"
fw={700}
ta={{ base: "center", sm: "left" }}
fz={{ base: "sm", sm: "md" }} // 👈 Atur ukuran font agar lebih proporsional di mobile
fz={{ base: "0.95rem", sm: "1rem" }}
lh={1.3}
>
Pelayanan Terhadap Publik Desa Darmasaba
</Text>
<Box
mt={{ base: "sm", sm: 0 }}
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-end', // 👈 Pastikan teks dan angka sejajar ke kanan
textAlign: 'right', // 👈 Agar teks rata kanan
alignItems: 'flex-end',
textAlign: 'right',
}}
>
<Text fz={{ base: "xs", sm: "sm" }} fw={"bold"} c={colors["blue-button"]}>
<Text fz={{ base: "0.8rem", sm: "0.95rem" }} fw={700} c={colors["blue-button"]} lh={1.2}>
Total Responden
</Text>
<Text
ta="end"
fz={{ base: "h2", sm: "h1" }} // 👈 Ukuran font lebih kecil di mobile
fw={"bold"}
fz={{ base: "1.6rem", sm: "2rem" }}
fw={800}
c={colors["blue-button"]}
lh={1.02}
>
{state.findMany.total.toLocaleString('id-ID')}
</Text>
</Box>
</Flex>
<Box style={{ overflowX: 'auto', width: '100%' }}>
<BarChart
h={300}
@@ -230,26 +250,20 @@ function Kepuasan() {
textAnchor: 'end',
fontSize: 12,
}}
// 👇 Tambahkan ini agar chart punya lebar minimum yang cukup untuk semua bar
style={{ minWidth: 'fit-content' }}
/>
</Box>
</Stack>
</Paper>
<Box py={"xl"}>
<SimpleGrid
cols={{ base: 1, sm: 2, lg: 3 }}
spacing="md"
verticalSpacing="md"
>
<Box py="xl">
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md" verticalSpacing="md">
{/* Chart Jenis Kelamin */}
<Paper bg={colors['white-1']} p="md" radius="md">
<Stack>
<Title order={4}>Jenis Kelamin</Title>
<Title order={4} fz={{ base: "1rem", md: "1.1rem" }} lh={1.2}>Jenis Kelamin</Title>
{donutDataJenisKelamin.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md">
Belum ada data untuk ditampilkan dalam grafik
</Text>
<Text c="dimmed" ta="center" my="md" fz="sm">Belum ada data untuk ditampilkan dalam grafik</Text>
) : (
<Paper p="md" radius="md" withBorder>
<Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
@@ -259,19 +273,20 @@ function Kepuasan() {
withTooltip
tooltipAnimationDuration={200}
withLabels
labelsPosition="inside" // 👈 ini yang penting!
labelsPosition="inside"
labelsType="percent"
withLabelsLine
size={isMobile ? 180 : 250} // 👈 kecilkan ukuran di mobile
size={isMobile ? 180 : 250}
data={donutDataJenisKelamin}
/>
</Center>
</Box>
<Stack gap="sm" mt="md">
{donutDataJenisKelamin.map((entry) => (
<Flex key={entry.name} gap="md" align="center">
<Box bg={entry.color} w={20} h={20} style={{ flexShrink: 0 }} />
<Text size="sm">{entry.name}: {entry.value}</Text>
<Text fz="sm" lh={1.25}>{entry.name}: {entry.value}</Text>
</Flex>
))}
</Stack>
@@ -284,11 +299,9 @@ function Kepuasan() {
{/* Chart Rating */}
<Paper bg={colors['white-1']} p="md" radius="md">
<Stack>
<Title order={4}>Ulasan</Title>
<Title order={4} fz={{ base: "1rem", md: "1.1rem" }} lh={1.2}>Ulasan</Title>
{donutDataRating.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md">
Belum ada data untuk ditampilkan dalam grafik
</Text>
<Text c="dimmed" ta="center" my="md" fz="sm">Belum ada data untuk ditampilkan dalam grafik</Text>
) : (
<Paper p="md" radius="md" withBorder>
<Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
@@ -298,20 +311,21 @@ function Kepuasan() {
withTooltip
tooltipAnimationDuration={200}
withLabels
labelsPosition="inside" // 👈 ini yang penting!
labelsPosition="inside"
labelsType="percent"
withLabelsLine
size={isMobile ? 180 : 250} // 👈 kecilkan ukuran di mobile
size={isMobile ? 180 : 250}
data={donutDataRating}
/>
</Center>
</Box>
<Box mt="md" style={{ width: '100%' }}>
<SimpleGrid cols={2} spacing="xs" verticalSpacing="xs">
{donutDataRating.map((entry) => (
<Flex key={entry.name} gap="sm" align="center" style={{ overflow: 'hidden' }}>
<Box bg={entry.color} w={16} h={16} style={{ flexShrink: 0 }} />
<Text size="xs" lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text fz="xs" lh={1.2} lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{entry.name}: {entry.value}
</Text>
</Flex>
@@ -327,11 +341,9 @@ function Kepuasan() {
{/* Chart Kelompok Umur */}
<Paper bg={colors['white-1']} p="md" radius="md">
<Stack>
<Title order={4}>Umur</Title>
<Title order={4} fz={{ base: "1rem", md: "1.1rem" }} lh={1.2}>Umur</Title>
{donutDataKelompokUmur.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md">
Belum ada data untuk ditampilkan dalam grafik
</Text>
<Text c="dimmed" ta="center" my="md" fz="sm">Belum ada data untuk ditampilkan dalam grafik</Text>
) : (
<Paper p="md" radius="md" withBorder>
<Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
@@ -341,20 +353,21 @@ function Kepuasan() {
withTooltip
tooltipAnimationDuration={200}
withLabels
labelsPosition="inside"// 👈 ini yang penting!
labelsPosition="inside"
labelsType="percent"
withLabelsLine
size={isMobile ? 180 : 250} // 👈 kecilkan ukuran di mobile
size={isMobile ? 180 : 250}
data={donutDataKelompokUmur}
/>
</Center>
</Box>
<Box mt="md" style={{ width: '100%' }}>
<SimpleGrid cols={2} spacing="xs" verticalSpacing="xs">
{donutDataKelompokUmur.map((entry) => (
<Flex key={entry.name} gap="sm" align="center" style={{ overflow: 'hidden' }}>
<Box bg={entry.color} w={16} h={16} style={{ flexShrink: 0 }} />
<Text size="xs" lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text fz="xs" lh={1.2} lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{entry.name}: {entry.value}
</Text>
</Flex>
@@ -366,17 +379,19 @@ function Kepuasan() {
)}
</Stack>
</Paper>
</SimpleGrid>
</Box>
</Paper>
</Box>
{/* Modal */}
<Modal opened={opened} onClose={close} title="Ajukan Responden" centered>
<Paper bg={colors['white-1']} p={'md'}>
<Paper bg={colors['white-1']} p="md">
<Stack>
<TextInput
label="Nama"
type='text'
type="text"
placeholder="Masukkan nama"
value={state.create.form.name}
onChange={(val) => {
@@ -450,8 +465,9 @@ function Kepuasan() {
mt={10}
bg={colors['blue-button']}
onClick={handleSubmit}
style={{ fontWeight: 700 }}
>
Submit
<Text fz="sm" ta="center" c="white">Submit</Text>
</Button>
</Stack>
</Paper>
@@ -459,62 +475,78 @@ function Kepuasan() {
</Stack>
);
}
return (
<Stack p={"sm"} my={"xs"}>
<Stack p="sm" my="xs">
<Container size="lg" px="sm">
<Center>
<Text
<Title
order={2}
ta="center"
fz={{ base: '2rem', md: '2.8rem' }}
lh={{ base: 1.05, md: 1.04 }}
c={colors['blue-button']}
fw={800}
style={{ letterSpacing: '-0.5px' }}
>Indeks Kepuasan Masyarakat</Text>
>
Indeks Kepuasan Masyarakat
</Title>
</Center>
<Text fz={{ base: "1.2rem", md: "1.4rem" }} ta={"center"}>Ukur kebahagiaan warga, tingkatkan layanan desa! Dengan partisipasi aktif masyarakat, kami berkomitmen untuk terus memperbaiki layanan agar lebih transparan, efektif, dan sesuai dengan kebutuhan warga. Kepuasan Anda adalah prioritas utama kami dalam membangun desa yang lebih baik!</Text>
<Center mt={10}>
<Button radius={"lg"} bg={colors["blue-button"]} onClick={open}>Ajukan Responden</Button>
<Text fz={{ base: "1rem", md: "1.25rem" }} ta="center" c="black" lh={1.5} mt="sm">
Ukur kebahagiaan warga, tingkatkan layanan desa! Dengan partisipasi aktif masyarakat, kami berkomitmen untuk terus memperbaiki layanan agar lebih transparan, efektif, dan sesuai dengan kebutuhan warga. Kepuasan Anda adalah prioritas utama kami dalam membangun desa yang lebih baik!
</Text>
<Center mt={12}>
<Button radius="lg" bg={colors["blue-button"]} onClick={open} style={{ paddingLeft: 20, paddingRight: 20, fontWeight: 600 }}>
<Text fz={{ base: "0.95rem", md: "1rem" }} ta="center" c="white">Ajukan Responden</Text>
</Button>
</Center>
</Container>
<Box px={"md"}>
<Paper p={"lg"} bg={colors.Bg}>
<Paper p={"lg"} >
<Stack gap={"xs"}>
<Box px="md">
<Paper p="lg" bg={colors.Bg}>
<Paper p="lg">
<Stack gap="xs">
<Flex
direction={{ base: "column", sm: "row" }}
justify="space-between"
align={{ base: "flex-start", sm: "center" }}
gap={{ base: "xs", sm: "md" }} // 👈 Tambahkan gap untuk memberi ruang
gap={{ base: "xs", sm: "md" }}
>
<Text
fw="bold"
fw={700}
ta={{ base: "center", sm: "left" }}
fz={{ base: "sm", sm: "md" }} // 👈 Atur ukuran font agar lebih proporsional di mobile
fz={{ base: "0.95rem", sm: "1rem" }}
lh={1.3}
>
Pelayanan Terhadap Publik Desa Darmasaba
</Text>
<Box
mt={{ base: "sm", sm: 0 }}
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-end', // 👈 Pastikan teks dan angka sejajar ke kanan
textAlign: 'right', // 👈 Agar teks rata kanan
alignItems: 'flex-end',
textAlign: 'right',
}}
>
<Text fz={{ base: "xs", sm: "sm" }} fw={"bold"} c={colors["blue-button"]}>
<Text fz={{ base: "0.8rem", sm: "0.95rem" }} fw={700} c={colors["blue-button"]} lh={1.2}>
Total Responden
</Text>
<Text
ta="end"
fz={{ base: "h2", sm: "h1" }} // 👈 Ukuran font lebih kecil di mobile
fw={"bold"}
fz={{ base: "1.6rem", sm: "2rem" }}
fw={800}
c={colors["blue-button"]}
lh={1.02}
>
{state.findMany.total.toLocaleString('id-ID')}
</Text>
</Box>
</Flex>
<Box style={{ overflowX: 'auto', width: '100%' }} pb={50}>
<BarChart
h={300}
@@ -536,23 +568,15 @@ function Kepuasan() {
</Box>
</Stack>
</Paper>
<Box py={"xl"}>
<SimpleGrid
cols={{
base: 1,
md: 1,
lg: 1,
xl: 3
}}
>
<Box py="xl">
<SimpleGrid cols={{ base: 1, md: 1, lg: 1, xl: 3 }}>
{/* Chart Jenis Kelamin */}
<Paper bg={colors['white-1']} p="md" radius="md">
<Stack>
<Title order={4}>Jenis Kelamin</Title>
<Title order={4} fz={{ base: "1rem", md: "1.1rem" }} lh={1.2}>Jenis Kelamin</Title>
{donutDataJenisKelamin.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md">
Belum ada data untuk ditampilkan dalam grafik
</Text>
<Text c="dimmed" ta="center" my="md" fz="sm">Belum ada data untuk ditampilkan dalam grafik</Text>
) : (
<Paper p="md" radius="md" withBorder>
<Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
@@ -562,18 +586,18 @@ function Kepuasan() {
withLabels
withTooltip
labelsPosition="inside"
labelsType="percent"
size={200}
data={donutDataJenisKelamin}
/>
</Center>
</Box>
<Stack gap="sm" mt="md">
{donutDataJenisKelamin.map((entry) => (
<Flex key={entry.name} gap="md" align="center">
<Box bg={entry.color} w={20} h={20} style={{ flexShrink: 0 }} />
<Text size="sm">{entry.name}: {entry.value}</Text>
<Text fz="sm" lh={1.25}>{entry.name}: {entry.value}</Text>
</Flex>
))}
</Stack>
@@ -586,11 +610,9 @@ function Kepuasan() {
{/* Chart Rating */}
<Paper bg={colors['white-1']} p="md" radius="md">
<Stack>
<Title order={4}>Ulasan</Title>
<Title order={4} fz={{ base: "1rem", md: "1.1rem" }} lh={1.2}>Ulasan</Title>
{donutDataRating.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md">
Belum ada data untuk ditampilkan dalam grafik
</Text>
<Text c="dimmed" ta="center" my="md" fz="sm">Belum ada data untuk ditampilkan dalam grafik</Text>
) : (
<Paper p="md" radius="md" withBorder>
<Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
@@ -600,7 +622,6 @@ function Kepuasan() {
withTooltip
tooltipAnimationDuration={200}
withLabels
labelsPosition="inside"
labelsType="percent"
withLabelsLine
@@ -609,12 +630,13 @@ function Kepuasan() {
/>
</Center>
</Box>
<Box mt="md" style={{ width: '100%' }}>
<SimpleGrid cols={2} spacing="xs" verticalSpacing="xs">
{donutDataRating.map((entry) => (
<Flex key={entry.name} gap="sm" align="center" style={{ overflow: 'hidden' }}>
<Box bg={entry.color} w={16} h={16} style={{ flexShrink: 0 }} />
<Text size="xs" lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text fz="xs" lh={1.2} lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{entry.name}: {entry.value}
</Text>
</Flex>
@@ -630,11 +652,9 @@ function Kepuasan() {
{/* Chart Kelompok Umur */}
<Paper bg={colors['white-1']} p="md" radius="md">
<Stack>
<Title order={4}>Umur</Title>
<Title order={4} fz={{ base: "1rem", md: "1.1rem" }} lh={1.2}>Umur</Title>
{donutDataKelompokUmur.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md">
Belum ada data untuk ditampilkan dalam grafik
</Text>
<Text c="dimmed" ta="center" my="md" fz="sm">Belum ada data untuk ditampilkan dalam grafik</Text>
) : (
<Paper p="md" radius="md" withBorder>
<Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
@@ -644,7 +664,6 @@ function Kepuasan() {
withTooltip
tooltipAnimationDuration={200}
withLabels
labelsPosition="inside"
labelsType="percent"
withLabelsLine
@@ -653,12 +672,13 @@ function Kepuasan() {
/>
</Center>
</Box>
<Box mt="md" style={{ width: '100%' }}>
<SimpleGrid cols={2} spacing="xs" verticalSpacing="xs">
{donutDataKelompokUmur.map((entry) => (
<Flex key={entry.name} gap="sm" align="center" style={{ overflow: 'hidden' }}>
<Box bg={entry.color} w={16} h={16} style={{ flexShrink: 0 }} />
<Text size="xs" lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text fz="xs" lh={1.2} lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{entry.name}: {entry.value}
</Text>
</Flex>
@@ -670,13 +690,15 @@ function Kepuasan() {
)}
</Stack>
</Paper>
</SimpleGrid>
</Box>
</Paper>
</Box>
{/* Modal */}
<Modal opened={opened} onClose={close} title="Ajukan Responden" centered>
<Paper bg={colors['white-1']} p={'md'}>
<Paper bg={colors['white-1']} p="md">
<Stack>
<TextInput
label="Nama"
@@ -754,8 +776,9 @@ function Kepuasan() {
mt={10}
bg={colors['blue-button']}
onClick={handleSubmit}
style={{ fontWeight: 700 }}
>
Submit
<Text fz="sm" ta="center" c="white">Submit</Text>
</Button>
</Stack>
</Paper>
@@ -764,4 +787,4 @@ function Kepuasan() {
);
}
export default Kepuasan;
export default Kepuasan;

View File

@@ -53,14 +53,23 @@ function ModuleItem({ data }: { data: ProgramInovasiItem }) {
) : (
<Stack align="center" gap="xs">
<IconPhotoOff size={38} stroke={1.5} />
<Text size="sm" c="dimmed">
{/* ❗ Caption konsisten */}
<Text fz={{ base: 13, md: 14 }} c="dimmed">
Belum ada gambar
</Text>
</Stack>
)}
</Center>
<Box mt="md">
<Text fw={600} ta="center" size="md">
{/* ❗ Responsive Title */}
<Text
fw={600}
ta="center"
fz={{ base: 16, md: 18 }} // mobile → desktop
lh={1.3}
>
{data.name}
</Text>
</Box>
@@ -91,10 +100,14 @@ function ModuleView() {
<Center h={320}>
<Stack align="center" gap="sm">
<IconPhotoOff size={54} stroke={1.5} />
<Text size="lg" fw={600}>
{/* ❗ Empty title lebih besar */}
<Text fw={600} fz={{ base: 18, md: 22 }}>
Belum ada program inovasi
</Text>
<Text size="sm" c="dimmed">
{/* ❗ Deskripsi kecil & lembut */}
<Text fz={{ base: 14, md: 16 }} c="dimmed" ta="center" lh={1.4}>
Tambahkan program inovasi untuk ditampilkan di sini
</Text>
</Stack>
@@ -103,11 +116,12 @@ function ModuleView() {
}
return (
<ScrollArea h={280} // ✅ tinggi fixed, bisa disesuaikan
<ScrollArea
h={280}
scrollbarSize={2}
offsetScrollbars
styles={{
viewport: { paddingRight: 8 }, // kasih jarak biar scroll nggak dempet
viewport: { paddingRight: 8 },
}}
>
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="lg" mt="lg">

View File

@@ -13,10 +13,23 @@ export default function ProfileView({ data }: ProfileViewProps) {
<Card radius="2xl" className="glass3" py="xl" px="lg" withBorder>
<Stack align="center" gap="sm">
<IconUserCircle size={72} stroke={1.4} />
<Text fw={500} c="dimmed">
{/* TITLE EMPTY */}
<Text
fw={600}
c="dimmed"
fz={{ base: 'lg', sm: 'xl', md: 'xl' }}
ta="center"
>
Profil belum tersedia
</Text>
<Text fz="sm" c="dimmed">
{/* DESCRIPTION EMPTY */}
<Text
fz={{ base: 'sm', sm: 'md' }}
c="dimmed"
ta="center"
>
Data pejabat desa akan muncul di sini
</Text>
</Stack>
@@ -30,12 +43,12 @@ export default function ProfileView({ data }: ProfileViewProps) {
align="end"
pos="relative"
w={{
base: '100%', // mobile: full width
xs: '100%', // small mobile
sm: '85%', // tablet: 85%
md: '60%', // laptop: 60%
lg: '55%', // laptop large: 55%
xl: '50%' // extra large (4K): 50%
base: '100%',
xs: '100%',
sm: '85%',
md: '60%',
lg: '55%',
xl: '50%',
}}
px={{ base: 'md', sm: 'lg', md: 'xl', xl: '2xl' }}
h={{ base: 'auto', sm: '500px', md: '600px', lg: '650px', xl: '700px' }}
@@ -67,13 +80,17 @@ export default function ProfileView({ data }: ProfileViewProps) {
) : (
<Stack align="center" gap="xs" w="100%" py="xl">
<IconUserCircle size={96} stroke={1.5} />
<Text c="dimmed" fz="sm">
<Text
c="dimmed"
fz={{ base: 'sm', sm: 'md' }}
ta="center"
>
Belum ada foto
</Text>
</Stack>
)}
{/* Box nama dan jabatan - responsive positioning */}
{/* Box nama & jabatan */}
<Box
pos="absolute"
bottom={{ base: -30, sm: -25, md: -20 }}
@@ -94,17 +111,21 @@ export default function ProfileView({ data }: ProfileViewProps) {
backgroundColor: 'rgba(255, 255, 255, 0.95)',
}}
>
<Text
fz={{ base: 'xs', sm: 'sm' }}
c="dimmed"
lineClamp={1}
>
{data.position || 'Tidak ada jabatan'}
</Text>
{/* POSITION / JABATAN */}
<Text
fz={{ base: 'xs', sm: 'sm', md: 'md' }}
c="dimmed"
lineClamp={1}
>
{data.position || 'Tidak ada jabatan'}
</Text>
{/* NAME */}
<Text
c={colors['blue-button']}
fw={700}
fz={{ base: 'lg', sm: 'xl' }}
fz={{ base: 'lg', sm: 'xl', md: 'xl', lg: '2xl' }}
mt={4}
lineClamp={2}
>
@@ -114,4 +135,4 @@ export default function ProfileView({ data }: ProfileViewProps) {
</Box>
</Stack>
);
}
}

View File

@@ -26,7 +26,11 @@ function SosmedView({
data.map((item, k) => (
<Tooltip
key={k}
label={item.name || "Tautan Sosial"}
label={
<Text fz={{ base: 12, md: 14 }}>
{item.name || "Tautan Sosial"}
</Text>
}
withArrow
position="top"
transitionProps={{ transition: "pop", duration: 150 }}
@@ -57,7 +61,7 @@ function SosmedView({
);
}
return <Box bg={colors['blue-button']} w="100%" h="100%" />;
return <Box bg={colors["blue-button"]} w="100%" h="100%" />;
})()}
</ActionIcon>
</Tooltip>
@@ -72,7 +76,12 @@ function SosmedView({
background: "linear-gradient(135deg, #1C6EA4 0%, #000 100%)",
}}
>
<Text ta="center" c="dimmed" size="sm">
<Text
ta="center"
c="dimmed"
fz={{ base: 13, md: 15 }}
lh={1.4}
>
Belum ada media sosial yang terhubung
</Text>
</Card>

View File

@@ -59,7 +59,7 @@ const getWorkStatus = (day: string, currentTime: string): { status: string; mess
: { status: "Tutup", message: "08:00 - 17:00" };
};
// Skeleton component untuk Social Media
// 🟦 Skeleton component untuk Social Media
const SosmedSkeleton = () => (
<Flex gap="md" justify="center" wrap="wrap">
{[1, 2, 3, 4].map((i) => (
@@ -68,7 +68,7 @@ const SosmedSkeleton = () => (
</Flex>
);
// Skeleton component untuk Profile
// 🟦 Skeleton component untuk Profile
const ProfileSkeleton = () => (
<Card
radius="xl"
@@ -158,6 +158,8 @@ function LandingPage() {
<Stack w={{ base: "100%", md: "65%" }} gap="lg">
<Card radius="xl" bg={colors.grey[1]} p="lg" mt={10} shadow="xl">
<Stack gap="xl">
{/* Header Logo */}
<Flex gap="md" wrap="wrap">
<Group>
<Box bg="white" w={72} h={72} p="sm" style={{ borderRadius: 24 }}>
@@ -167,6 +169,8 @@ function LandingPage() {
<Image loading="lazy" src="/pudak-icon.png" alt="Logo Pudak" fit="contain" />
</Box>
</Group>
{/* Jam Operasional */}
<Grid w="100%">
<Grid.Col span={12}>
<Paper
@@ -177,36 +181,58 @@ function LandingPage() {
style={{ position: "relative", overflow: "hidden" }}
>
<Grid gutter="md">
{/* Kolom 1 */}
<GridCol span={{ base: 12, md: 6 }}>
<Stack gap="xs">
<Flex align="center" gap="xs">
<IconCalendarTime size={16} color="white" />
<Text c="white" fz="sm">Jam Operasional</Text>
<Text c="white" fz={{ base: "xs", md: "sm" }}>
Jam Operasional
</Text>
</Flex>
<Paper p="sm" radius="md" bg="white">
<Tooltip label="Status saat ini berdasarkan jam operasional kantor">
<Badge
color={workStatus.status === "Buka" ? "green" : "red"}
radius="sm"
variant="filled"
size="md"
>
{workStatus.status}
</Badge>
</Tooltip>
<Text fw="bold" fz="lg">{workStatus.message}</Text>
<Text
fw={700}
fz={{ base: "md", md: "lg" }}
mt={4}
>
{workStatus.message}
</Text>
</Paper>
</Stack>
</GridCol>
{/* Kolom 2 */}
<GridCol span={{ base: 12, md: 6 }}>
<Stack gap="xs">
<Flex align="center" gap="xs">
<IconInfoCircle size={16} color="white" />
<Text c="white" fz="sm">Hari Ini</Text>
<Text c="white" fz={{ base: "xs", md: "sm" }}>
Hari Ini
</Text>
</Flex>
<Paper p="sm" radius="md" bg="white">
<Text fz="sm">Status Kantor</Text>
<Text fw="bold" fz="lg">
{workStatus.status === "Buka" ? "Sedang Beroperasi" : "Tidak Beroperasi"}
<Text fz={{ base: "xs", md: "sm" }} c="dimmed">
Status Kantor
</Text>
<Text fw={700} fz={{ base: "md", md: "lg" }}>
{workStatus.status === "Buka"
? "Sedang Beroperasi"
: "Tidak Beroperasi"}
</Text>
</Paper>
</Stack>
@@ -217,19 +243,29 @@ function LandingPage() {
</Grid>
</Flex>
{/* MODULE VIEW */}
<ModuleView />
{/* Sosmed */}
{isLoadingSosmed ? (
<SosmedSkeleton />
) : socialMedia.length > 0 ? (
<SosmedView data={socialMedia} />
) : (
<Center>
<Text c="dimmed">Belum ada tautan media sosial yang tersedia</Text>
<Text fz={{ base: "sm", md: "md" }} c="dimmed">
Belum ada tautan media sosial yang tersedia
</Text>
</Center>
)}
<Text ta="center" c={colors.trans.dark[2]}>
{/* CTA Text */}
<Text
ta="center"
c={colors.trans.dark[2]}
fz={{ base: "sm", md: "md" }}
lh={1.5}
>
Bagikan ide, kritik, atau saran Anda untuk mendukung pembangunan desa.
Semua lebih mudah dengan fitur interaktif yang kami sediakan.
</Text>
@@ -237,6 +273,7 @@ function LandingPage() {
</Card>
</Stack>
{/* PROFIL */}
{isLoadingProfile ? (
<ProfileSkeleton />
) : profile ? (
@@ -251,7 +288,9 @@ function LandingPage() {
style={{ height: "fit-content" }}
>
<Center h={300}>
<Text c="dimmed">Informasi profil belum tersedia</Text>
<Text fz={{ base: "sm", md: "md" }} c="dimmed">
Informasi profil belum tersedia
</Text>
</Center>
</Card>
)}
@@ -260,4 +299,4 @@ function LandingPage() {
);
}
export default LandingPage;
export default LandingPage;

View File

@@ -28,20 +28,41 @@ const textHeading = {
const HEIGHT = 720;
function Layanan() {
// responsive breakpoints: base = mobile, md = desktop/tablet landscape
return (
<Stack pos="relative" bg={colors.grey[1]} gap="xl" py="md">
<Container w={{ base: "100%", md: "80%" }} p="md">
<Stack align="center" gap="0">
{/* Main title - semantic h1 */}
<Text
fw="bold"
component="h1"
fw={700}
c={colors["blue-button"]}
fz={{ base: "1.8rem", md: "3.4rem" }}
ta="center"
// responsive sizes: mobile ~28px, desktop ~48px
fz={{ base: "1.75rem", md: "3rem" }}
// tighter line-height for large headings, slightly more compact on desktop
style={{ lineHeight: "1.05" }}
>
{textHeading.title}
</Text>
<Text ta="center" fz={{ base: "1rem", md: "1.3rem" }}>
{/* Description - readable line-height and constrained width on desktop */}
<Text
component="p"
ta="center"
fz={{ base: "0.95rem", md: "1.15rem" }}
// more comfortable line-height for paragraphs
style={{
lineHeight: "1.6",
maxWidth: "70ch",
marginTop: 8,
}}
c="black"
>
{textHeading.des}
</Text>
<Box p="md">
<Button
component={Link}
@@ -49,6 +70,14 @@ function Layanan() {
variant="filled"
bg={colors["blue-button"]}
radius={100}
// accessible sizing: slightly smaller on mobile, comfortable on desktop
style={{
paddingLeft: 20,
paddingRight: 20,
fontSize: "md",
// ensure button text doesn't overflow on very narrow screens
whiteSpace: "nowrap",
}}
>
Detail
</Button>
@@ -175,7 +204,7 @@ function Slider() {
startXRef.current = e.pageX - containerRef.current.offsetLeft;
scrollLeftRef.current = containerRef.current.scrollLeft;
velocityRef.current = 0;
containerRef.current.style.cursor = 'grabbing';
containerRef.current.style.cursor = "grabbing";
};
const handleMouseMove = (e: React.MouseEvent) => {
@@ -196,7 +225,7 @@ function Slider() {
if (!containerRef.current || mobile) return;
isDraggingRef.current = false;
containerRef.current.style.cursor = 'grab';
containerRef.current.style.cursor = "grab";
};
const handleWheel = (e: React.WheelEvent) => {
@@ -215,7 +244,7 @@ function Slider() {
if (data.length === 0) {
return (
<Container>
<Text ta="center" c="dimmed">
<Text ta="center" c="dimmed" fz={{ base: "0.95rem", md: "1rem" }}>
Tidak ada layanan tersedia
</Text>
</Container>
@@ -240,6 +269,8 @@ function Slider() {
scrollbarWidth: "none",
msOverflowStyle: "none",
}}
// ensure keyboard accessibility: allow focus outline when focused
tabIndex={0}
>
<Box
style={{
@@ -287,26 +318,56 @@ function Slider() {
pos="relative"
>
<Box p="lg">
{/* slide title - semantic h2 */}
<Text
fw="bold"
component="h2"
fw={700}
c="white"
fz={{base: "xl", md: "3.5rem"}}
fz={{ base: "1.25rem", md: "2.4rem" }}
// tighter heading line-height but ensure readability on mobile
style={{
textAlign: "center",
lineHeight: mobile ? "1.15" : "1.02",
// clamp long names visually
display: "-webkit-box",
WebkitLineClamp: 2,
WebkitBoxOrient: "vertical",
overflow: "hidden",
}}
title={_.startCase(item.name)}
>
{_.startCase(item.name)}
</Text>
{/* optional short description - rendered if exists */}
{item.description ? (
<Text
component="p"
mt="sm"
c="white"
fz={{ base: "0.9rem", md: "1rem" }}
style={{ lineHeight: "1.5", textAlign: "center" }}
>
{item.description}
</Text>
) : null}
</Box>
<Group justify="center">
<Group justify="center" mb="lg">
<Button
onClick={() =>
router.push(`/darmasaba/desa/layanan/${item.id}`)
}
px={46}
px={mobile ? 20 : 46}
radius="100"
size="md"
size={mobile ? "sm" : "md"}
bg={colors["blue-button"]}
// ensure button text readable on all sizes
style={{
fontSize: mobile ? "0.95rem" : "1rem",
whiteSpace: "nowrap",
}}
aria-label={`Detail layanan ${_.startCase(item.name)}`}
>
Detail
</Button>
@@ -320,4 +381,4 @@ function Slider() {
);
}
export default Layanan;
export default Layanan;

View File

@@ -1,9 +1,21 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client';
import penghargaanState from "@/app/admin/(dashboard)/_state/desa/penghargaan";
import { Stack, Box, Container, Button, Text, Loader, Paper, Center, ActionIcon } from "@mantine/core";
import {
Stack,
Box,
Container,
Button,
Text,
Loader,
Paper,
Center,
ActionIcon,
Title,
} from "@mantine/core";
import { IconAward, IconArrowRight, IconPlayerPlay } from "@tabler/icons-react";
import { useTransitionRouter } from 'next-view-transitions';
import { useTransitionRouter } from "next-view-transitions";
import { useEffect, useState, useRef } from "react";
import { useProxy } from "valtio/utils";
import { useMediaQuery } from "@mantine/hooks";
@@ -12,43 +24,33 @@ function Penghargaan() {
const router = useTransitionRouter();
const state = useProxy(penghargaanState);
const [loading, setLoading] = useState(false);
const isMobile = useMediaQuery('(max-width: 768px)');
const isMobile = useMediaQuery("(max-width: 768px)");
const [isVideoLoaded, setIsVideoLoaded] = useState(false);
const [showVideo, setShowVideo] = useState(true);
const [videoError, setVideoError] = useState(false);
const [showPlayButton, setShowPlayButton] = useState(false);
const videoRef = useRef<HTMLVideoElement>(null);
const hasTriedAutoplay = useRef(false);
// Deteksi iOS dengan lebih akurat
const isIOS = typeof window !== 'undefined' && (
/iPad|iPhone|iPod/.test(navigator.userAgent) ||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1) // iPad dengan iPadOS 13+
);
useEffect(() => {
// Di iOS, coba autoplay dulu, kalau gagal tampilkan fallback
if (isIOS && videoRef.current) {
const playPromise = videoRef.current.play();
if (playPromise !== undefined) {
playPromise
.then(() => {
// Autoplay berhasil
setShowVideo(true);
setIsVideoLoaded(true);
})
.catch(() => {
// Autoplay gagal, tampilkan fallback
setShowVideo(false);
setVideoError(true);
});
}
}
}, [isIOS]);
// ---- TYPOGRAPHY SCALE (RESPONSIVE) ----
// ukuran dalam px, lh = line-height
const TYPO = {
// utama / hero title
title: { base: 22, md: 36, lh: 1.08 }, // lebih menonjol di desktop
// subheading / loader / tagline
subtitle: { base: 14, md: 16, lh: 1.35 },
// teks body / deskripsi umum
body: { base: 14, md: 16, lh: 1.6 },
// caption / small notes
small: { base: 12, md: 13, lh: 1.4 },
// judul dalam kartu (card title)
paperTitle: { base: 15, md: 18, lh: 1.25 },
};
// Load data
useEffect(() => {
const loadData = async () => {
setLoading(true);
try {
setLoading(true);
await state.findMany.load();
} finally {
setLoading(false);
@@ -57,99 +59,134 @@ function Penghargaan() {
loadData();
}, []);
const handlePlayVideo = () => {
setShowVideo(true);
setVideoError(false);
// Paksa play video setelah user interaction
setTimeout(() => {
if (videoRef.current) {
videoRef.current.play().catch(err => {
console.error("Video play error:", err);
setVideoError(true);
});
// Attempt autoplay setelah video loaded
useEffect(() => {
if (isVideoLoaded && videoRef.current && !hasTriedAutoplay.current) {
hasTriedAutoplay.current = true;
const attemptAutoplay = async () => {
try {
// Pastikan video muted sebelum play
videoRef.current!.muted = true;
await videoRef.current!.play();
setShowPlayButton(false);
console.log("✅ Autoplay berhasil");
} catch (err) {
console.warn("⚠️ Autoplay diblokir browser:", err);
// Tampilkan tombol play jika autoplay gagal
setShowPlayButton(true);
}
};
// Delay sedikit untuk memastikan video siap
setTimeout(attemptAutoplay, 100);
}
}, [isVideoLoaded]);
// Handle manual play
const handlePlayVideo = async () => {
if (videoRef.current) {
try {
videoRef.current.muted = true;
await videoRef.current.play();
setShowPlayButton(false);
setVideoError(false);
} catch (err) {
console.error("❌ Gagal memutar video:", err);
setVideoError(true);
}
}, 100);
}
};
// kalau mobile ambil 1 data aja, kalau desktop ambil 3
// Ambil data terbatas berdasarkan perangkat
const data = state.findMany.data?.slice(0, isMobile ? 1 : 3);
return (
<Stack pos="relative" h="auto" mih={{ base: 500, md: 720 }} style={{ overflow: 'hidden' }}>
{/* Video Layer */}
{showVideo && !videoError && (
<Stack pos="relative" h="auto" mih={{ base: 500, md: 720 }} style={{ overflow: "hidden" }}>
{/* Video background */}
{!videoError && (
<video
ref={videoRef}
autoPlay
muted
loop
playsInline
preload="auto"
webkit-playsinline="true"
onLoadedData={() => setIsVideoLoaded(true)}
onError={() => {
console.error("Video load error");
console.error("Video gagal dimuat");
setVideoError(true);
setShowVideo(false);
}}
onCanPlayThrough={() => {
console.log("✅ Video siap diputar");
}}
style={{
position: 'absolute',
position: "absolute",
top: 0,
left: 0,
width: '100%',
height: '100%',
objectFit: 'cover',
width: "100%",
height: "100%",
objectFit: "cover",
opacity: isVideoLoaded ? 1 : 0,
transition: 'opacity 0.5s ease',
transition: "opacity 0.5s ease",
zIndex: 0,
}}
>
<source src="/assets/videos/award.mp4" type="video/mp4" />
Browser Anda tidak mendukung video.
</video>
)}
{/* Fallback Image + Play Button */}
{(!showVideo || videoError) && (
{/* Fallback background image */}
{(videoError || !isVideoLoaded) && (
<Box
onClick={handlePlayVideo}
style={{
position: 'absolute',
position: "absolute",
top: 0,
left: 0,
width: '100%',
height: '100%',
width: "100%",
height: "100%",
backgroundImage: "url('/mangupuraaward.jpeg')",
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
cursor: 'pointer',
backgroundSize: "cover",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
zIndex: 0,
}}
>
<Center
style={{
width: '100%',
height: '100%',
background: 'rgba(0,0,0,0.3)', // overlay gelap agar icon terlihat
}}
>
<ActionIcon
size={80}
radius="xl"
variant="filled"
color="blue"
style={{
backgroundColor: 'rgba(255,255,255,0.9)',
boxShadow: '0 8px 32px rgba(0,0,0,0.3)',
}}
>
<IconPlayerPlay size={40} color="var(--mantine-color-blue-6)" />
</ActionIcon>
</Center>
</Box>
/>
)}
{/* Overlay Gradient + Content */}
{/* Tombol Play (muncul jika autoplay gagal atau video error) */}
{(showPlayButton || videoError) && (
<Center
onClick={handlePlayVideo}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
cursor: "pointer",
zIndex: 2,
pointerEvents: showPlayButton || videoError ? "auto" : "none",
}}
>
<ActionIcon
size={isMobile ? 64 : 80}
radius="xl"
variant="filled"
style={{
backgroundColor: "rgba(255,255,255,0.95)",
boxShadow: "0 8px 32px rgba(0,0,0,0.3)",
animation: "pulse 2s infinite",
}}
aria-label="Play background video"
>
<IconPlayerPlay size={isMobile ? 34 : 40} color="var(--mantine-color-blue-6)" />
</ActionIcon>
</Center>
)}
{/* Overlay konten */}
<Box
style={{
width: "100%",
@@ -161,22 +198,39 @@ function Penghargaan() {
zIndex: 1,
}}
>
<Container w={{ base: "100%", md: "80%" }} mih={{ base: 500, md: 720 }} p="xl">
<Container w={{ base: "100%", md: "80%" }} maw={1100} mih={{ base: 500, md: 720 }} p={{ base: "lg", md: "xl" }}>
<Stack justify="center" align="center" gap="xl" h="100%">
<Text
fw={900}
fz={{ base: "2rem", md: "2.8rem" }}
ta="center"
variant="gradient"
gradient={{ from: "cyan", to: "blue", deg: 60 }}
{/* Hero Title - pakai Title agar semantics lebih jelas */}
<Title
order={2}
style={{
fontWeight: 800,
lineHeight: TYPO.title.lh,
// Mantine support fz prop but inline style fallback ok:
fontSize: isMobile ? TYPO.title.base : TYPO.title.md,
textAlign: "center",
// gradient via CSS text-fill technique (ke Mantine gradient prop juga bisa)
background: "-webkit-linear-gradient(60deg, #22D3EE 0%, #3B82F6 100%)",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
}}
aria-label="Penghargaan Desa"
>
Penghargaan Desa
</Text>
</Title>
{/* Content area */}
{loading ? (
<Stack align="center" gap="sm">
<Loader color="blue" size="lg" />
<Text c="gray.3" fz="lg">Sedang memuat data penghargaan...</Text>
<Loader color="blue" size={isMobile ? "md" : "lg"} />
<Text
c="gray.3"
fz={isMobile ? TYPO.subtitle.base : TYPO.subtitle.md}
lh={TYPO.subtitle.lh}
ta="center"
>
Sedang memuat data penghargaan...
</Text>
</Stack>
) : data && data.length > 0 ? (
<Stack gap="md" w="100%" maw={600}>
@@ -185,47 +239,98 @@ function Penghargaan() {
key={k}
withBorder
radius="xl"
p="lg"
p={isMobile ? "md" : "lg"}
shadow="xl"
style={{
background: "rgba(255,255,255,0.07)",
backdropFilter: "blur(12px)",
transition: "all 0.3s ease",
}}
aria-label={`Penghargaan ${v.name}`}
>
<Stack align="center" gap="xs">
<IconAward size={40} color="var(--mantine-color-blue-4)" />
<Text fz="lg" fw={700} c="white" ta="center">
<IconAward size={isMobile ? 36 : 40} color="var(--mantine-color-blue-4)" />
<Text
// card title: lebih tegas
fz={isMobile ? TYPO.paperTitle.base : TYPO.paperTitle.md}
fw={700}
c="white"
ta="center"
lh={TYPO.paperTitle.lh}
style={{ wordBreak: "break-word" }}
title={v.name}
>
{v.name}
</Text>
{/* Jika ingin menambahkan deskripsi ringkas di card, gunakan body scale */}
{v.description && (
<Text
fz={isMobile ? TYPO.body.base : TYPO.body.md}
c="gray.2"
ta="center"
lh={TYPO.body.lh}
style={{ maxWidth: 520 }}
>
{v.description}
</Text>
)}
</Stack>
</Paper>
))}
</Stack>
) : (
<Stack align="center" gap="xs">
<IconAward size={48} color="var(--mantine-color-gray-5)" />
<Text c="gray.4" fz="lg" ta="center">
<IconAward size={isMobile ? 40 : 48} color="var(--mantine-color-gray-5)" />
<Text
c="gray.4"
fz={isMobile ? TYPO.body.base : TYPO.body.md}
ta="center"
lh={TYPO.body.lh}
>
Belum ada penghargaan yang tercatat
</Text>
</Stack>
)}
<Button
size="lg"
size={isMobile ? "md" : "lg"}
radius="xl"
variant="gradient"
gradient={{ from: "#26667F", to: "#124170", deg: 45 }}
rightSection={<IconArrowRight size={20} />}
rightSection={<IconArrowRight size={isMobile ? 16 : 20} />}
onClick={() => router.push("/darmasaba/penghargaan")}
aria-label="Lihat semua penghargaan"
>
Lihat Semua Penghargaan
<Text
c="white"
fz={isMobile ? TYPO.body.base : TYPO.body.md}
fw={700}
style={{ lineHeight: TYPO.body.lh }}
>
Lihat Semua Penghargaan
</Text>
</Button>
</Stack>
</Container>
</Box>
{/* CSS untuk animasi tombol play */}
<style jsx>{`
@keyframes pulse {
0%,
100% {
transform: scale(1);
opacity: 0.9;
}
50% {
transform: scale(1.05);
opacity: 1;
}
}
`}</style>
</Stack>
);
}
export default Penghargaan;
export default Penghargaan;

View File

@@ -50,31 +50,52 @@ function Potensi() {
return (
<Stack p="sm" gap="xl">
<Container w={{ base: "100%", md: "80%" }} p={"md"} >
<Text id="news-title" ta={"center"} fw={"bold"} c={colors["blue-button"]} fz={{ base: "1.8rem", md: "3.4rem" }}>
{/* HEADER */}
<Container w={{ base: "100%", md: "80%" }} p="md">
<Text
id="news-title"
ta="center"
fw={800}
c={colors["blue-button"]}
fz={{ base: "2rem", md: "3.2rem" }}
lh={{ base: "2.6rem", md: "3.6rem" }}
style={{ letterSpacing: "-0.5px" }}
>
{textHeading.title}
</Text>
<Text id="news-content" ta={"center"} fz={{ base: "1rem", md: "1.3rem" }}>
<Text
id="news-content"
ta="center"
c="gray.7"
fz={{ base: "1rem", md: "1.25rem" }}
lh={{ base: "1.5rem", md: "1.9rem" }}
style={{ marginTop: 8, maxWidth: 800, marginInline: "auto" }}
>
{textHeading.des}
</Text>
</Container>
{/* LOADING STATE */}
{loading ? (
<Stack align="center" justify="center" h={300}>
<Loader size="lg" color={colors["blue-button"]} />
<Text c="gray.4">Sedang memuat potensi desa...</Text>
<Text c="gray.4" fz="1rem" lh="1.4rem">
Sedang memuat potensi desa...
</Text>
</Stack>
) : data.length === 0 ? (
<Stack align="center" justify="center" h={300} gap="xs">
<IconInfoCircle size={48} color={colors["blue-button"]} />
<Text fw={600} c="gray.3">
<Text fw={600} c="gray.3" fz="1.2rem" lh="1.4rem">
Belum ada potensi tersedia
</Text>
<Text size="sm" c="gray.5">
<Text fz="0.9rem" lh="1.3rem" c="gray.5">
Silakan cek kembali nanti untuk pembaruan terbaru.
</Text>
</Stack>
) : (
/* CARD LIST */
<SimpleGrid cols={{ base: 1, sm: 2 }}>
{_.take(data, 4).map((v, k) => (
<motion.div
@@ -84,7 +105,12 @@ function Potensi() {
onClick={() => router.push(`/darmasaba/desa/potensi/${v.id}`)}
style={{ cursor: "pointer" }}
>
<BackgroundImage src={v.image?.link} h={320} radius={20} pos="relative">
<BackgroundImage
src={v.image?.link}
h={320}
radius={20}
pos="relative"
>
<Box
pos="absolute"
w="100%"
@@ -92,6 +118,8 @@ function Potensi() {
bg={colors.trans.dark[2]}
style={{ borderRadius: 20, zIndex: 0 }}
/>
{/* CARD CONTENT */}
<Stack
justify="end"
h="100%"
@@ -101,11 +129,24 @@ function Potensi() {
style={{ zIndex: 1 }}
>
<Tooltip label={v.name} position="top-start">
<Text fw={700} c="white" fz={{ base: "1.2rem", md: "1.4rem" }} truncate>
<Text
fw={700}
c="white"
fz={{ base: "1.25rem", md: "1.45rem" }}
lh={{ base: "1.6rem", md: "1.8rem" }}
truncate
>
{v.name}
</Text>
</Tooltip>
<Text lineClamp={2} c="gray.2" fz={{ base: "0.8rem", md: "1rem" }} dangerouslySetInnerHTML={{ __html: v.deskripsi }} />
<Text
lineClamp={2}
c="gray.2"
fz={{ base: "0.85rem", md: "1rem" }}
lh={{ base: "1.2rem", md: "1.4rem" }}
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
/>
</Stack>
</BackgroundImage>
</motion.div>
@@ -113,16 +154,18 @@ function Potensi() {
</SimpleGrid>
)}
{/* BUTTON */}
<Stack align="center">
<Group>
<Button
onClick={() => router.push("/darmasaba/desa/potensi")}
color={colors["blue-button"]}
variant="gradient"
gradient={{ from: "#26667F", to: "#124170", }}
gradient={{ from: "#26667F", to: "#124170" }}
radius="xl"
size="md"
rightSection={<IconArrowRight size={18} />}
style={{ fontWeight: 600 }}
>
Lihat Semua Potensi
</Button>

View File

@@ -2,7 +2,19 @@
'use client'
import prestasiState from "@/app/admin/(dashboard)/_state/landing-page/prestasi-desa";
import colors from "@/con/colors";
import { BackgroundImage, Box, Button, Center, Container, Group, Loader, SimpleGrid, Stack, Text } from "@mantine/core";
import {
BackgroundImage,
Box,
Button,
Center,
Container,
Group,
Loader,
SimpleGrid,
Stack,
Text,
Title,
} from "@mantine/core";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
@@ -32,12 +44,31 @@ function Prestasi() {
<Stack p="sm" bg="linear-gradient(180deg, #ffffff 0%, #f8fbff 100%)">
<Container w={{ base: "100%", md: "80%" }} p="xl">
<Stack align="center" gap="sm">
<Text c={colors["blue-button"]} ta="center" fz={{ base: "2rem", md: "3.4rem" }} fw={700}>
Prestasi Desa
</Text>
<Text fz={{ base: "1rem", md: "1.3rem" }} ta="center" c="dimmed" maw={700}>
Kami bangga dengan pencapaian desa hingga saat ini. Semoga prestasi ini menjadi inspirasi untuk terus berkarya dan berinovasi demi kemajuan bersama.
{/* TITLE UTAMA */}
<Title
order={1}
c={colors["blue-button"]}
ta="center"
fz={{ base: "2rem", sm: "2.6rem", md: "3.2rem" }}
lh={{ base: "2.4rem", md: "3.5rem" }}
>
Prestasi Desa
</Title>
{/* SUBTEXT */}
<Text
fz={{ base: "1rem", md: "1.2rem" }}
lh={{ base: "1.5rem", md: "1.8rem" }}
ta="center"
c="black"
maw={700}
>
Kami bangga dengan pencapaian desa hingga saat ini. Semoga prestasi ini
menjadi inspirasi untuk terus berkarya dan berinovasi demi kemajuan
bersama.
</Text>
<Button
radius="xl"
size="lg"
@@ -59,13 +90,13 @@ function Prestasi() {
) : data.length === 0 ? (
<Center mih={200}>
<Stack align="center" gap="xs">
<Text fz="1.2rem" fw={500} c="dimmed">
<Text fz="1.2rem" fw={500} c="dimmed" ta="center" lh="1.4rem">
Belum ada prestasi yang ditampilkan
</Text>
</Stack>
</Center>
) : (
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="lg" mb={"xl"}>
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="lg" mb="xl">
{data.map((v, k) => (
<BackgroundImage
key={k}
@@ -79,26 +110,32 @@ function Prestasi() {
bg="linear-gradient(180deg, rgba(0,0,0,0.4) 0%, rgba(0,0,0,0.7) 100%)"
style={{ borderRadius: 27 }}
/>
<Stack justify="space-between" h="100%" pos="relative" p="lg">
<Box>
<Text
c="white"
fz={{ base: "1rem", md: "1.25rem" }}
ta="center"
fw={500}
>
{v.kategori.name}
</Text>
</Box>
{/* KATEGORI */}
<Text
c="white"
fz={{ base: "1rem", md: "1.15rem" }}
lh={{ base: "1.4rem", md: "1.6rem" }}
ta="center"
fw={500}
>
{v.kategori.name}
</Text>
{/* DESKRIPSI */}
<Text
fw={700}
c="white"
fz={{ base: "1.5rem", md: "2rem", lg: "2.5rem" }}
fz={{ base: "1.4rem", md: "1.8rem", lg: "2.2rem" }}
lh={{ base: "1.8rem", md: "2.2rem", lg: "2.6rem" }}
ta="center"
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
lineClamp={5}
/>
<Group justify="center">
<Button
onClick={() => router.push(`/darmasaba/prestasi-desa/${v.id}`)}

View File

@@ -20,12 +20,11 @@ export default function SDGS() {
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
const result = await response.json()
let data = []
if (Array.isArray(result.data)) data = result.data
else if (Array.isArray(result)) data = result
else {
setSdgsDesa([])
return
}
else return setSdgsDesa([])
const top4Sdgs = [...data].sort((a, b) => parseInt(b.jumlah) - parseInt(a.jumlah)).slice(0, 4)
setSdgsDesa(top4Sdgs)
} catch {
@@ -36,24 +35,38 @@ export default function SDGS() {
}, [])
return (
<Stack p="sm" my={"xs"}>
<Stack p="sm" my="xs">
<Container w={{ base: "100%", md: "80%" }} p="xl">
{/* ========== TITLE SECTION ========== */}
<Center>
<Title
order={1}
fz={{ base: "2.4rem", md: "3.6rem" }}
fz={{ base: "2.2rem", md: "3.4rem" }}
lh={{ base: 1.1, md: 1.1 }}
fw={900}
c={colors["blue-button"]}
ta="center"
>
SDGs Desa
</Title>
</Center>
<Text ta={"center"} fz={{ base: "1rem", md: "1.3rem" }}>
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
ta="center"
fz={{ base: "1rem", md: "1.2rem" }}
lh={{ base: 1.5, md: 1.6 }}
c="black"
mt="xs"
mb="md"
>
SDGs Desa adalah upaya desa untuk menciptakan pembangunan yang maju, inklusif, dan berkelanjutan melalui 17 tujuan mulai dari pengentasan kemiskinan, pendidikan, kesehatan, hingga pelestarian lingkungan.
</Text>
<Box py="lg">
{sdgsDesa && sdgsDesa.length > 0 ? (
/* ========== LIST GRID ========== */
<SimpleGrid cols={{ base: 1, sm: 4 }} spacing="xl" verticalSpacing="xl" pb={30}>
{sdgsDesa.map((item) => (
<motion.div
@@ -70,7 +83,7 @@ export default function SDGS() {
background: "linear-gradient(180deg, #FFFFFF, #F6F8FA)",
border: "1px solid rgba(0,0,0,0.05)",
transition: "all 0.3s ease",
height: "100%", // biar tinggi antar card konsisten
height: "100%",
display: "flex",
flexDirection: "column",
}}
@@ -101,23 +114,26 @@ export default function SDGS() {
</Box>
</Center>
{/* Stack isi teks & angka */}
<Stack justify="space-between" align="center" gap="xs" h="100%">
{/* JUDUL ITEM */}
<Text
ta="center"
fz={{ base: "lg", md: "xl" }}
lh={{ base: 1.3, md: 1.3 }}
fw={700}
mb="xs"
style={{ minHeight: mobile ? 60 : 70 }} // biar judulnya punya tinggi tetap
style={{ minHeight: mobile ? 60 : 70 }}
>
{item.name}
</Text>
{/* ANGKA */}
<Title
order={2}
ta="center"
style={{
fontSize: mobile ? "2.4rem" : "3.2rem",
fontSize: mobile ? "2.2rem" : "3rem",
lineHeight: 1.1,
fontWeight: 900,
letterSpacing: "-0.5px",
color: "#124170",
@@ -132,14 +148,15 @@ export default function SDGS() {
</SimpleGrid>
) : (
/* ========== EMPTY STATE ========== */
<Center mih={200} style={{ flexDirection: "column" }}>
<IconMoodSad size={48} stroke={1.5} style={{ marginBottom: "1rem" }} />
<Text fz="lg" c="dimmed">
Data SDGs Desa belum tersedia
</Text>
<Text fz="lg" lh={1.4} c="dimmed">Data SDGs Desa belum tersedia</Text>
</Center>
)}
{/* BUTTON */}
<Center>
<Button
component={Link}
@@ -152,18 +169,19 @@ export default function SDGS() {
style={{
boxShadow: "0 6px 14px rgba(18,65,112,0.25)",
transition: "all 0.3s ease",
transform: "translateY(0)",
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = "translateY(-4px)";
e.currentTarget.style.boxShadow = "0 10px 20px rgba(18,65,112,0.35)";
e.currentTarget.style.transform = "translateY(-4px)"
e.currentTarget.style.boxShadow = "0 10px 20px rgba(18,65,112,0.35)"
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = "translateY(0)";
e.currentTarget.style.boxShadow = "0 6px 14px rgba(18,65,112,0.25)";
e.currentTarget.style.transform = "translateY(0)"
e.currentTarget.style.boxShadow = "0 6px 14px rgba(18,65,112,0.25)"
}}
>
<Text c="white" fz={{ base: "md", md: "lg" }} fw="bold">Jelajahi Semua Tujuan SDGs Desa</Text>
<Text c="white" fz={{ base: "md", md: "lg" }} lh={1.3} fw={600}>
Jelajahi Semua Tujuan SDGs Desa
</Text>
</Button>
</Center>
</Box>