Fix QC Kak Inno 8 Des

Fix QC Kak Ayu 8 Des
Fix QC Pak Jun 8 Des
This commit is contained in:
2025-12-09 17:27:23 +08:00
parent 9dbe172165
commit ac2fc1a705
44 changed files with 712 additions and 477 deletions

View File

@@ -20,9 +20,9 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
icon: <IconActivity size={18} stroke={1.8} /> icon: <IconActivity size={18} stroke={1.8} />
}, },
{ {
label: "Grafik Hasil Kepuasan Masyarakat", label: "Penderita Penyakit",
value: "grafikhasilkepuasan", value: "penderitapenyakit",
href: "/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan", href: "/admin/kesehatan/data-kesehatan-warga/penderita_penyakit",
icon: <IconGauge size={18} stroke={1.8} /> icon: <IconGauge size={18} stroke={1.8} />
}, },
{ {

View File

@@ -70,8 +70,8 @@ function EditGrafikHasilKepuasan() {
}); });
} }
} catch (err) { } catch (err) {
console.error("Error loading grafik hasil kepuasan:", err); console.error("Error loading penderita penyakit:", err);
toast.error("Gagal memuat data grafik hasil kepuasan"); toast.error("Gagal memuat data penderita penyakit");
} }
}; };
@@ -99,11 +99,11 @@ function EditGrafikHasilKepuasan() {
setIsSubmitting(true); setIsSubmitting(true);
editState.update.form = { ...editState.update.form, ...formData }; editState.update.form = { ...editState.update.form, ...formData };
await editState.update.submit(); await editState.update.submit();
toast.success('Grafik hasil kepuasan berhasil diperbarui!'); toast.success('penderita penyakit berhasil diperbarui!');
router.push('/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan'); router.push('/admin/kesehatan/data-kesehatan-warga/penderita_penyakit');
} catch (err) { } catch (err) {
console.error('Error updating grafik hasil kepuasan:', err); console.error('Error updating penderita penyakit:', err);
toast.error('Terjadi kesalahan saat memperbarui grafik hasil kepuasan'); toast.error('Terjadi kesalahan saat memperbarui penderita penyakit');
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
} }
@@ -122,7 +122,7 @@ function EditGrafikHasilKepuasan() {
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
<Title order={4} ml="sm" c="dark"> <Title order={4} ml="sm" c="dark">
Edit Grafik Hasil Kepuasan Edit Penderita Penyakit
</Title> </Title>
</Group> </Group>

View File

@@ -26,7 +26,7 @@ function DetailGrafikHasilKepuasan() {
state.delete.byId(selectedId); state.delete.byId(selectedId);
setModalHapus(false); setModalHapus(false);
setSelectedId(null); setSelectedId(null);
router.push("/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan"); router.push("/admin/kesehatan/data-kesehatan-warga/penderita_penyakit");
} }
}; };
@@ -63,7 +63,7 @@ function DetailGrafikHasilKepuasan() {
> >
<Stack gap="md"> <Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}> <Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Data Grafik Hasil Kepuasan Detail Data Penderita Penyakit
</Text> </Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs"> <Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
@@ -118,7 +118,7 @@ function DetailGrafikHasilKepuasan() {
color="green" color="green"
onClick={() => onClick={() =>
router.push( router.push(
`/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/${data.id}/edit` `/admin/kesehatan/data-kesehatan-warga/penderita_penyakit/${data.id}/edit`
) )
} }
variant="light" variant="light"

View File

@@ -40,7 +40,7 @@ function CreateGrafikHasilKepuasanMasyarakat() {
setIsSubmitting(true); setIsSubmitting(true);
await stateGrafikKepuasan.create.create(); await stateGrafikKepuasan.create.create();
resetForm(); resetForm();
router.push("/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan"); router.push("/admin/kesehatan/data-kesehatan-warga/penderita_penyakit");
} catch (error) { } catch (error) {
console.error("Error creating grafik kepuasan:", error); console.error("Error creating grafik kepuasan:", error);
toast.error("Terjadi kesalahan saat membuat grafik kepuasan"); toast.error("Terjadi kesalahan saat membuat grafik kepuasan");
@@ -62,7 +62,7 @@ function CreateGrafikHasilKepuasanMasyarakat() {
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
<Title order={4} ml="sm" c="dark"> <Title order={4} ml="sm" c="dark">
Tambah Grafik Hasil Kepuasan Masyarakat Tambah Penderita Penyakit
</Title> </Title>
</Group> </Group>

View File

@@ -36,7 +36,7 @@ function GrafikHasilKepuasanMasyarakat() {
<Box> <Box>
{/* Header Search */} {/* Header Search */}
<HeaderSearch <HeaderSearch
title='Grafik Hasil Kepuasan Masyarakat' title='Penderita Penyakit'
placeholder='Cari nama atau alamat...' placeholder='Cari nama atau alamat...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
@@ -115,14 +115,14 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
{/* Judul + Tombol Tambah */} {/* Judul + Tombol Tambah */}
<Group justify="space-between" mb="md"> <Group justify="space-between" mb="md">
<Title order={4}>Daftar Grafik Hasil Kepuasan Masyarakat</Title> <Title order={4}>Daftar Penderita Penyakit</Title>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
variant="light" variant="light"
onClick={() => onClick={() =>
router.push( router.push(
'/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/create' '/admin/kesehatan/data-kesehatan-warga/penderita_penyakit/create'
) )
} }
> >
@@ -176,7 +176,7 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
color="blue" color="blue"
onClick={() => onClick={() =>
router.push( router.push(
`/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/${item.id}` `/admin/kesehatan/data-kesehatan-warga/penderita_penyakit/${item.id}`
) )
} }
> >
@@ -221,7 +221,7 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
{/* Chart */} {/* Chart */}
<Box mt="lg" style={{ width: '100%', minWidth: 300, height: 420, minHeight: 300 }}> <Box mt="lg" style={{ width: '100%', minWidth: 300, height: 420, minHeight: 300 }}>
<Paper withBorder bg={colors['white-1']} p={'md'}> <Paper withBorder bg={colors['white-1']} p={'md'}>
<Title pb={10} order={4}>Grafik Hasil Kepuasan Masyarakat</Title> <Title pb={10} order={4}>Penderita Penyakit</Title>
{mounted && diseaseChartData.length > 0 ? ( {mounted && diseaseChartData.length > 0 ? (
<Center> <Center>
<BarChart <BarChart

View File

@@ -1,6 +1,5 @@
import Elysia from "elysia"; import Elysia from "elysia";
import DaftarInformasiPublik from "./daftar_informasi_publik"; import DaftarInformasiPublik from "./daftar_informasi_publik";
import GrafikHasilKepuasanMasyarakat from "./ikm/grafik_hasil_kepuasan_masyarakat";
import GrafikBerdasarkanJenisKelamin from "./ikm/grafik_berdasarkan_jenis_kelamin"; import GrafikBerdasarkanJenisKelamin from "./ikm/grafik_berdasarkan_jenis_kelamin";
import GrafikBerdasarkanResponden from "./ikm/grafik_responden"; import GrafikBerdasarkanResponden from "./ikm/grafik_responden";
import GrafikBerdasarkanUmur from "./ikm/grafik_berdasarkan_umur"; import GrafikBerdasarkanUmur from "./ikm/grafik_berdasarkan_umur";
@@ -10,6 +9,7 @@ import ProfilePPID from "./profile_ppid";
import VisiMisiPPID from "./visi_misi_ppid/visi_misi_ppid"; import VisiMisiPPID from "./visi_misi_ppid/visi_misi_ppid";
import DasarHukumPPID from "./dasar_hukum"; import DasarHukumPPID from "./dasar_hukum";
import StrukturPPID from "./struktur_ppid"; import StrukturPPID from "./struktur_ppid";
import GrafikHasilKepuasanMasyarakat from "./ikm/grafik_hasil_kepuasan_masyarakat";

View File

@@ -26,7 +26,7 @@ function Page() {
const state = useProxy(lowonganKerjaState) const state = useProxy(lowonganKerjaState)
const router = useRouter() const router = useRouter()
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const { const {
data, data,

View File

@@ -14,7 +14,7 @@ function Page() {
const router = useRouter() const router = useRouter()
const state = useProxy(pasarDesaState.pasarDesa) const state = useProxy(pasarDesaState.pasarDesa)
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const [selectedCategory, setSelectedCategory] = useState<string | null>(null); const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
const { const {
data, data,

View File

@@ -32,7 +32,7 @@ interface ProgramKemiskinanData {
function Page() { function Page() {
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const state = useProxy(programKemiskinanState) const state = useProxy(programKemiskinanState)
// 🔧 Get valid statistics data with proper type checking // 🔧 Get valid statistics data with proper type checking

View File

@@ -11,7 +11,7 @@ import { useRouter } from 'next/navigation';
function Page() { function Page() {
const [search, setSearch] = useState("") const [search, setSearch] = useState("")
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const state = useProxy(desaDigitalState) const state = useProxy(desaDigitalState)
const router = useRouter() const router = useRouter()
const { const {

View File

@@ -11,7 +11,7 @@ import { IconSearch } from '@tabler/icons-react';
function Page() { function Page() {
const [search, setSearch] = useState("") const [search, setSearch] = useState("")
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const state = useProxy(infoTeknoState) const state = useProxy(infoTeknoState)
const { const {
data, data,

View File

@@ -45,7 +45,7 @@ import BackButton from '../../desa/layanan/_com/BackButto';
function Page() { function Page() {
const listState = useProxy(programKreatifState); const listState = useProxy(programKreatifState);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [debouncedSearch] = useDebouncedValue(search, 500); const [debouncedSearch] = useDebouncedValue(search, 1000);
const router = useTransitionRouter() const router = useTransitionRouter()
const { const {
data, data,

View File

@@ -14,7 +14,7 @@ function Page() {
const state = useProxy(keamananLingkunganState) const state = useProxy(keamananLingkunganState)
const router = useRouter() const router = useRouter()
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const { const {
data, data,
page, page,

View File

@@ -12,7 +12,7 @@ import { IconKey, IconMapper } from '@/app/admin/(dashboard)/_com/iconMap';
function Page() { function Page() {
const kontakState = useProxy(kontakDarurat.kontakDaruratKeamananState); const kontakState = useProxy(kontakDarurat.kontakDaruratKeamananState);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [debouncedSearch] = useDebouncedValue(search, 500); const [debouncedSearch] = useDebouncedValue(search, 1000);
const { const {
data, data,
page, page,

View File

@@ -1,10 +1,26 @@
'use client' 'use client'
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import laporanPublikState from '@/app/admin/(dashboard)/_state/keamanan/laporan-publik'; import laporanPublikState from '@/app/admin/(dashboard)/_state/keamanan/laporan-publik';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, ColorSwatch, Flex, Group, Modal, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core'; import {
Box,
Button,
Center,
ColorSwatch,
Flex,
Group,
Modal,
Pagination,
Paper,
SimpleGrid,
Skeleton,
Stack,
Text,
TextInput,
} from '@mantine/core';
import { DateTimePicker } from '@mantine/dates'; import { DateTimePicker } from '@mantine/dates';
import { useDebouncedValue, useDisclosure, useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useDisclosure, useMediaQuery, useShallowEffect } from '@mantine/hooks';
import { IconArrowRight, IconPlus, IconSearch } from '@tabler/icons-react'; import { IconArrowRight, IconPlus, IconSearch } from '@tabler/icons-react';
import { useTransitionRouter } from 'next-view-transitions'; import { useTransitionRouter } from 'next-view-transitions';
import { useState } from 'react'; import { useState } from 'react';
@@ -12,9 +28,10 @@ import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto'; import BackButton from '../../desa/layanan/_com/BackButto';
function Page() { function Page() {
const [search, setSearch] = useState(""); const mobile = useMediaQuery('(max-width: 768px)');
const router = useTransitionRouter() const [search, setSearch] = useState('');
const [debouncedSearch] = useDebouncedValue(search, 500); const router = useTransitionRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const [opened, { open, close }] = useDisclosure(false); const [opened, { open, close }] = useDisclosure(false);
const stateLaporan = useProxy(laporanPublikState); const stateLaporan = useProxy(laporanPublikState);
const { const {
@@ -49,143 +66,219 @@ function Page() {
const handleSubmit = async () => { const handleSubmit = async () => {
await stateLaporan.create.create(); await stateLaporan.create.create();
resetForm(); resetForm();
close();
}; };
return ( return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}> <Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
{/* Header: Back + Search */}
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
<Group justify="space-between" align="center"> <Group justify="space-between" align="center">
<BackButton /> <BackButton />
<TextInput <TextInput
radius={"lg"} radius="lg"
placeholder='Cari Laporan Publik' placeholder="Cari Laporan Publik"
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
leftSection={<IconSearch size={20} />} leftSection={<IconSearch size={20} />}
w={{ base: "100%", md: "30%" }} w={{ base: '100%', md: '30%' }}
/> size={mobile ? 'sm' : 'md'}
/>
</Group> </Group>
</Box> </Box>
{/* Title + Add Button */}
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
<Group justify="space-between"> <Group justify="space-between" align="flex-start">
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}> <Text
ta="center"
fz={{ base: 'xl', sm: '2xl', md: '2.5rem' }}
c={colors['blue-button']}
fw="bold"
lineClamp={2}
style={{ wordBreak: 'break-word' }}
>
Laporan Keamanan Lingkungan Laporan Keamanan Lingkungan
</Text> </Text>
<Button <Button
onClick={open} onClick={open}
bg={colors['blue-button']} bg={colors['blue-button']}
size="md" size={mobile ? 'sm' : 'md'}
radius="md" radius="md"
rightSection={<IconPlus size={20} />} rightSection={<IconPlus size={20} />}
> >
Tambah Laporan {mobile ? 'Tambah' : 'Tambah Laporan'}
</Button> </Button>
</Group> </Group>
</Box> </Box>
<Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'}> {/* Legend Status */}
<Flex justify={'space-between'} align={'center'}> <Box px={{ base: 'md', md: 100 }}>
<Text fz={{ base: 'sm', md: 'h4' }} fw={'bold'}>Laporan Terbaru</Text> <Flex
<Box> justify="space-between"
<Flex gap={'lg'}> align="center"
<Box> direction={mobile ? 'column' : 'row'}
<Flex gap={{ base: 2, md: 5 }} align={'center'}> gap={mobile ? 'xs' : 'lg'}
<Text fz={{ base: 'sm', md: 'h4' }}>Terselesaikan</Text> >
<ColorSwatch color="#2A742D" size={20} /> <Text fz={{ base: 'sm', md: 'h4' }} fw="bold">
</Flex> Laporan Terbaru
</Box> </Text>
<Box> <Flex
<Flex gap={{ base: 2, md: 5 }} align={'center'}> gap={mobile ? 'xs' : 'lg'}
<Text fz={{ base: 'sm', md: 'h4' }}>Dalam Proses</Text> wrap="wrap"
<ColorSwatch color="#D1961F" size={20} /> justify={mobile ? 'center' : 'flex-start'}
</Flex> align="center"
</Box>
<Box>
<Flex gap={{ base: 2, md: 5 }} align={'center'}>
<Text fz={{ base: 'sm', md: 'h4' }}>Gagal</Text>
<ColorSwatch color="#A34437" size={20} />
</Flex>
</Box>
</Flex>
</Box>
</Flex>
<SimpleGrid
cols={{
base: 1,
md: 3
}}
> >
{data.map((v, k) => { <Flex gap={2} align="center">
return ( <ColorSwatch color="#2A742D" size={16} />
<Paper radius={'lg'} key={k} bg={colors['white-trans-1']} p={'xl'}> <Text fz={{ base: 'xs', md: 'sm' }}>Terselesaikan</Text>
<Stack> </Flex>
<Text c={colors['blue-button']} lineClamp={3} truncate="end" fz="h4" fw="bold">{v.judul}</Text> <Flex gap={2} align="center">
<Text fs={'italic'} fz={'xl'}> <ColorSwatch color="#D1961F" size={16} />
{v.tanggalWaktu <Text fz={{ base: 'xs', md: 'sm' }}>Dalam Proses</Text>
? new Date(v.tanggalWaktu).toLocaleString('id-ID') </Flex>
: '-'} <Flex gap={2} align="center">
</Text> <ColorSwatch color="#A34437" size={16} />
<Box> <Text fz={{ base: 'xs', md: 'sm' }}>Gagal</Text>
<Text fw={'bold'}>Penanganan:</Text> </Flex>
{v.penanganan?.length ? ( </Flex>
v.penanganan.map((item, index) => ( </Flex>
<Box key={index}>
<Text
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }}
style={{wordBreak: "break-word", whiteSpace: "normal"}}
/>
</Box>
))
) : (
<Text fz="sm" fs="italic" c="dimmed">
Belum ada penanganan
</Text>
)}
</Box>
<Box
style={{
display: 'inline-block',
padding: '4px 12px',
borderRadius: '16px',
backgroundColor:
v.status === 'Selesai' ? '#94EF95FF' :
v.status === 'Proses' ? '#F1D295FF' :
'#F38E8EFF',
color:
v.status === 'Selesai' ? '#01BA01FF' :
v.status === 'Proses' ? '#B67A00FF' :
'#AE1700FF',
fontWeight: 900,
fontSize: '0.75rem',
textAlign: 'center',
minWidth: '80px',
}}
>
{v.status}
</Box>
<Button
bg={colors['blue-button']}
rightSection={<IconArrowRight size={20} color={colors['white-1']} />}
onClick={() => router.push(`/darmasaba/keamanan/laporan-publik/${v.id}`)}
>Lihat Detail Kronologi
</Button>
</Stack>
</Paper>
)
})}
</SimpleGrid>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
total={totalPages}
my="md"
/>
</Center>
</Stack>
</Box> </Box>
<Modal opened={opened} onClose={close} title="Tambah Laporan Publik">
{/* Cards Grid */}
<Box px={{ base: 'md', md: 100 }}>
<SimpleGrid
cols={{
base: 1,
md: 3,
}}
spacing="lg"
>
{data.map((v, k) => (
<Paper
key={k}
radius="lg"
bg={colors['white-trans-1']}
p="lg"
shadow="sm"
style={{
'&:hover': {
transform: 'translateY(-4px)',
boxShadow: '0 8px 20px rgba(0,0,0,0.1)',
},
transition: 'transform 0.2s ease, box-shadow 0.2s ease',
}}
>
<Stack gap="sm">
<Text
c={colors['blue-button']}
lineClamp={2}
fz={{ base: 'lg', md: 'xl' }}
fw="bold"
style={{ wordBreak: 'break-word' }}
>
{v.judul}
</Text>
<Text
fs="italic"
fz={{ base: 'sm', md: 'md' }}
c="dimmed"
>
{v.tanggalWaktu
? new Date(v.tanggalWaktu).toLocaleString('id-ID')
: '-'}
</Text>
<Box>
<Text fw="bold" fz="sm">
Penanganan:
</Text>
{v.penanganan?.length ? (
v.penanganan.map((item, index) => (
<Box key={index}>
<Text
fz="xs"
c="dimmed"
dangerouslySetInnerHTML={{
__html: item.deskripsi || '-',
}}
style={{
wordBreak: 'break-word',
whiteSpace: 'normal',
maxHeight: '80px',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
/>
</Box>
))
) : (
<Text fz="xs" fs="italic" c="dimmed">
Belum ada penanganan
</Text>
)}
</Box>
<Box
style={{
display: 'inline-block',
padding: '4px 8px',
borderRadius: '12px',
backgroundColor:
v.status === 'Selesai'
? '#94EF95FF'
: v.status === 'Proses'
? '#F1D295FF'
: '#F38E8EFF',
color:
v.status === 'Selesai'
? '#01BA01FF'
: v.status === 'Proses'
? '#B67A00FF'
: '#AE1700FF',
fontWeight: 700,
fontSize: '0.75rem',
textAlign: 'center',
minWidth: '70px',
}}
>
{v.status}
</Box>
<Button
bg={colors['blue-button']}
rightSection={
<IconArrowRight
size={18}
color={colors['white-1']}
/>
}
onClick={() => router.push(`/darmasaba/keamanan/laporan-publik/${v.id}`)}
size={mobile ? 'sm' : 'md'}
fullWidth
>
{mobile ? 'Detail' : 'Lihat Detail Kronologi'}
</Button>
</Stack>
</Paper>
))}
</SimpleGrid>
</Box>
{/* Pagination */}
<Center px={{ base: 'md', md: 100 }}>
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
total={totalPages}
my="md"
size={mobile ? 'sm' : 'md'}
/>
</Center>
{/* Modal Form */}
<Modal opened={opened} onClose={close} title="Tambah Laporan Publik" size="xl">
<Paper <Paper
bg={colors['white-1']} bg={colors['white-1']}
p="lg" p="lg"
@@ -196,18 +289,26 @@ function Page() {
<Stack gap="md"> <Stack gap="md">
<TextInput <TextInput
value={stateLaporan.create.form.judul} value={stateLaporan.create.form.judul}
onChange={(e) => (stateLaporan.create.form.judul = e.target.value)} onChange={(e) =>
(stateLaporan.create.form.judul = e.target.value)
}
label={<Text fw="bold" fz="sm">Judul Laporan Publik</Text>} label={<Text fw="bold" fz="sm">Judul Laporan Publik</Text>}
placeholder="Masukkan judul laporan publik" placeholder="Masukkan judul laporan publik"
required required
w="100%"
size={mobile ? 'sm' : 'md'}
/> />
<TextInput <TextInput
value={stateLaporan.create.form.lokasi} value={stateLaporan.create.form.lokasi}
onChange={(e) => (stateLaporan.create.form.lokasi = e.target.value)} onChange={(e) =>
(stateLaporan.create.form.lokasi = e.target.value)
}
label={<Text fw="bold" fz="sm">Lokasi Laporan Publik</Text>} label={<Text fw="bold" fz="sm">Lokasi Laporan Publik</Text>}
placeholder="Masukkan lokasi laporan publik" placeholder="Masukkan lokasi laporan publik"
required required
w="100%"
size={mobile ? 'sm' : 'md'}
/> />
<DateTimePicker <DateTimePicker
@@ -220,6 +321,8 @@ function Page() {
onChange={(val) => { onChange={(val) => {
stateLaporan.create.form.tanggalWaktu = val ? val.toString() : ''; stateLaporan.create.form.tanggalWaktu = val ? val.toString() : '';
}} }}
w="100%"
size={mobile ? 'sm' : 'md'}
/> />
<Box> <Box>
@@ -238,7 +341,7 @@ function Page() {
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
size="md" size={mobile ? 'sm' : 'md'}
style={{ style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff', color: '#fff',

View File

@@ -13,7 +13,7 @@ import { useDebouncedValue } from '@mantine/hooks';
function Page() { function Page() {
const state = useProxy(polsekTerdekatState); const state = useProxy(polsekTerdekatState);
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const router = useRouter() const router = useRouter()
const { const {

View File

@@ -12,7 +12,7 @@ import { IconSearch } from '@tabler/icons-react';
function Page() { function Page() {
const state = useProxy(tipsKeamananState) const state = useProxy(tipsKeamananState)
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const { const {
data, data,
page, page,

View File

@@ -2,7 +2,7 @@
import artikelKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/artikelKesehatan'; import artikelKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/artikelKesehatan';
import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto'; import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Divider, Group, Image, List, ListItem, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import { Box, Divider, Flex, Group, Image, List, ListItem, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconAlertCircle, IconCalendar, IconInfoCircle } from '@tabler/icons-react'; import { IconAlertCircle, IconCalendar, IconInfoCircle } from '@tabler/icons-react';
import { useParams } from 'next/navigation'; import { useParams } from 'next/navigation';
@@ -78,25 +78,33 @@ function Page() {
<Box> <Box>
<Text fz="h4" fw="bold">Pendahuluan</Text> <Text fz="h4" fw="bold">Pendahuluan</Text>
<Divider my="xs" /> <Divider my="xs" />
<Text fz="md" lh={1.6} ta="justify" dangerouslySetInnerHTML={{ __html: state.findUnique.data.introduction?.content }} /> <Box pl={20}>
<Text fz="md" lh={1.6} ta="justify" dangerouslySetInnerHTML={{ __html: state.findUnique.data.introduction?.content }} />
</Box>
</Box> </Box>
<Box> <Box>
<Text fz="h4" fw="bold">{state.findUnique.data.symptom?.title}</Text> <Text fz="h4" fw="bold">{state.findUnique.data.symptom?.title}</Text>
<Divider my="xs" /> <Divider my="xs" />
<Text fz="md" lh={1.6} ta="justify" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: state.findUnique.data.symptom?.content }} /> <Box pl={20}>
<Text fz="md" lh={1.6} ta="justify" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.symptom?.content }} />
</Box>
</Box> </Box>
<Box> <Box>
<Text fz="h4" fw="bold">{state.findUnique.data.prevention?.title}</Text> <Text fz="h4" fw="bold">{state.findUnique.data.prevention?.title}</Text>
<Divider my="xs" /> <Divider my="xs" />
<Text fz="md" lh={1.6} ta="justify" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: state.findUnique.data.prevention?.content }} /> <Box pl={20}>
<Text fz="md" lh={1.6} ta="justify" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.prevention?.content }} />
</Box>
</Box> </Box>
<Box> <Box>
<Text fz="h4" fw="bold">{state.findUnique.data.firstaid?.title}</Text> <Text fz="h4" fw="bold">{state.findUnique.data.firstaid?.title}</Text>
<Divider my="xs" /> <Divider my="xs" />
<Text fz="md" lh={1.6} ta="justify" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: state.findUnique.data.firstaid?.content }} /> <Box pl={20}>
<Text fz="md" lh={1.6} ta="justify" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.firstaid?.content }} />
</Box>
</Box> </Box>
<Box> <Box>
@@ -114,10 +122,14 @@ function Page() {
{state.findUnique.data?.mythvsfact ? ( {state.findUnique.data?.mythvsfact ? (
<TableTr> <TableTr>
<TableTd> <TableTd>
<Text fz="sm" lh={1.6} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: state.findUnique.data.mythvsfact.mitos }} /> <Box pl={20}>
<Text fz="sm" lh={1.6} style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.mythvsfact.mitos }} />
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Text fz="sm" lh={1.6} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: state.findUnique.data.mythvsfact.fakta }} /> <Box pl={20}>
<Text fz="sm" lh={1.6} style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.mythvsfact.fakta }} />
</Box>
</TableTd> </TableTd>
</TableTr> </TableTr>
) : ( ) : (
@@ -133,17 +145,15 @@ function Page() {
<Box> <Box>
<Text fz="h4" fw="bold">Kapan Harus ke Dokter?</Text> <Text fz="h4" fw="bold">Kapan Harus ke Dokter?</Text>
<Divider my="xs" /> <Divider my="xs" />
<Group gap="xs" mb="xs"> <Flex justify={'flex-start'} gap={"xs"} align={"center"} mb="xs">
<IconAlertCircle size={18} color="red" /> <IconAlertCircle size={18} color="red" />
<Text fz="md">Segera bawa penderita ke fasilitas kesehatan jika mengalami:</Text> <Text fz="md">Segera bawa penderita ke fasilitas kesehatan jika mengalami:</Text>
</Group> </Flex>
<Text fz="md" lh={1.6} dangerouslySetInnerHTML={{ __html: state.findUnique.data.doctorsign.content }} /> <Box pl={20}>
<Text fz="md" lh={1.6} dangerouslySetInnerHTML={{ __html: state.findUnique.data.doctorsign.content }} /></Box>
</Box> </Box>
<Box> <Box>
<Text fz="h4" fw="bold">Kasus DBD di Wilayah Abiansemal</Text>
<Divider my="xs" />
<Paper p="lg" radius="md" bg={colors['blue-button-trans']} withBorder> <Paper p="lg" radius="md" bg={colors['blue-button-trans']} withBorder>
<Group gap="xs" mb="sm"> <Group gap="xs" mb="sm">
<IconInfoCircle size={20} color={colors['white-1']} /> <IconInfoCircle size={20} color={colors['white-1']} />

View File

@@ -95,7 +95,7 @@ function GrafikPenyakit() {
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}> <Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}>
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p={'md'}>
<Center> <Center>
<Title pb={10} order={3}>Grafik Hasil Kepuasan Masyarakat</Title> <Title pb={10} order={2}>Penderita Penyakit</Title>
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text> <Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text>
</Center> </Center>
</Paper> </Paper>
@@ -103,7 +103,7 @@ function GrafikPenyakit() {
) : ( ) : (
<Box style={{ width: '100%', minWidth: 300, height: 420, minHeight: 300 }}> <Box style={{ width: '100%', minWidth: 300, height: 420, minHeight: 300 }}>
<Paper bg={colors["white-trans-1"]} p={'md'}> <Paper bg={colors["white-trans-1"]} p={'md'}>
<Title pb={10} order={4}>Grafik Hasil Kepuasan Masyarakat</Title> <Title pb={10} order={2}>Penderita Penyakit</Title>
{mounted && diseaseChartData.length > 0 && ( {mounted && diseaseChartData.length > 0 && (
<Center> <Center>
<BarChart width={isMobile ? 450 : isTablet ? 500 : 550} height={350} data={diseaseChartData} > <BarChart width={isMobile ? 450 : isTablet ? 500 : 550} height={350} data={diseaseChartData} >

View File

@@ -69,25 +69,33 @@ function Page() {
<Stack gap="sm"> <Stack gap="sm">
<Text fz="lg" fw="bold">Deskripsi Kegiatan</Text> <Text fz="lg" fw="bold">Deskripsi Kegiatan</Text>
<Divider /> <Divider />
<Text ta="justify" fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.deskripsijadwalkegiatan.deskripsi }} /> <Box pl={20}>
<Text ta="justify" fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.deskripsijadwalkegiatan.deskripsi }} />
</Box>
</Stack> </Stack>
<Stack gap="sm"> <Stack gap="sm">
<Text fz="lg" fw="bold">Layanan yang Tersedia</Text> <Text fz="lg" fw="bold">Layanan yang Tersedia</Text>
<Divider /> <Divider />
<Text ta="justify" fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.layananjadwalkegiatan.content }} /> <Box pl={20}>
<Text ta="justify" fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.layananjadwalkegiatan.content }} />
</Box>
</Stack> </Stack>
<Stack gap="sm"> <Stack gap="sm">
<Text fz="lg" fw="bold">Syarat & Ketentuan</Text> <Text fz="lg" fw="bold">Syarat & Ketentuan</Text>
<Divider /> <Divider />
<Text ta="justify" fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.syaratketentuanjadwalkegiatan.content }} /> <Box pl={20}>
<Text ta="justify" fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.syaratketentuanjadwalkegiatan.content }} />
</Box>
</Stack> </Stack>
<Stack gap="sm"> <Stack gap="sm">
<Text fz="lg" fw="bold">Dokumen yang Perlu Dibawa</Text> <Text fz="lg" fw="bold">Dokumen yang Perlu Dibawa</Text>
<Divider /> <Divider />
<Text ta="justify" fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.dokumenjadwalkegiatan.content }} /> <Box pl={20}>
<Text ta="justify" fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.dokumenjadwalkegiatan.content }} />
</Box>
</Stack> </Stack>
<Stack gap="sm"> <Stack gap="sm">

View File

@@ -71,11 +71,13 @@ function DetailInfoWabahPenyakitUser() {
{/* Deskripsi */} {/* Deskripsi */}
<Box> <Box>
<Text fz="lg" fw="bold">Deskripsi</Text> <Text fz="lg" fw="bold">Deskripsi</Text>
<Text <Box pl={20}>
fz="md" <Text
dangerouslySetInnerHTML={{ __html: data.deskripsiLengkap || '-' }} fz="md"
style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.deskripsiLengkap || '-' }}
/> style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/>
</Box>
</Box> </Box>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -30,7 +30,7 @@ function Page() {
const state = useProxy(infoWabahPenyakit); const state = useProxy(infoWabahPenyakit);
const router = useTransitionRouter(); const router = useTransitionRouter();
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [debouncedSearch] = useDebouncedValue(search, 500) const [debouncedSearch] = useDebouncedValue(search, 1000)
const { data, page, totalPages, loading, load } = state.findMany; const { data, page, totalPages, loading, load } = state.findMany;
useShallowEffect(() => { useShallowEffect(() => {

View File

@@ -0,0 +1,111 @@
'use client'
import kontakDarurat from '@/app/admin/(dashboard)/_state/kesehatan/kontak-darurat/kontakDarurat';
import colors from '@/con/colors';
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconBrandWhatsapp } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
function Page() {
const state = useProxy(kontakDarurat);
const router = useRouter();
const params = useParams();
useShallowEffect(() => {
state.findUnique.load(params?.id as string);
}, []);
if (!state.findUnique.data) {
return (
<Stack py={10}>
<Skeleton height={500} radius="md" />
</Stack>
);
}
const data = state.findUnique.data;
return (
<Box px={{base: 'md', md: 100}} py={10}>
{/* Tombol Back */}
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15}
>
Kembali
</Button>
{/* Wrapper Detail */}
<Paper
withBorder
w="100%"
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Kontak Darurat
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz="lg" fw="bold">Judul</Text>
<Text fz="md" c="dimmed">{data.name || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Whatsapp</Text>
<Text fz="md" c="dimmed">{data.whatsapp || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Deskripsi</Text>
<Text
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/>
</Box>
<Box>
<Text fz="lg" fw="bold">Gambar</Text>
{data.image?.link ? (
<Image
src={data.image.link}
alt="gambar"
radius="md"
maw={300}
loading="lazy"
/>
) : (
<Text fz="md" c="dimmed">-</Text>
)}
</Box>
<Group>
<Button
variant="light"
leftSection={<IconBrandWhatsapp size={18} />}
component="a"
href={`https://wa.me/${data.whatsapp.replace(/\D/g, '')}`}
target="_blank"
aria-label="Hubungi WhatsApp"
>
WhatsApp
</Button>
</Group>
</Stack>
</Paper>
</Stack>
</Paper>
</Box>
);
}
export default Page;

View File

@@ -7,6 +7,7 @@ import {
Center, Center,
Grid, Grid,
GridCol, GridCol,
Group,
Image, Image,
Pagination, Pagination,
Paper, Paper,
@@ -17,17 +18,18 @@ import {
TextInput, TextInput,
Tooltip Tooltip
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconBrandWhatsapp, IconSearch } from '@tabler/icons-react'; import { IconSearch } from '@tabler/icons-react';
import { useTransitionRouter } from 'next-view-transitions';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto'; import BackButton from '../../desa/layanan/_com/BackButto';
import { useDebouncedValue } from '@mantine/hooks';
function Page() { function Page() {
const state = useProxy(kontakDarurat); const state = useProxy(kontakDarurat);
const router = useTransitionRouter()
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [debouncedSearch] = useDebouncedValue(search, 500) const [debouncedSearch] = useDebouncedValue(search, 1000)
const { data, page, totalPages, loading, load } = state.findMany; const { data, page, totalPages, loading, load } = state.findMany;
useShallowEffect(() => { useShallowEffect(() => {
@@ -88,81 +90,77 @@ function Page() {
) : ( ) : (
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="xl" mt="lg"> <SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="xl" mt="lg">
{data.map((v, k) => ( {data.map((v, k) => (
<Paper <Paper
key={k} key={k}
radius="xl" radius="xl"
shadow="md" shadow="md"
withBorder withBorder
p="lg" p="lg"
bg={colors['white-trans-1']} bg={colors['white-trans-1']}
style={{ style={{
transition: 'all 200ms ease', transition: 'all 200ms ease',
cursor: 'pointer', cursor: 'pointer',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
justifyContent: 'space-between', // ✅ biar button selalu di bawah justifyContent: 'space-between', // ✅ biar button selalu di bawah
height: '100%', // ✅ bikin tinggi seragam height: '100%', // ✅ bikin tinggi seragam
}} }}
> >
<Stack align="center" gap="sm" style={{ flexGrow: 1 }}> <Stack align="center" gap="sm" style={{ flexGrow: 1 }}>
<Box <Box
style={{ style={{
width: '100%', width: '100%',
aspectRatio: '16/9', aspectRatio: '16/9',
borderRadius: '12px', borderRadius: '12px',
overflow: 'hidden', overflow: 'hidden',
position: 'relative', position: 'relative',
}} }}
> >
<Image <Image
src={v.image.link} src={v.image.link}
alt={v.name} alt={v.name}
fit="cover" fit="cover"
loading="lazy" loading="lazy"
style={{ style={{
width: '100%', width: '100%',
height: '100%', height: '100%',
transition: 'transform 0.4s ease', transition: 'transform 0.4s ease',
}} }}
onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.05)')} onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.05)')}
onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')} onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')}
/> />
</Box> </Box>
<Text ta="center" fw={700} fz="lg" c={colors['blue-button']}> <Text ta="center" fw={700} fz="lg" c={colors['blue-button']}>
{v.name} {v.name}
</Text> </Text>
<Text <Text
fz="sm" fz="sm"
ta="center" ta="center"
lineClamp={3} lineClamp={3}
lh={1.6} lh={1.6}
style={{ style={{
minHeight: '4.8em', // tinggi tetap 3 baris minHeight: '4.8em', // tinggi tetap 3 baris
}} }}
> >
<span <span
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }} style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
dangerouslySetInnerHTML={{ __html: v.deskripsi }} dangerouslySetInnerHTML={{ __html: v.deskripsi }}
/> />
</Text> </Text>
</Stack> </Stack>
{/* ✅ Tombol selalu di bagian bawah card */} {/* ✅ Tombol selalu di bagian bawah card */}
<Center mt="md"> <Group mt="md" justify='center'>
<Button <Button
variant="light" bg={colors['blue-button']}
leftSection={<IconBrandWhatsapp size={18} />} onClick={() => router.push(`/darmasaba/kesehatan/kontak-darurat/${v.id}`)}
component="a" >
href={`https://wa.me/${v.whatsapp.replace(/\D/g, '')}`} Detail
target="_blank" </Button>
aria-label="Hubungi WhatsApp" </Group>
> </Paper>
WhatsApp
</Button>
</Center>
</Paper>
))} ))}

View File

@@ -26,7 +26,7 @@ import BackButton from '../../desa/layanan/_com/BackButto'
function Page() { function Page() {
const state = useProxy(penangananDarurat) const state = useProxy(penangananDarurat)
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const [debouncedSearch] = useDebouncedValue(search, 500) const [debouncedSearch] = useDebouncedValue(search, 1000)
const { data, page, totalPages, loading, load } = state.findMany const { data, page, totalPages, loading, load } = state.findMany
useShallowEffect(() => { useShallowEffect(() => {

View File

@@ -12,7 +12,7 @@ import { useTransitionRouter } from "next-view-transitions";
export default function Page() { export default function Page() {
const state = useProxy(posyandustate); const state = useProxy(posyandustate);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const router = useTransitionRouter() const router = useTransitionRouter()
const { data, page, totalPages, loading, load } = state.findMany; const { data, page, totalPages, loading, load } = state.findMany;

View File

@@ -57,7 +57,7 @@ export default function Page() {
const state = useProxy(programKesehatan); const state = useProxy(programKesehatan);
const router = useRouter(); const router = useRouter();
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const { data, page, totalPages, loading, load } = state.findMany; const { data, page, totalPages, loading, load } = state.findMany;
useShallowEffect(() => { useShallowEffect(() => {

View File

@@ -7,10 +7,12 @@ import { IconSearch, IconMapPin, IconPhone, IconMail } from '@tabler/icons-react
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto'; import BackButton from '../../desa/layanan/_com/BackButto';
import { useDebouncedValue } from '@mantine/hooks';
function Page() { function Page() {
const state = useProxy(puskesmasState) const state = useProxy(puskesmasState)
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { const {
data, data,
@@ -21,8 +23,8 @@ function Page() {
} = state.findMany; } = state.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 6, search) load(page, 6, debouncedSearch)
}, [page, search]) }, [page, debouncedSearch])
if (loading || !data) { if (loading || !data) {
return ( return (
@@ -95,17 +97,17 @@ function Page() {
</Group> </Group>
<Stack gap={6}> <Stack gap={6}>
<Group gap="xs" align="flex-start" wrap="nowrap"> <Group gap="xs" align="flex-start" wrap="nowrap">
<Box pt={2}><IconMapPin size={16} /></Box> <Box pt={2}><IconMapPin size={20} /></Box>
<Text fz="sm" c="dimmed">{v.alamat}</Text> <Text fz="sm" c="dimmed">{v.alamat}</Text>
</Group> </Group>
<Group gap="xs" align="flex-start" wrap="nowrap"> <Group gap="xs" align="flex-start" wrap="nowrap">
<Box pt={2}><IconPhone size={16} /></Box> <Box pt={2}><IconPhone size={20} /></Box>
<Text fz="sm" c="dimmed">{v.kontak.kontakPuskesmas}</Text> <Text fz="sm" c="dimmed">{v.kontak.kontakPuskesmas}</Text>
</Group> </Group>
<Group gap="xs" align="flex-start" wrap="nowrap"> <Group gap="xs" align="flex-start" wrap="nowrap">
<Box pt={2}><IconMail size={16} /></Box> <Box pt={2}><IconMail size={20} /></Box>
<Text fz="sm" c="dimmed">{v.kontak.email}</Text> <Text fz="sm" c="dimmed">{v.kontak.email}</Text>
</Group> </Group>
</Stack> </Stack>

View File

@@ -11,7 +11,7 @@ import BackButton from '../../desa/layanan/_com/BackButto';
function Page() { function Page() {
const state = useProxy(dataLingkunganDesaState.findMany) const state = useProxy(dataLingkunganDesaState.findMany)
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const { const {
data, data,

View File

@@ -49,6 +49,7 @@ export function EdukasiCard({ icon, title, description, color = '#1e88e5' }: Edu
</Stack> </Stack>
<Text <Text
size="sm" size="sm"
pl={20}
style={{ style={{
wordBreak: 'break-word', wordBreak: 'break-word',
lineHeight: 1.6, lineHeight: 1.6,

View File

@@ -21,7 +21,7 @@ function Page() {
const state2 = useProxy(pengelolaanSampahState.keteranganSampah) const state2 = useProxy(pengelolaanSampahState.keteranganSampah)
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const [debouncedSearch] = useDebouncedValue(search, 500); const [debouncedSearch] = useDebouncedValue(search, 1000);
const { const {
data, data,

View File

@@ -13,7 +13,7 @@ import { useTransitionRouter } from 'next-view-transitions';
function Page() { function Page() {
const state = useProxy(programPenghijauanState); const state = useProxy(programPenghijauanState);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [debouncedSearch] = useDebouncedValue(search, 500); const [debouncedSearch] = useDebouncedValue(search, 1000);
const router = useTransitionRouter() const router = useTransitionRouter()
const { data, load, page, totalPages, loading } = state.findMany; const { data, load, page, totalPages, loading } = state.findMany;

View File

@@ -14,7 +14,7 @@ interface PageProps {
function Page({ params }: PageProps) { function Page({ params }: PageProps) {
const [search, setSearch] = useState("") const [search, setSearch] = useState("")
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const stateList = useProxy(infoSekolahPaud.lembagaPendidikan) const stateList = useProxy(infoSekolahPaud.lembagaPendidikan)
const { jenjangPendidikan } = use(params); const { jenjangPendidikan } = use(params);

View File

@@ -14,7 +14,7 @@ interface PageProps {
function Page({ params }: PageProps) { function Page({ params }: PageProps) {
const [search, setSearch] = useState("") const [search, setSearch] = useState("")
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const stateList = useProxy(infoSekolahPaud.pengajar) const stateList = useProxy(infoSekolahPaud.pengajar)
const { jenjangPendidikan } = use(params); const { jenjangPendidikan } = use(params);

View File

@@ -14,7 +14,7 @@ interface PageProps {
function Page({ params }: PageProps) { function Page({ params }: PageProps) {
const [search, setSearch] = useState("") const [search, setSearch] = useState("")
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const stateList = useProxy(infoSekolahPaud.siswa) const stateList = useProxy(infoSekolahPaud.siswa)
const { jenjangPendidikan } = use(params); const { jenjangPendidikan } = use(params);

View File

@@ -9,7 +9,7 @@ import { useProxy } from 'valtio/utils';
function Page() { function Page() {
const [search, setSearch] = useState("") const [search, setSearch] = useState("")
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const stateList = useProxy(infoSekolahPaud.lembagaPendidikan) const stateList = useProxy(infoSekolahPaud.lembagaPendidikan)
const { const {

View File

@@ -10,7 +10,7 @@ import { useProxy } from 'valtio/utils';
function Page() { function Page() {
const stateList = useProxy(infoSekolahPaud.pengajar) const stateList = useProxy(infoSekolahPaud.pengajar)
const [search, setSearch] = useState("") const [search, setSearch] = useState("")
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const { const {
data, data,
page, page,

View File

@@ -9,7 +9,7 @@ import { useState } from 'react';
function Page() { function Page() {
const [search, setSearch] = useState("") const [search, setSearch] = useState("")
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const stateList = useProxy(infoSekolahPaud.siswa) const stateList = useProxy(infoSekolahPaud.siswa)
const { const {

View File

@@ -33,7 +33,7 @@ import { useTransitionRouter } from 'next-view-transitions';
function Page() { function Page() {
const listData = useProxy(daftarInformasiPublik) const listData = useProxy(daftarInformasiPublik)
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const router = useTransitionRouter() const router = useTransitionRouter()
const { const {
data, data,

View File

@@ -82,13 +82,13 @@ const state = useProxy(indeksKepuasanState.responden);
// Update gender chart data // Update gender chart data
setDonutDataJenisKelamin([ setDonutDataJenisKelamin([
{ name: 'Laki-laki', value: totalLaki, color: colors['blue-button'] }, { name: 'Laki-laki', value: totalLaki, color: '#52ABE3FF' },
{ name: 'Perempuan', value: totalPerempuan, color: '#10A85AFF' }, { name: 'Perempuan', value: totalPerempuan, color: '#10A85AFF' },
]); ]);
// Update rating chart data // Update rating chart data
setDonutDataRating([ setDonutDataRating([
{ name: 'Sangat Baik', value: totalSangatBaik, color: colors['blue-button'] }, { name: 'Sangat Baik', value: totalSangatBaik, color: '#52ABE3FF' },
{ name: 'Baik', value: totalBaik, color: '#10A85AFF' }, { name: 'Baik', value: totalBaik, color: '#10A85AFF' },
{ name: 'Kurang Baik', value: totalKurangBaik, color: '#FFA500' }, { name: 'Kurang Baik', value: totalKurangBaik, color: '#FFA500' },
{ name: 'Sangat Kurang Baik', value: totalSangatKurangBaik, color: '#FF4500' }, { name: 'Sangat Kurang Baik', value: totalSangatKurangBaik, color: '#FF4500' },
@@ -96,7 +96,7 @@ const state = useProxy(indeksKepuasanState.responden);
// Update age group chart data // Update age group chart data
setDonutDataKelompokUmur([ setDonutDataKelompokUmur([
{ name: 'Muda', value: totalMuda, color: colors['blue-button'] }, { name: 'Muda', value: totalMuda, color: '#52ABE3FF' },
{ name: 'Dewasa', value: totalDewasa, color: '#10A85AFF' }, { name: 'Dewasa', value: totalDewasa, color: '#10A85AFF' },
{ name: 'Lansia', value: totalLansia, color: '#FFA500' }, { name: 'Lansia', value: totalLansia, color: '#FFA500' },
]); ]);
@@ -216,6 +216,7 @@ const state = useProxy(indeksKepuasanState.responden);
<PieChart <PieChart
withLabels withLabels
withTooltip withTooltip
labelsPosition="inside"
labelsType="percent" labelsType="percent"
size={250} // Fixed size in pixels size={250} // Fixed size in pixels
data={donutDataJenisKelamin} data={donutDataJenisKelamin}
@@ -253,7 +254,7 @@ const state = useProxy(indeksKepuasanState.responden);
withTooltip withTooltip
tooltipAnimationDuration={200} tooltipAnimationDuration={200}
withLabels withLabels
labelsPosition="outside" labelsPosition="inside"
labelsType="percent" labelsType="percent"
withLabelsLine withLabelsLine
size={250} size={250}
@@ -296,7 +297,7 @@ const state = useProxy(indeksKepuasanState.responden);
withTooltip withTooltip
tooltipAnimationDuration={200} tooltipAnimationDuration={200}
withLabels withLabels
labelsPosition="outside" labelsPosition="inside"
labelsType="percent" labelsType="percent"
withLabelsLine withLabelsLine
size={250} size={250}
@@ -482,6 +483,7 @@ const state = useProxy(indeksKepuasanState.responden);
<PieChart <PieChart
withLabels withLabels
withTooltip withTooltip
labelsPosition="inside"
labelsType="percent" labelsType="percent"
size={200} size={200}
data={donutDataJenisKelamin} data={donutDataJenisKelamin}
@@ -519,7 +521,7 @@ const state = useProxy(indeksKepuasanState.responden);
withTooltip withTooltip
tooltipAnimationDuration={200} tooltipAnimationDuration={200}
withLabels withLabels
labelsPosition="outside" labelsPosition="inside"
labelsType="percent" labelsType="percent"
withLabelsLine withLabelsLine
size={200} size={200}
@@ -562,7 +564,7 @@ const state = useProxy(indeksKepuasanState.responden);
withTooltip withTooltip
tooltipAnimationDuration={200} tooltipAnimationDuration={200}
withLabels withLabels
labelsPosition="outside" labelsPosition="inside"
labelsType="percent" labelsType="percent"
withLabelsLine withLabelsLine
size={190} size={190}

View File

@@ -55,8 +55,8 @@ function APBDesProgress({ apbdesData }: APBDesProgressProps) {
return ( return (
<Box key={label}> <Box key={label}>
<Text fw={600} fz="sm">{label}</Text> <Text fw={600} fz={{base: "sm", md: "md"}}>{label}</Text>
<Text fw={700} mb="xs"> <Text fw={700} fz={{base: "sm", md: "md"}} mb="xs">
{formatRupiah(dataset.realisasi)} | {formatRupiah(dataset.anggaran)} {formatRupiah(dataset.realisasi)} | {formatRupiah(dataset.anggaran)}
</Text> </Text>
<Progress <Progress

View File

@@ -39,22 +39,20 @@ function Slider() {
const state = useProxy(penghargaanState); const state = useProxy(penghargaanState);
const router = useTransitionRouter(); const router = useTransitionRouter();
// Refs for smooth animation
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const scrollPositionRef = useRef(0); const scrollPosRef = useRef(0);
const animationFrameRef = useRef<number>(0); const animFrameRef = useRef<number>(0);
const isHoveredRef = useRef(false); const isHoveredRef = useRef(false);
// Refs for drag functionality
const isDraggingRef = useRef(false); const isDraggingRef = useRef(false);
const startXRef = useRef(0); const startXRef = useRef(0);
const scrollLeftRef = useRef(0); const scrollLeftRef = useRef(0);
const velocityRef = useRef(0); const velocityRef = useRef(0);
const lastScrollTimeRef = useRef(0); const lastScrollRef = useRef(0);
// Speed configuration const SPEED_NORMAL = 1.0;
const normalSpeed = 1.0; // pixels per frame const SPEED_HOVER = 0.5;
const hoverSpeed = 0.5; // slower speed on hover const VELOCITY_DECAY = 0.95;
const SCROLL_THRESHOLD = 100;
useEffect(() => { useEffect(() => {
state.findMany.load(); state.findMany.load();
@@ -63,92 +61,88 @@ function Slider() {
const data = state.findMany.data || []; const data = state.findMany.data || [];
const loading = state.findMany.loading; const loading = state.findMany.loading;
// Duplicate slides for seamless infinite loop // Triple data untuk infinite loop (desktop only)
const slidesData = [...data, ...data, ...data]; const slidesData = mobile ? data : [...data, ...data, ...data];
// Auto-scroll animation untuk desktop
useEffect(() => { useEffect(() => {
if (loading || !containerRef.current || slidesData.length === 0) return; if (loading || !containerRef.current || data.length === 0 || mobile) return;
const container = containerRef.current; const container = containerRef.current;
const slideWidth = container.scrollWidth / slidesData.length; const slideWidth = container.scrollWidth / slidesData.length;
const originalDataLength = data.length; const originalLength = data.length;
// Start from the middle set of slides // Start dari middle set
scrollPositionRef.current = slideWidth * originalDataLength; scrollPosRef.current = slideWidth * originalLength;
container.scrollLeft = scrollPositionRef.current; container.scrollLeft = scrollPosRef.current;
const animate = () => { const animate = () => {
if (!containerRef.current) return; if (!containerRef.current) return;
const container = containerRef.current; const container = containerRef.current;
const slideWidth = container.scrollWidth / slidesData.length; const slideWidth = container.scrollWidth / slidesData.length;
const timeSinceScroll = Date.now() - lastScrollRef.current;
const isUserScrolling = timeSinceScroll < SCROLL_THRESHOLD;
// Check if user recently scrolled manually
const timeSinceLastScroll = Date.now() - lastScrollTimeRef.current;
const isUserScrolling = timeSinceLastScroll < 100;
// Only auto-scroll if user is not actively scrolling or dragging
if (!isDraggingRef.current && !isUserScrolling) { if (!isDraggingRef.current && !isUserScrolling) {
const currentSpeed = isHoveredRef.current ? hoverSpeed : normalSpeed; const speed = isHoveredRef.current ? SPEED_HOVER : SPEED_NORMAL;
scrollPositionRef.current += currentSpeed; scrollPosRef.current += speed;
// Reset position for infinite loop // Reset untuk infinite loop
if (scrollPositionRef.current >= slideWidth * (originalDataLength * 2)) { if (scrollPosRef.current >= slideWidth * (originalLength * 2)) {
scrollPositionRef.current -= slideWidth * originalDataLength; scrollPosRef.current -= slideWidth * originalLength;
}
if (scrollPosRef.current <= 0) {
scrollPosRef.current += slideWidth * originalLength;
} }
if (scrollPositionRef.current <= 0) { container.scrollLeft = scrollPosRef.current;
scrollPositionRef.current += slideWidth * originalDataLength;
}
container.scrollLeft = scrollPositionRef.current;
} else { } else {
// Sync scroll position when user is scrolling scrollPosRef.current = container.scrollLeft;
scrollPositionRef.current = container.scrollLeft;
// Apply momentum/velocity for smooth drag release // Momentum untuk drag release
if (!isDraggingRef.current && Math.abs(velocityRef.current) > 0.1) { if (!isDraggingRef.current && Math.abs(velocityRef.current) > 0.1) {
scrollPositionRef.current += velocityRef.current; scrollPosRef.current += velocityRef.current;
velocityRef.current *= 0.95; // Decay velocity velocityRef.current *= VELOCITY_DECAY;
container.scrollLeft = scrollPositionRef.current; container.scrollLeft = scrollPosRef.current;
} }
} }
animationFrameRef.current = requestAnimationFrame(animate); animFrameRef.current = requestAnimationFrame(animate);
}; };
animationFrameRef.current = requestAnimationFrame(animate); animFrameRef.current = requestAnimationFrame(animate);
return () => { return () => {
if (animationFrameRef.current) { if (animFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current); cancelAnimationFrame(animFrameRef.current);
} }
}; };
}, [loading, slidesData.length, data.length, mobile]); }, [loading, data.length, mobile]);
const handleMouseEnter = () => { const handleMouseEnter = () => {
isHoveredRef.current = true; if (!mobile) isHoveredRef.current = true;
}; };
const handleMouseLeave = () => { const handleMouseLeave = () => {
isHoveredRef.current = false; if (!mobile) {
isDraggingRef.current = false; isHoveredRef.current = false;
isDraggingRef.current = false;
}
}; };
// Mouse drag handlers
const handleMouseDown = (e: React.MouseEvent) => { const handleMouseDown = (e: React.MouseEvent) => {
if (!containerRef.current) return; if (!containerRef.current || mobile) return;
isDraggingRef.current = true; isDraggingRef.current = true;
startXRef.current = e.pageX - containerRef.current.offsetLeft; startXRef.current = e.pageX - containerRef.current.offsetLeft;
scrollLeftRef.current = containerRef.current.scrollLeft; scrollLeftRef.current = containerRef.current.scrollLeft;
velocityRef.current = 0; velocityRef.current = 0;
containerRef.current.style.cursor = 'grabbing'; containerRef.current.style.cursor = 'grabbing';
}; };
const handleMouseMove = (e: React.MouseEvent) => { const handleMouseMove = (e: React.MouseEvent) => {
if (!isDraggingRef.current || !containerRef.current) return; if (!isDraggingRef.current || !containerRef.current || mobile) return;
e.preventDefault(); e.preventDefault();
const x = e.pageX - containerRef.current.offsetLeft; const x = e.pageX - containerRef.current.offsetLeft;
@@ -156,27 +150,25 @@ function Slider() {
const newScrollLeft = scrollLeftRef.current - walk; const newScrollLeft = scrollLeftRef.current - walk;
velocityRef.current = containerRef.current.scrollLeft - newScrollLeft; velocityRef.current = containerRef.current.scrollLeft - newScrollLeft;
containerRef.current.scrollLeft = newScrollLeft; containerRef.current.scrollLeft = newScrollLeft;
scrollPositionRef.current = newScrollLeft; scrollPosRef.current = newScrollLeft;
lastScrollTimeRef.current = Date.now(); lastScrollRef.current = Date.now();
}; };
const handleMouseUp = () => { const handleMouseUp = () => {
if (!containerRef.current) return; if (!containerRef.current || mobile) return;
isDraggingRef.current = false; isDraggingRef.current = false;
containerRef.current.style.cursor = 'grab'; containerRef.current.style.cursor = 'grab';
}; };
// Wheel scroll handler
const handleWheel = (e: React.WheelEvent) => { const handleWheel = (e: React.WheelEvent) => {
if (!containerRef.current) return; if (!containerRef.current || mobile) return;
e.preventDefault(); e.preventDefault();
containerRef.current.scrollLeft += e.deltaY; containerRef.current.scrollLeft += e.deltaY;
scrollPositionRef.current = containerRef.current.scrollLeft; scrollPosRef.current = containerRef.current.scrollLeft;
lastScrollTimeRef.current = Date.now(); lastScrollRef.current = Date.now();
}; };
if (loading) { if (loading) {
@@ -211,37 +203,45 @@ function Slider() {
onWheel={handleWheel} onWheel={handleWheel}
py="xl" py="xl"
style={{ style={{
overflow: "hidden", overflowX: mobile ? "auto" : "hidden",
cursor: "grab", overflowY: "hidden",
cursor: mobile ? "default" : "grab",
userSelect: "none", userSelect: "none",
position: "relative", position: "relative",
WebkitOverflowScrolling: "touch",
scrollbarWidth: "none",
msOverflowStyle: "none",
}} }}
> >
{/* Blur edges effect */} {/* Blur edges - hanya untuk desktop */}
<Box {!mobile && (
style={{ <>
position: "absolute", <Box
top: 0, style={{
left: 0, position: "absolute",
width: "120px", top: 0,
height: "100%", left: 0,
background: "linear-gradient(to right, rgba(249, 250, 251, 1), rgba(249, 250, 251, 0))", width: "120px",
zIndex: 10, height: "100%",
pointerEvents: "none", background: "linear-gradient(to right, rgba(249, 250, 251, 1), rgba(249, 250, 251, 0))",
}} zIndex: 10,
/> pointerEvents: "none",
<Box }}
style={{ />
position: "absolute", <Box
top: 0, style={{
right: 0, position: "absolute",
width: "120px", top: 0,
height: "100%", right: 0,
background: "linear-gradient(to left, rgba(249, 250, 251, 1), rgba(249, 250, 251, 0))", width: "120px",
zIndex: 10, height: "100%",
pointerEvents: "none", background: "linear-gradient(to left, rgba(249, 250, 251, 1), rgba(249, 250, 251, 0))",
}} zIndex: 10,
/> pointerEvents: "none",
}}
/>
</>
)}
<Box <Box
style={{ style={{
@@ -255,8 +255,8 @@ function Slider() {
<Box <Box
key={`${item.id}-${index}`} key={`${item.id}-${index}`}
style={{ style={{
flex: `0 0 ${mobile ? "90%" : "calc(33.333% - 1rem)"}`, flex: `0 0 ${mobile ? "85%" : "calc(33.333% - 1rem)"}`,
minWidth: mobile ? "90%" : "calc(33.333% - 1rem)", minWidth: mobile ? "85%" : "calc(33.333% - 1rem)",
}} }}
> >
<Paper <Paper
@@ -272,12 +272,16 @@ function Slider() {
overflow: "hidden", overflow: "hidden",
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
e.currentTarget.style.transform = "translateY(-8px) scale(1.02)"; if (!mobile) {
e.currentTarget.style.boxShadow = "0 12px 28px rgba(0,0,0,0.25)"; e.currentTarget.style.transform = "translateY(-8px) scale(1.02)";
e.currentTarget.style.boxShadow = "0 12px 28px rgba(0,0,0,0.25)";
}
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
e.currentTarget.style.transform = "translateY(0) scale(1)"; if (!mobile) {
e.currentTarget.style.boxShadow = "0 4px 12px rgba(0,0,0,0.15)"; e.currentTarget.style.transform = "translateY(0) scale(1)";
e.currentTarget.style.boxShadow = "0 4px 12px rgba(0,0,0,0.15)";
}
}} }}
> >
<Box <Box

View File

@@ -13,7 +13,7 @@ function Page() {
const state = useProxy(prestasiState.prestasiDesa); const state = useProxy(prestasiState.prestasiDesa);
const router = useRouter(); const router = useRouter();
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const { data, page, totalPages, loading, load } = state.findMany; const { data, page, totalPages, loading, load } = state.findMany;

View File

@@ -25,25 +25,27 @@ const textHeading = {
des: "Layanan adalah fitur yang membantu warga desa mengakses berbagai kebutuhan administrasi, informasi, dan bantuan secara cepat, mudah, dan transparan. Dengan fitur ini, semua layanan desa ada dalam genggaman Anda!", des: "Layanan adalah fitur yang membantu warga desa mengakses berbagai kebutuhan administrasi, informasi, dan bantuan secara cepat, mudah, dan transparan. Dengan fitur ini, semua layanan desa ada dalam genggaman Anda!",
}; };
const HEIGHT = 720;
function Layanan() { function Layanan() {
return ( return (
<Stack pos={"relative"} bg={colors.grey[1]} gap={"xl"} py={"md"}> <Stack pos="relative" bg={colors.grey[1]} gap="xl" py="md">
<Container w={{ base: "100%", md: "80%" }} p={"md"}> <Container w={{ base: "100%", md: "80%" }} p="md">
<Stack align="center" gap={"0"}> <Stack align="center" gap="0">
<Text <Text
fw={"bold"} fw="bold"
c={colors["blue-button"]} c={colors["blue-button"]}
fz={{ base: "1.8rem", md: "3.4rem" }} fz={{ base: "1.8rem", md: "3.4rem" }}
> >
{textHeading.title} {textHeading.title}
</Text> </Text>
<Text ta={"center"} fz={{ base: "1rem", md: "1.3rem" }}> <Text ta="center" fz={{ base: "1rem", md: "1.3rem" }}>
{textHeading.des} {textHeading.des}
</Text> </Text>
<Box p={"md"}> <Box p="md">
<Button <Button
component={Link} component={Link}
href={"/darmasaba/desa/layanan"} href="/darmasaba/desa/layanan"
variant="filled" variant="filled"
bg={colors["blue-button"]} bg={colors["blue-button"]}
radius={100} radius={100}
@@ -59,30 +61,26 @@ function Layanan() {
); );
} }
const height = 720;
function Slider() { function Slider() {
const state = useProxy(stateLayananDesa); const state = useProxy(stateLayananDesa);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const mobile = useMediaQuery("(max-width: 768px)", false); const mobile = useMediaQuery("(max-width: 768px)", false);
const router = useTransitionRouter(); const router = useTransitionRouter();
// Refs for smooth animation
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const scrollPositionRef = useRef(0); const scrollPosRef = useRef(0);
const animationFrameRef = useRef<number>(0); const animFrameRef = useRef<number>(0);
const isHoveredRef = useRef(false); const isHoveredRef = useRef(false);
// Refs for drag functionality
const isDraggingRef = useRef(false); const isDraggingRef = useRef(false);
const startXRef = useRef(0); const startXRef = useRef(0);
const scrollLeftRef = useRef(0); const scrollLeftRef = useRef(0);
const velocityRef = useRef(0); const velocityRef = useRef(0);
const lastScrollTimeRef = useRef(0); const lastScrollRef = useRef(0);
// Speed configuration const SPEED_NORMAL = 1.0;
const normalSpeed = 1.0; // pixels per frame const SPEED_HOVER = 0.3;
const hoverSpeed = 0.3; // slower speed on hover const VELOCITY_DECAY = 0.95;
const SCROLL_THRESHOLD = 100;
useEffect(() => { useEffect(() => {
const loadData = async () => { const loadData = async () => {
@@ -100,126 +98,118 @@ function Slider() {
const data = state.suratKeterangan.findMany.data || []; const data = state.suratKeterangan.findMany.data || [];
// Duplicate slides for seamless infinite loop // Triple data untuk infinite loop (desktop only)
// We need at least 3x the data for smooth infinite scrolling const slidesData = mobile ? data : [...data, ...data, ...data];
const slidesData = [...data, ...data, ...data];
// Auto-scroll animation untuk desktop
useEffect(() => { useEffect(() => {
if (loading || !containerRef.current || slidesData.length === 0) return; if (loading || !containerRef.current || data.length === 0 || mobile) return;
const container = containerRef.current; const container = containerRef.current;
const slideWidth = container.scrollWidth / slidesData.length; const slideWidth = container.scrollWidth / slidesData.length;
const originalDataLength = data.length; const originalLength = data.length;
// Start from the middle set of slides // Start dari middle set
scrollPositionRef.current = slideWidth * originalDataLength; scrollPosRef.current = slideWidth * originalLength;
container.scrollLeft = scrollPositionRef.current; container.scrollLeft = scrollPosRef.current;
const animate = () => { const animate = () => {
if (!containerRef.current) return; if (!containerRef.current) return;
const container = containerRef.current; const container = containerRef.current;
const slideWidth = container.scrollWidth / slidesData.length; const slideWidth = container.scrollWidth / slidesData.length;
const timeSinceScroll = Date.now() - lastScrollRef.current;
const isUserScrolling = timeSinceScroll < SCROLL_THRESHOLD;
// Check if user recently scrolled manually
const timeSinceLastScroll = Date.now() - lastScrollTimeRef.current;
const isUserScrolling = timeSinceLastScroll < 100;
// Only auto-scroll if user is not actively scrolling or dragging
if (!isDraggingRef.current && !isUserScrolling) { if (!isDraggingRef.current && !isUserScrolling) {
const currentSpeed = isHoveredRef.current ? hoverSpeed : normalSpeed; const speed = isHoveredRef.current ? SPEED_HOVER : SPEED_NORMAL;
scrollPositionRef.current += currentSpeed; scrollPosRef.current += speed;
// Reset position for infinite loop // Reset untuk infinite loop
if (scrollPositionRef.current >= slideWidth * (originalDataLength * 2)) { if (scrollPosRef.current >= slideWidth * (originalLength * 2)) {
scrollPositionRef.current -= slideWidth * originalDataLength; scrollPosRef.current -= slideWidth * originalLength;
}
if (scrollPosRef.current <= 0) {
scrollPosRef.current += slideWidth * originalLength;
} }
if (scrollPositionRef.current <= 0) { container.scrollLeft = scrollPosRef.current;
scrollPositionRef.current += slideWidth * originalDataLength;
}
container.scrollLeft = scrollPositionRef.current;
} else { } else {
// Sync scroll position when user is scrolling scrollPosRef.current = container.scrollLeft;
scrollPositionRef.current = container.scrollLeft;
// Apply momentum/velocity for smooth drag release // Momentum untuk drag release
if (!isDraggingRef.current && Math.abs(velocityRef.current) > 0.1) { if (!isDraggingRef.current && Math.abs(velocityRef.current) > 0.1) {
scrollPositionRef.current += velocityRef.current; scrollPosRef.current += velocityRef.current;
velocityRef.current *= 0.95; // Decay velocity velocityRef.current *= VELOCITY_DECAY;
container.scrollLeft = scrollPositionRef.current; container.scrollLeft = scrollPosRef.current;
} }
} }
animationFrameRef.current = requestAnimationFrame(animate); animFrameRef.current = requestAnimationFrame(animate);
}; };
animationFrameRef.current = requestAnimationFrame(animate); animFrameRef.current = requestAnimationFrame(animate);
return () => { return () => {
if (animationFrameRef.current) { if (animFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current); cancelAnimationFrame(animFrameRef.current);
} }
}; };
}, [loading, slidesData.length, data.length, mobile]); }, [loading, data.length, mobile]);
const handleMouseEnter = () => { const handleMouseEnter = () => {
isHoveredRef.current = true; if (!mobile) isHoveredRef.current = true;
}; };
const handleMouseLeave = () => { const handleMouseLeave = () => {
isHoveredRef.current = false; if (!mobile) {
isDraggingRef.current = false; isHoveredRef.current = false;
isDraggingRef.current = false;
}
}; };
// Mouse drag handlers
const handleMouseDown = (e: React.MouseEvent) => { const handleMouseDown = (e: React.MouseEvent) => {
if (!containerRef.current) return; if (!containerRef.current || mobile) return;
isDraggingRef.current = true; isDraggingRef.current = true;
startXRef.current = e.pageX - containerRef.current.offsetLeft; startXRef.current = e.pageX - containerRef.current.offsetLeft;
scrollLeftRef.current = containerRef.current.scrollLeft; scrollLeftRef.current = containerRef.current.scrollLeft;
velocityRef.current = 0; velocityRef.current = 0;
containerRef.current.style.cursor = 'grabbing'; containerRef.current.style.cursor = 'grabbing';
}; };
const handleMouseMove = (e: React.MouseEvent) => { const handleMouseMove = (e: React.MouseEvent) => {
if (!isDraggingRef.current || !containerRef.current) return; if (!isDraggingRef.current || !containerRef.current || mobile) return;
e.preventDefault(); e.preventDefault();
const x = e.pageX - containerRef.current.offsetLeft; const x = e.pageX - containerRef.current.offsetLeft;
const walk = (x - startXRef.current) * 2; // Multiply for faster scroll const walk = (x - startXRef.current) * 2;
const newScrollLeft = scrollLeftRef.current - walk; const newScrollLeft = scrollLeftRef.current - walk;
// Calculate velocity for momentum
velocityRef.current = containerRef.current.scrollLeft - newScrollLeft; velocityRef.current = containerRef.current.scrollLeft - newScrollLeft;
containerRef.current.scrollLeft = newScrollLeft; containerRef.current.scrollLeft = newScrollLeft;
scrollPositionRef.current = newScrollLeft; scrollPosRef.current = newScrollLeft;
lastScrollTimeRef.current = Date.now(); lastScrollRef.current = Date.now();
}; };
const handleMouseUp = () => { const handleMouseUp = () => {
if (!containerRef.current) return; if (!containerRef.current || mobile) return;
isDraggingRef.current = false; isDraggingRef.current = false;
containerRef.current.style.cursor = 'grab'; containerRef.current.style.cursor = 'grab';
}; };
// Wheel scroll handler
const handleWheel = (e: React.WheelEvent) => { const handleWheel = (e: React.WheelEvent) => {
if (!containerRef.current) return; if (!containerRef.current || mobile) return;
e.preventDefault(); e.preventDefault();
containerRef.current.scrollLeft += e.deltaY; containerRef.current.scrollLeft += e.deltaY;
scrollPositionRef.current = containerRef.current.scrollLeft; scrollPosRef.current = containerRef.current.scrollLeft;
lastScrollTimeRef.current = Date.now(); lastScrollRef.current = Date.now();
}; };
if (loading) { if (loading) {
return <Skeleton height={height} />; return <Skeleton height={HEIGHT} />;
} }
if (data.length === 0) { if (data.length === 0) {
@@ -242,9 +232,13 @@ function Slider() {
onMouseUp={handleMouseUp} onMouseUp={handleMouseUp}
onWheel={handleWheel} onWheel={handleWheel}
style={{ style={{
overflow: "hidden", overflowX: mobile ? "auto" : "hidden",
cursor: "grab", overflowY: "hidden",
cursor: mobile ? "default" : "grab",
userSelect: "none", userSelect: "none",
WebkitOverflowScrolling: "touch",
scrollbarWidth: "none",
msOverflowStyle: "none",
}} }}
> >
<Box <Box
@@ -259,13 +253,13 @@ function Slider() {
<Box <Box
key={`${item.id}-${index}`} key={`${item.id}-${index}`}
style={{ style={{
flex: `0 0 ${mobile ? "100%" : "calc(33.333% - 1rem)"}`, flex: `0 0 ${mobile ? "90%" : "calc(33.333% - 1rem)"}`,
minWidth: mobile ? "100%" : "calc(33.333% - 1rem)", minWidth: mobile ? "90%" : "calc(33.333% - 1rem)",
}} }}
> >
<Paper <Paper
h={height} h={HEIGHT}
pos={"relative"} pos="relative"
style={{ style={{
backgroundImage: `url(${item.image?.link})`, backgroundImage: `url(${item.image?.link})`,
backgroundSize: "cover", backgroundSize: "cover",
@@ -280,23 +274,23 @@ function Slider() {
borderRadius: 8, borderRadius: 8,
zIndex: 0, zIndex: 0,
}} }}
pos={"absolute"} pos="absolute"
w={"100%"} w="100%"
h={"100%"} h="100%"
bg={colors.trans.dark[2]} bg={colors.trans.dark[2]}
/> />
<Stack <Stack
justify="space-between" justify="space-between"
h={"100%"} h="100%"
gap={0} gap={0}
p={"lg"} p="lg"
pos={"relative"} pos="relative"
> >
<Box p={"lg"}> <Box p="lg">
<Text <Text
fw={"bold"} fw="bold"
c={"white"} c="white"
size={"3.5rem"} fz={{base: "xl", md: "3.5rem"}}
style={{ style={{
textAlign: "center", textAlign: "center",
}} }}
@@ -310,7 +304,7 @@ function Slider() {
router.push(`/darmasaba/desa/layanan/${item.id}`) router.push(`/darmasaba/desa/layanan/${item.id}`)
} }
px={46} px={46}
radius={"100"} radius="100"
size="md" size="md"
bg={colors["blue-button"]} bg={colors["blue-button"]}
> >