Compare commits

...

7 Commits

61 changed files with 1771 additions and 960 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -3,7 +3,7 @@
"version": "0.1.5",
"private": true,
"scripts": {
"dev": "bun --bun next dev --hostname 0.0.0.0",
"dev": "bun --bun next dev",
"build": "bun --bun next build",
"start": "bun --bun next start"
},
@@ -43,6 +43,7 @@
"@types/bun": "^1.2.2",
"@types/leaflet": "^1.9.20",
"@types/lodash": "^4.17.16",
"@types/nodemailer": "^7.0.2",
"add": "^2.0.6",
"adm-zip": "^0.5.16",
"animate.css": "^4.1.1",
@@ -52,6 +53,7 @@
"classnames": "^2.5.1",
"colors": "^1.4.0",
"dayjs": "^1.11.13",
"dotenv": "^17.2.3",
"elysia": "^1.3.5",
"embla-carousel-autoplay": "^8.5.2",
"embla-carousel-react": "^7.1.0",
@@ -71,6 +73,7 @@
"next": "^15.5.2",
"next-view-transitions": "^0.3.4",
"node-fetch": "^3.3.2",
"nodemailer": "^7.0.10",
"p-limit": "^6.2.0",
"primeicons": "^7.0.0",
"primereact": "^10.9.6",

View File

@@ -55,9 +55,9 @@ function EditProgramKemiskinan() {
useEffect(() => {
if (!id) return;
stateProgram.findUnique
.load(id)
.then(() => {
const loadData = async () => {
try {
await stateProgram.findUnique.load(id);
const data = stateProgram.findUnique.data;
if (data) {
setFormData({
@@ -70,12 +70,16 @@ function EditProgramKemiskinan() {
},
});
}
})
.catch((err) => {
} catch (err) {
console.error('Error load data:', err);
toast.error('Gagal mengambil data program');
});
}, [id, stateProgram.findUnique]);
}
};
loadData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]); // ✅ hanya trigger saat id berubah
// generic handler untuk field top-level
const handleChange = useCallback(

View File

@@ -132,7 +132,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
<Tooltip label="Keluar" position="bottom" withArrow>
<ActionIcon
onClick={() => {
router.push("/login");
router.push("/darmasaba");
}}
color={colors["blue-button"]}
radius="xl"

View File

@@ -25,26 +25,40 @@ const searchState = proxy({
searchState.results = [];
return;
}
searchState.loading = true;
try {
const res = await ApiFetch.api.search.findMany.get({
query: {
query: searchState.query,
page: searchState.page,
limit: searchState.limit,
type: searchState.type,
},
});
console.log("Search API Response:", res);
const rawItems = res.data?.data || [];
const parsedItems = structuredClone(rawItems); // ✅ penting!
console.log("✅ Parsed items:", parsedItems);
if (searchState.page === 1) {
searchState.results = parsedItems;
} else {
searchState.results.push(...parsedItems);
}
const res = await ApiFetch.api.search.findMany.get({
query: {
query: searchState.query,
page: searchState.page,
limit: searchState.limit,
type: searchState.type,
},
});
console.log("Search results render:", searchState.results);
if (searchState.page === 1) {
searchState.results = res.data?.data || [];
} else {
searchState.results.push(...(res.data?.data || []));
searchState.nextPage = res.data?.nextPage || null;
} catch (error) {
console.error("Search fetch error:", error);
} finally {
searchState.loading = false;
}
searchState.nextPage = res.data?.nextPage || null;
searchState.loading = false;
},
async next() {

View File

@@ -0,0 +1,54 @@
import { NextResponse } from 'next/server';
import nodemailer from 'nodemailer';
export async function POST(request: Request) {
try {
const { email } = await request.json();
// Input validation
if (!email) {
return NextResponse.json(
{ success: false, message: 'Email is required' },
{ status: 400 }
);
}
// Email regex validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return NextResponse.json(
{ success: false, message: 'Invalid email format' },
{ status: 400 }
);
}
// Configure nodemailer
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS,
},
});
// Send email
await transporter.sendMail({
from: `"Tim Info" <${process.env.EMAIL_USER}>`,
to: email,
subject: '✅ Berhasil Berlangganan!',
html: `<p>Terima kasih telah berlangganan info terbaru dari kami!</p>`,
});
return NextResponse.json({
success: true,
message: 'Subscription successful! Please check your email.',
});
} catch (error) {
console.error('Error in subscribe API:', error);
return NextResponse.json(
{ success: false, message: 'Internal server error' },
{ status: 500 }
);
}
}

View File

@@ -62,11 +62,23 @@ function Page() {
Informasi dan Pelayanan Administrasi Digital
</Text>
</Box>
<Image src={state.findUnique.data?.image?.link || ''} alt='' w={"100%"} loading="lazy"/>
<Image src={state.findUnique.data?.image?.link || ''} alt='' w={"100%"} loading="lazy" />
</Container>
<Box px={{ base: "md", md: 100 }}>
<Stack gap={"xs"}>
<Text py={20} fz={{ base: "sm", md: "lg" }} ta={"justify"} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: state.findUnique.data?.content || '' }} />
<Text
py={20}
fz={{ base: "sm", md: "lg" }}
lh={{ base: 1.6, md: 1.8 }} // ✅ line-height lebih rapat dan responsif
ta="justify"
style={{
wordBreak: "break-word",
whiteSpace: "normal",
}}
dangerouslySetInnerHTML={{
__html: state.findUnique.data?.content || "",
}}
/>
</Stack>
</Box>
</Stack>

View File

@@ -186,12 +186,11 @@ function Page() {
stateCreate.create.form.kategoriId = '';
}
}}
searchable
// searchable
clearable
nothingFoundMessage="Tidak ditemukan"
required
/>
<Button bg={colors['blue-button']} onClick={handleSubmit}>
Simpan
</Button>

View File

@@ -7,34 +7,52 @@ import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
function PelayananPerizinanBerusaha() {
const state = useProxy(stateLayananDesa)
const [loading, setLoading] = useState(false)
const [active, setActive] = useState(1);
const nextStep = () => setActive((current) => (current < 6 ? current + 1 : current));
const prevStep = () => setActive((current) => (current > 0 ? current - 1 : current));
const state = useProxy(stateLayananDesa);
const [loading, setLoading] = useState(false);
const [active, setActive] = useState(0);
const totalSteps = 6;
const nextStep = () => {
if (active < totalSteps - 1) {
setActive(active + 1);
} else if (active === totalSteps - 1) {
setActive(totalSteps); // Mark as completed
}
};
const prevStep = () => {
if (active > 0) {
setActive(active - 1);
}
};
useEffect(() => {
const loadData = async () => {
try {
setLoading(true);
await state.pelayananPerizinanBerusaha.findById.load('edit')
await state.pelayananPerizinanBerusaha.findById.load('edit');
} catch (error) {
console.error('Gagal memuat data:', error);
} finally {
setLoading(false);
}
}
loadData()
}, [])
};
loadData();
}, []);
const data = state.pelayananPerizinanBerusaha.findById.data;
if (!data && !loading) {
return (
<Center mih={300}>
<Stack align="center" gap="sm">
<Text fz="lg" fw={500} c="dimmed">Belum ada informasi layanan yang tersedia</Text>
<Button component="a" href="https://oss.go.id" target="_blank" radius="xl">Kunjungi OSS</Button>
<Text fz="lg" fw={500} c="dimmed">
Belum ada informasi layanan yang tersedia
</Text>
<Button component="a" href="https://oss.go.id" target="_blank" radius="xl">
Kunjungi OSS
</Button>
</Stack>
</Center>
);
@@ -47,72 +65,111 @@ function PelayananPerizinanBerusaha() {
<Loader size="lg" color="blue" />
</Center>
) : (
<Stack gap="lg">
<Box>
<Title order={2} fw={700} fz={{ base: 22, md: 32 }} mb="sm">
Perizinan Berusaha Berbasis Risiko melalui OSS
</Title>
<Text fz={{ base: 'sm', md: 'md' }} c="dimmed">
Sistem Online Single Submission (OSS) untuk pendaftaran NIB
</Text>
</Box>
<Stack gap="lg">
<Box>
<Title order={2} fw={700} fz={{ base: 22, md: 32 }} mb="sm">
Perizinan Berusaha Berbasis Risiko melalui OSS
</Title>
<Text fz={{ base: 'sm', md: 'md' }} c="dimmed">
Sistem Online Single Submission (OSS) untuk pendaftaran NIB
</Text>
</Box>
<Text fz={{ base: 'sm', md: 'md' }} ta="justify" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: data?.deskripsi || '' }} />
<Text
fz={{ base: 'sm', md: 'md' }}
ta="justify"
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
dangerouslySetInnerHTML={{ __html: data?.deskripsi || '' }}
/>
<Box>
<Text fw={600} mb="sm" fz={{ base: 'sm', md: 'lg' }}>Alur pendaftaran NIB:</Text>
<Stepper active={active} onStepClick={setActive} orientation="vertical" color="blue" radius="md"
styles={{
step: { padding: '14px 0' },
stepBody: { marginLeft: 8 }
}}
>
<StepperStep label="Langkah 1" description="Daftar Akun">
<Text fz="sm">Membuat akun di portal OSS</Text>
</StepperStep>
<StepperStep label="Langkah 2" description="Isi Data Perusahaan">
<Text fz="sm">Lengkapi informasi perusahaan, data pemegang saham, dan alamat</Text>
</StepperStep>
<StepperStep label="Langkah 3" description="Pilih KBLI">
<Text fz="sm">Menentukan kode KBLI sesuai jenis usaha</Text>
</StepperStep>
<StepperStep label="Langkah 4" description="Unggah Dokumen">
<Text fz="sm">Unggah akta pendirian, surat izin, dan dokumen wajib lainnya</Text>
</StepperStep>
<StepperStep label="Langkah 5" description="Verifikasi Instansi">
<Text fz="sm">Menunggu verifikasi dan persetujuan dari pihak berwenang</Text>
</StepperStep>
<StepperStep label="Langkah 6" description="Terbit NIB">
<Text fz="sm">Menerima NIB sebagai identitas resmi usaha</Text>
</StepperStep>
<StepperCompleted>
<Center>
<Stack align="center" gap="xs">
<IconCheck size={40} color="green" />
<Text fz="sm" fw={500}>Proses pendaftaran selesai</Text>
</Stack>
</Center>
</StepperCompleted>
</Stepper>
<Box>
<Text fw={600} mb="sm" fz={{ base: 'sm', md: 'lg' }}>
Alur pendaftaran NIB:
</Text>
<Stepper
active={active}
onStepClick={(step) => {
if (step <= active) { // Only allow clicking on previous or current steps
setActive(step);
}
}}
orientation="vertical"
color="blue"
radius="md"
styles={{
step: { padding: '14px 0' },
stepBody: { marginLeft: 8 }
}}
>
<StepperStep label="Langkah 1" description="Daftar Akun">
<Text fz="sm">Membuat akun di portal OSS</Text>
</StepperStep>
<StepperStep label="Langkah 2" description="Isi Data Perusahaan">
<Text fz="sm">Lengkapi informasi perusahaan, data pemegang saham, dan alamat</Text>
</StepperStep>
<StepperStep label="Langkah 3" description="Pilih KBLI">
<Text fz="sm">Menentukan kode KBLI sesuai jenis usaha</Text>
</StepperStep>
<StepperStep label="Langkah 4" description="Unggah Dokumen">
<Text fz="sm">Unggah akta pendirian, surat izin, dan dokumen wajib lainnya</Text>
</StepperStep>
<StepperStep label="Langkah 5" description="Verifikasi Instansi">
<Text fz="sm">Menunggu verifikasi dan persetujuan dari pihak berwenang</Text>
</StepperStep>
<StepperStep label="Langkah 6" description="Terbit NIB">
<Text fz="sm">Menerima NIB sebagai identitas resmi usaha</Text>
</StepperStep>
<StepperCompleted>
<Center>
<Stack align="center" gap="xs">
<IconCheck size={40} color="green" />
<Text fz="sm" fw={500}>Proses pendaftaran selesai</Text>
</Stack>
</Center>
</StepperCompleted>
</Stepper>
{active < totalSteps && (
<Group justify="center" mt="lg">
<Button variant="light" leftSection={<IconArrowLeft size={18} />} onClick={prevStep} disabled={active === 0}>
<Button
variant="light"
leftSection={<IconArrowLeft size={18} />}
onClick={prevStep}
disabled={active === 0}
>
Kembali
</Button>
<Button rightSection={<IconArrowRight size={18} />} onClick={nextStep}>
Lanjut
</Button>
</Group>
</Box>
<Text fz="sm" ta="justify" c="dimmed" mt="md">
Catatan: Persyaratan dan prosedur dapat berubah sewaktu-waktu. Untuk informasi resmi terbaru, silakan kunjungi situs{" "}
<a href="https://oss.go.id/" target="_blank" rel="noopener noreferrer">oss.go.id</a> atau hubungi instansi pemerintah terkait.
</Text>
</Stack>
{active < totalSteps ? (
<Button
rightSection={active < totalSteps - 1 ? <IconArrowRight size={18} /> : null}
onClick={nextStep}
>
{active === totalSteps - 1 ? 'Selesai' : 'Lanjut'}
</Button>
) : (
<Button
variant="light"
onClick={() => setActive(0)}
>
Mulai Lagi
</Button>
)}
</Group>
)}
</Box>
<Text fz="sm" ta="justify" c="dimmed" mt="md">
Catatan: Persyaratan dan prosedur dapat berubah sewaktu-waktu. Untuk informasi resmi terbaru, silakan kunjungi situs{' '}
<a href="https://oss.go.id/" target="_blank" rel="noopener noreferrer">
oss.go.id
</a>{' '}
atau hubungi instansi pemerintah terkait.
</Text>
</Stack>
)}
</Box>
);
}
export default PelayananPerizinanBerusaha;
export default PelayananPerizinanBerusaha;

View File

@@ -1,167 +1,3 @@
// 'use client'
// import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
// import colors from '@/con/colors';
// import { Box, Grid, GridCol, Paper, SimpleGrid, Stack, Text, Title } from '@mantine/core';
// import { useProxy } from 'valtio/utils';
// import BackButton from '../../desa/layanan/_com/BackButto';
// import { useShallowEffect } from '@mantine/hooks';
// function Page() {
// const state = useProxy(PendapatanAsliDesa.ApbDesa);
// useShallowEffect(() => {
// state.findMany.load();
// }, []);
// useShallowEffect(() => {
// PendapatanAsliDesa.pembiayaan.findMany.load();
// PendapatanAsliDesa.belanja.findMany.load();
// PendapatanAsliDesa.pendapatan.findMany.load();
// }, []);
// // Get the latest APB data
// const latestApb = state.findMany.data?.[0];
// // Calculate totals
// const totalPendapatan = latestApb?.pendapatan?.reduce((sum, item) => sum + (item?.value || 0), 0) || 0;
// const totalBelanja = latestApb?.belanja?.reduce((sum, item) => sum + (item?.value || 0), 0) || 0;
// const totalPembiayaan = latestApb?.pembiayaan?.reduce((sum, item) => sum + (item?.value || 0), 0) || 0;
// return (
// <Stack pos="relative" bg={colors.Bg} py="xl" gap="lg">
// <Box px={{ base: 'md', md: 100 }}>
// <BackButton />
// </Box>
// <Text ta="center" fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw="bold">
// Pendapatan Asli Desa
// </Text>
// <Box px={{ base: "md", md: 100 }}>
// <Stack gap="lg" justify="center">
// <Paper bg={colors['white-1']} p="xl">
// <SimpleGrid cols={{ base: 1, md: 3 }} spacing="md">
// {/* Pendapatan Card */}
// <Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
// <Stack gap={"xs"}>
// <Title order={3}>Pendapatan</Title>
// {PendapatanAsliDesa.pendapatan.findMany.data?.map((item) => (
// <Box key={item.id}>
// <Grid>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="md" fw={500}>{item.name}</Text>
// </GridCol>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', {
// style: 'currency',
// currency: 'IDR',
// minimumFractionDigits: 0
// }).format(item.value)}</Text>
// </GridCol>
// </Grid>
// </Box>
// ))}
// <Grid>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="lg" fw={600} mb="xs">Total Pendapatan</Text>
// </GridCol>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="xl" fw={700} c={colors['blue-button']}>
// {new Intl.NumberFormat('id-ID', {
// style: 'currency',
// currency: 'IDR',
// minimumFractionDigits: 0
// }).format(totalPendapatan)}
// </Text>
// </GridCol>
// </Grid>
// </Stack>
// </Box>
// {/* Belanja Card */}
// <Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
// <Stack gap={"xs"}>
// <Title order={3}>Belanja</Title>
// {PendapatanAsliDesa.belanja.findMany.data?.map((item) => (
// <Box key={item.id}>
// <Grid>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="md" fw={500}>{item.name}</Text>
// </GridCol>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', {
// style: 'currency',
// currency: 'IDR',
// minimumFractionDigits: 0
// }).format(item.value)}</Text>
// </GridCol>
// </Grid>
// </Box>
// ))}
// <Grid>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="lg" fw={600} mb="xs">Total Belanja</Text>
// </GridCol>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="xl" fw={700} c="orange">
// {new Intl.NumberFormat('id-ID', {
// style: 'currency',
// currency: 'IDR',
// minimumFractionDigits: 0
// }).format(totalBelanja)}
// </Text>
// </GridCol>
// </Grid>
// </Stack>
// </Box>
// {/* Pembiayaan Card */}
// <Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
// <Stack gap={"xs"}>
// <Title order={3}>Pembiayaan</Title>
// {PendapatanAsliDesa.pembiayaan.findMany.data?.map((item) => (
// <Box key={item.id}>
// <Grid>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="md" fw={500}>{item.name}</Text>
// </GridCol>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', {
// style: 'currency',
// currency: 'IDR',
// minimumFractionDigits: 0
// }).format(item.value)}</Text>
// </GridCol>
// </Grid>
// </Box>
// ))}
// <Grid>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="lg" fw={600} mb="xs">Total Pembiayaan</Text>
// </GridCol>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="xl" fw={700} c="green">
// {new Intl.NumberFormat('id-ID', {
// style: 'currency',
// currency: 'IDR',
// minimumFractionDigits: 0
// }).format(totalPembiayaan)}
// </Text>
// </GridCol>
// </Grid>
// </Stack>
// </Box>
// </SimpleGrid>
// </Paper>
// </Stack>
// </Box>
// </Stack>
// );
// }
// export default Page;
'use client'
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
import colors from '@/con/colors';
@@ -206,32 +42,41 @@ function Page() {
<Stack gap="lg" justify="center">
<Paper bg={colors['white-1']} p="xl">
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="md">
{/* Pendapatan Card */}
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
{/* Pendapatan Card */}
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
<Stack gap={"xs"}>
<Title order={3}>Pendapatan</Title>
{PendapatanAsliDesa.pendapatan.findMany.data?.map((item) => (
{latestApb?.pendapatan?.map((item) => (
<Box key={item.id}>
<Grid>
<GridCol span={{ base: 12, md: 6 }}>
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text fz="md" fw={500}>{item.name}</Text>
</GridCol>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(item.value)}</Text>
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text
fz="md"
fw={500}
style={{
wordBreak: 'break-word',
whiteSpace: 'normal',
textAlign: 'right',
}}
>
{new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(item.value)}
</Text>
</GridCol>
</Grid>
</Box>
))}
<Grid>
<GridCol span={{ base: 12, md: 6 }}>
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text fz="lg" fw={600} mb="xs">Total Pendapatan</Text>
</GridCol>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="xl" fw={700} c={colors['blue-button']}>
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text style={{
wordBreak: 'break-word',
whiteSpace: 'normal'
}} fz="xl" fw={700} c={colors['blue-button']}>
{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
@@ -247,18 +92,28 @@ function Page() {
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
<Stack gap={"xs"}>
<Title order={3}>Belanja</Title>
{PendapatanAsliDesa.belanja.findMany.data?.map((item) => (
{latestApb?.belanja?.map((item) => (
<Box key={item.id}>
<Grid>
<GridCol span={{ base: 12, md: 6 }}>
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text fz="md" fw={500}>{item.name}</Text>
</GridCol>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(item.value)}</Text>
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text
fz="md"
fw={500}
style={{
wordBreak: 'break-word',
whiteSpace: 'normal',
textAlign: 'right',
}}
>
{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(item.value)}
</Text>
</GridCol>
</Grid>
</Box>
@@ -284,18 +139,28 @@ function Page() {
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
<Stack gap={"xs"}>
<Title order={3}>Pembiayaan</Title>
{PendapatanAsliDesa.pembiayaan.findMany.data?.map((item) => (
{latestApb?.pembiayaan?.map((item) => (
<Box key={item.id}>
<Grid>
<GridCol span={{ base: 12, md: 6 }}>
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text fz="md" fw={500}>{item.name}</Text>
</GridCol>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(item.value)}</Text>
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text
fz="md"
fw={500}
style={{
wordBreak: 'break-word',
whiteSpace: 'normal',
textAlign: 'right',
}}
>
{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(item.value)}
</Text>
</GridCol>
</Grid>
</Box>
@@ -366,5 +231,4 @@ function Page() {
);
}
export default Page;
export default Page;

View File

@@ -48,7 +48,7 @@ function Page() {
p={10}
mb={50}
h={400}
w={150}
w={Math.max(data.length * 120, 800)} // auto lebar sesuai jumlah data
data={data.map((item) => ({
id: item.id,
Pekerjaan: item.pekerjaan,

View File

@@ -0,0 +1,155 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, Image, Skeleton, Group, Badge, Divider } from '@mantine/core';
import { IconArrowBack, IconMapPin, IconPhone, IconStar } from '@tabler/icons-react';
import { useRouter, useParams } from 'next/navigation';
import React from 'react';
import { useProxy } from 'valtio/utils';
import { useShallowEffect } from '@mantine/hooks';
import pasarDesaState from '@/app/admin/(dashboard)/_state/ekonomi/pasar-desa/pasar-desa';
function DetailProdukPasarUser() {
const router = useRouter();
const params = useParams();
const statePasar = useProxy(pasarDesaState);
useShallowEffect(() => {
statePasar.pasarDesa.findUnique.load(params?.id as string);
}, []);
const data = statePasar.pasarDesa.findUnique.data;
if (!data) {
return (
<Stack py={10}>
<Skeleton height={400} radius="md" />
</Stack>
);
}
return (
<Box py={20}>
{/* Tombol kembali */}
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={20} color={colors['blue-button']} />}
mb={15}
>
Kembali ke daftar produk
</Button>
<Paper
w={{ base: '100%', md: '70%' }}
mx="auto"
p="lg"
radius="md"
shadow="sm"
bg={colors['white-1']}
>
<Stack gap="lg">
{/* Gambar Produk */}
{data.image?.link ? (
<Image
src={data.image.link}
alt={data.nama}
radius="md"
h={300}
w="100%"
fit="cover"
loading="lazy"
/>
) : (
<Box
h={300}
bg="gray.1"
display="flex"
style={{ alignItems: 'center', justifyContent: 'center', borderRadius: 'md' }}
>
<Text c="dimmed">Tidak ada gambar</Text>
</Box>
)}
{/* Detail Produk */}
<Stack gap="xs">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
{data.nama || 'Produk Tanpa Nama'}
</Text>
<Group>
<Badge color="green" size="lg" radius="md">
Rp {data.harga?.toLocaleString('id-ID')}
</Badge>
{data.rating && (
<Group gap={4}>
<IconStar size={18} color="#FFD43B" />
<Text fz="md" fw={500}>{data.rating}</Text>
</Group>
)}
</Group>
</Stack>
<Divider my="sm" />
{/* Info Tambahan */}
<Stack gap="sm">
<Box>
<Text fz="lg" fw={600}>Kategori</Text>
<Group gap="xs" mt={4}>
{data.KategoriToPasar && data.KategoriToPasar.length > 0 ? (
data.KategoriToPasar.map((kategori) => (
<Badge key={kategori.id} color="blue" variant="light">
{kategori.kategori.nama}
</Badge>
))
) : (
<Text fz="sm" c="dimmed">Tidak ada kategori</Text>
)}
</Group>
</Box>
{data.alamatUsaha && (
<Group gap={6}>
<IconMapPin size={18} color={colors['blue-button']} />
<Text fz="md">{data.alamatUsaha}</Text>
</Group>
)}
{data.kontak && (
<Group gap={6}>
<IconPhone size={18} color={colors['blue-button']} />
<Text fz="md">{data.kontak}</Text>
</Group>
)}
</Stack>
<Divider my="sm" />
{/* Deskripsi */}
<Box>
<Text fz="lg" fw={600}>Deskripsi Produk</Text>
<Text fz="md" c="dimmed" mt={4}>
Tidak ada deskripsi.
</Text>
</Box>
{/* Tombol Aksi User */}
{data.kontak && (
<Button
mt="md"
color="green"
size="lg"
radius="md"
component="a"
href={`https://wa.me/${data.kontak.replace(/[^0-9]/g, '')}`}
target="_blank"
>
Hubungi Penjual via WhatsApp
</Button>
)}
</Stack>
</Paper>
</Box>
);
}
export default DetailProdukPasarUser;

View File

@@ -105,7 +105,7 @@ function Page() {
return (
<Stack key={k}>
<motion.div
onClick={() => router.push(`https://wa.me/${v.kontak?.replace(/\D/g, '')}`)}
onClick={() => router.push(`/darmasaba/ekonomi/pasar-desa/${v.id}`)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.8 }}
>
@@ -117,7 +117,7 @@ function Page() {
h={200}
w='100%'
style={{ objectFit: 'cover' }}
loading="lazy"
loading="lazy"
/>
<Text py={10} fw={'bold'} fz={'lg'}>{v.nama}</Text>
<Text fz={'md'}>Rp {v.harga.toLocaleString('id-ID')}</Text>

View File

@@ -1,14 +1,14 @@
'use client'
import { IconKey, IconMapper } from '@/app/admin/(dashboard)/_com/iconMap';
import programKreatifState from '@/app/admin/(dashboard)/_state/inovasi/program-kreatif';
import colors from '@/con/colors';
import { Box, Button, Center, Group, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
import { Box, Button, Center, Grid, GridCol, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconSearch } from '@tabler/icons-react';
import { useTransitionRouter } from 'next-view-transitions';
import React, { useState } from 'react';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto';
import { IconSearch } from '@tabler/icons-react';
import { IconKey, IconMapper } from '@/app/admin/(dashboard)/_com/iconMap';
// const data = [
// {
@@ -75,17 +75,23 @@ function Page() {
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }} >
<Group justify="space-between" mb="md" align='center'>
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
Program Kreatif Desa
</Text>
<TextInput
placeholder="Cari program kreatif..."
leftSection={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
</Group>
<Grid align='center'>
<GridCol span={{ base: 12, md: 9 }}>
<Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
Program Kreatif Desa
</Text>
</GridCol>
<GridCol span={{ base: 12, md: 3 }}>
<TextInput
radius={"lg"}
placeholder='Cari Program Kreatif'
value={search}
onChange={(e) => setSearch(e.target.value)}
leftSection={<IconSearch size={20} />}
w={{ base: "50%", md: "100%" }}
/>
</GridCol>
</Grid>
</Box>
<Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'} justify='center'>

View File

@@ -1,15 +1,15 @@
'use client'
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import laporanPublikState from '@/app/admin/(dashboard)/_state/keamanan/laporan-publik';
import colors from '@/con/colors';
import { Box, Button, Center, ColorSwatch, Flex, Group, Modal, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } 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 { useDebouncedValue, useDisclosure, useShallowEffect } from '@mantine/hooks';
import { IconArrowRight, IconPlus, IconSearch } from '@tabler/icons-react';
import { useTransitionRouter } from 'next-view-transitions';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto';
import { useTransitionRouter } from 'next-view-transitions';
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
function Page() {
const [search, setSearch] = useState("");
@@ -53,7 +53,7 @@ function Page() {
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}>
<Flex justify="space-between" align="center">
<Group justify="space-between" align="center">
<BackButton />
<TextInput
radius={"lg"}
@@ -61,9 +61,9 @@ function Page() {
value={search}
onChange={(e) => setSearch(e.target.value)}
leftSection={<IconSearch size={20} />}
w={{ base: "50%", md: "100%" }}
w={{ base: "100%", md: "30%" }}
/>
</Flex>
</Group>
</Box>
<Box px={{ base: 'md', md: 100 }}>
<Group justify="space-between">
@@ -118,7 +118,7 @@ function Page() {
return (
<Paper radius={'lg'} key={k} bg={colors['white-trans-1']} p={'xl'}>
<Stack>
<Title c={colors['blue-button']} order={1}>{v.judul}</Title>
<Text c={colors['blue-button']} lineClamp={3} truncate="end" fz="h4" fw="bold">{v.judul}</Text>
<Text fs={'italic'} fz={'xl'}>
{v.tanggalWaktu
? new Date(v.tanggalWaktu).toLocaleString('id-ID')

View File

@@ -84,9 +84,8 @@ function Page() {
</Box>
<Box>
<Text fz="h4" fw="bold">Kenali Gejala DBD</Text>
<Text fz="h4" fw="bold">{state.findUnique.data.symptom?.title}</Text>
<Divider my="xs" />
<Text fz="md" fw="semibold">{state.findUnique.data.symptom?.title}</Text>
<Text fz="md" lh={1.6} ta="justify" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: state.findUnique.data.symptom?.content }} />
</Box>

View File

@@ -3,9 +3,9 @@
import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
import colors from '@/con/colors';
import { ActionIcon, Anchor, AspectRatio, Badge, Box, Button, Card, Chip, CopyButton, Divider, Grid, Group, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, ThemeIcon, Title, Tooltip } from '@mantine/core';
import { ActionIcon, AspectRatio, Badge, Box, Button, Card, CopyButton, Divider, Grid, Group, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, ThemeIcon, Title, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconBrandWhatsapp, IconCheck, IconCopy, IconDeviceLandlinePhone, IconHeart, IconInfoCircle, IconMail, IconMapPin, IconMoodEmpty, IconSearch, IconStethoscope, IconUser, IconUsersGroup, IconWallet } from '@tabler/icons-react';
import { IconBrandWhatsapp, IconCheck, IconCopy, IconDeviceLandlinePhone, IconHeart, IconInfoCircle, IconMail, IconMapPin, IconMoodEmpty, IconSearch, IconUser } from '@tabler/icons-react';
import { useParams } from 'next/navigation';
import { useMemo } from 'react';
import { useProxy } from 'valtio/utils';
@@ -149,11 +149,6 @@ function Page() {
</CopyButton>
</Group>
</Group>
<Group gap="xs" mt="sm" wrap="wrap">
<Chip defaultChecked radius="xl" variant="light" icon={<IconStethoscope size={16} />}>Layanan Medis</Chip>
<Chip radius="xl" variant="light" icon={<IconUsersGroup size={16} />}>Ramah Keluarga</Chip>
<Chip radius="xl" variant="light" icon={<IconWallet size={16} />}>Pembayaran Non-Tunai</Chip>
</Group>
</Stack>
</Card>
</Box>
@@ -210,7 +205,6 @@ function Page() {
<Button variant="light" leftSection={<IconBrandWhatsapp size={18} />} component="a" href={`https://wa.me/${kontak.whatsapp.replace(/\D/g, '')}`} target="_blank" aria-label="Hubungi WhatsApp">WhatsApp</Button>
<Button variant="light" leftSection={<IconMail size={18} />} component="a" href={`mailto:${kontak.email}`} aria-label="Kirim Email">Email</Button>
</Group>
<Anchor target="_blank" underline="hover">Kunjungi situs resmi</Anchor>
</Stack>
</Card>
@@ -246,15 +240,8 @@ function Page() {
</Table>
</Stack>
</Card>
</Stack>
</Grid.Col>
</Grid>
</Box>
<Box px={{ base: 'md', md: 100 }}>
<Grid gutter="lg">
<Grid.Col span={{ base: 12, md: 8 }}>
<Card radius="xl" p="lg" withBorder>
<Card radius="xl" p="lg" withBorder>
<Stack gap="md">
<Title order={3}>Fasilitas Pendukung</Title>
<Divider />
@@ -270,8 +257,7 @@ function Page() {
)}
</Stack>
</Card>
</Grid.Col>
<Grid.Col span={{ base: 12, md: 4 }}>
<Card radius="xl" p="lg" withBorder>
<Stack gap="md">
<Title order={3}>Layanan & Tarif</Title>
@@ -309,10 +295,11 @@ function Page() {
)}
</Stack>
</Card>
</Stack>
</Grid.Col>
</Grid>
</Box>
<Box px={{ base: 'md', md: 100 }} pb="xl">
<Paper radius="xl" p="lg" withBorder>
<Stack gap="md">

View File

@@ -4,14 +4,16 @@ import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
import colors from '@/con/colors';
import {
Box,
Button,
Divider,
Group,
Modal,
Paper,
Skeleton,
Stack,
Text
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useDisclosure, useShallowEffect } from '@mantine/hooks';
import { IconMail, IconPhone, IconUser } from '@tabler/icons-react';
import { useParams } from 'next/navigation';
import { useProxy } from 'valtio/utils';
@@ -21,6 +23,7 @@ import CreatePendaftaran from '../create/page';
function Page() {
const params = useParams();
const state = useProxy(jadwalkegiatanState);
const [opened, { open, close }] = useDisclosure(false);
useShallowEffect(() => {
state.findUnique.load(params?.id as string);
@@ -66,28 +69,38 @@ function Page() {
<Stack gap="sm">
<Text fz="lg" fw="bold">Deskripsi Kegiatan</Text>
<Divider />
<Text ta="justify" fz="md" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: state.findUnique.data.deskripsijadwalkegiatan.deskripsi }} />
<Text ta="justify" fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.deskripsijadwalkegiatan.deskripsi }} />
</Stack>
<Stack gap="sm">
<Text fz="lg" fw="bold">Layanan yang Tersedia</Text>
<Divider />
<Text ta="justify" fz="md" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: state.findUnique.data.layananjadwalkegiatan.content }} />
<Text ta="justify" fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.layananjadwalkegiatan.content }} />
</Stack>
<Stack gap="sm">
<Text fz="lg" fw="bold">Syarat & Ketentuan</Text>
<Divider />
<Text ta="justify" fz="md" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: state.findUnique.data.syaratketentuanjadwalkegiatan.content }} />
<Text ta="justify" fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.syaratketentuanjadwalkegiatan.content }} />
</Stack>
<Stack gap="sm">
<Text fz="lg" fw="bold">Dokumen yang Perlu Dibawa</Text>
<Divider />
<Text ta="justify" fz="md" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: state.findUnique.data.dokumenjadwalkegiatan.content }} />
<Text ta="justify" fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.dokumenjadwalkegiatan.content }} />
</Stack>
<CreatePendaftaran />
<Stack gap="sm">
<Text fz="lg" fw="bold">Pendaftaran Kegiatan</Text>
<Divider />
<Group>
<Button onClick={open}>Buat Pendaftaran</Button>
</Group>
</Stack>
<Modal opened={opened} onClose={close}>
<CreatePendaftaran />
</Modal>
<Paper p="lg" radius="md" bg={colors['blue-button-trans']} shadow="sm">
<Stack gap="xs">

View File

@@ -121,12 +121,15 @@ function Page() {
</Badge>
</Group>
<Text fz="sm" c="dimmed">
Diposting: {v.createdAt.toLocaleDateString()}
Diposting: {new Date(v.createdAt).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric',
})}
</Text>
<Divider />
<Text fz="sm" lh={1.5} lineClamp={3} truncate="end">
{v.deskripsiSingkat}
</Text>
<Text fz="sm" lh={1.5} lineClamp={3} truncate="end" dangerouslySetInnerHTML={{ __html: v.deskripsiSingkat }} />
<Button variant="light" radius="md" size="md" onClick={() => router.push(`/darmasaba/kesehatan/info-wabah-penyakit/${v.id}`)}>
Selengkapnya
</Button>

View File

@@ -108,20 +108,23 @@ function Page() {
<Box
style={{
width: '100%',
aspectRatio: '16/9',
borderRadius: '12px',
height: 180, // 🔥 tinggi fix biar semua seragam
borderRadius: 12,
overflow: 'hidden',
position: 'relative',
backgroundColor: '#f0f2f5', // fallback kalau gambar loading
}}
>
<Image
src={v.image.link}
src={v.image?.link || '/img/default.png'}
alt={v.name}
fit="cover"
width="100%"
height="100%"
loading="lazy"
style={{
width: '100%',
height: '100%',
objectFit: 'cover',
objectPosition: 'center',
transition: 'transform 0.4s ease',
}}
onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.05)')}
@@ -129,6 +132,7 @@ function Page() {
/>
</Box>
</Center>
<Stack gap={4} w="100%">
<Text

View File

@@ -0,0 +1,120 @@
'use client';
import colors from '@/con/colors';
import { Button, Center, Flex, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconCalendar, IconInfoCircle, IconPhone } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import posyanduState from '@/app/admin/(dashboard)/_state/kesehatan/posyandu/posyandu';
export default function DetailPosyanduUser() {
const statePosyandu = useProxy(posyanduState);
const params = useParams();
const router = useRouter();
useShallowEffect(() => {
statePosyandu.findUnique.load(params?.id as string);
}, []);
if (!statePosyandu.findUnique.data) {
return (
<Stack py="xl" px={{ base: 'md', md: 100 }}>
<Skeleton height={500} radius="md" />
</Stack>
);
}
const data = statePosyandu.findUnique.data;
return (
<Stack pos="relative" bg={colors.Bg} py="xl" px={{ base: 'md', md: 100 }} gap="xl">
{/* Tombol Kembali */}
<Group>
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={22} color={colors['blue-button']} />}
mb="sm"
c={colors['blue-button']}
>
Kembali
</Button>
</Group>
<Paper
withBorder
p="xl"
radius="lg"
shadow="md"
bg={colors['white-trans-1']}
maw={800}
mx="auto"
>
<Stack gap="md">
{/* Header */}
<Text
ta="center"
fz={{ base: '1.8rem', md: '2.2rem' }}
fw={700}
c={colors['blue-button']}
>
{data.name || 'Posyandu Desa'}
</Text>
{/* Gambar */}
{data.image?.link ? (
<Center>
<Image
src={data.image.link}
alt={`Gambar ${data.name}`}
w="100%"
h={300}
radius="md"
fit="cover"
loading="lazy"
/>
</Center>
) : (
<Center>
<Text fz="sm" c="dimmed">
Tidak ada gambar
</Text>
</Center>
)}
{/* Info utama */}
<Stack gap="sm" mt="md">
<Flex align="flex-start" gap="xs">
<IconPhone size={18} stroke={1.5} style={{ marginTop: 3 }} />
<Text fz="sm" c="dimmed">
{data.nomor || 'Nomor tidak tersedia'}
</Text>
</Flex>
<Flex align="flex-start" gap="xs">
<IconCalendar size={18} stroke={1.5} style={{ marginTop: 3 }} />
<Text
fz="sm"
c="dimmed"
dangerouslySetInnerHTML={{ __html: data.jadwalPelayanan || '-' }}
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
/>
</Flex>
<Flex align="flex-start" gap="xs">
<IconInfoCircle size={18} stroke={1.5} style={{ marginTop: 3 }} />
<Text
fz="sm"
c="dimmed"
lh={1.6}
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
/>
</Flex>
</Stack>
</Stack>
</Paper>
</Stack>
);
}

View File

@@ -1,18 +1,19 @@
'use client'
import posyandustate from "@/app/admin/(dashboard)/_state/kesehatan/posyandu/posyandu";
import colors from "@/con/colors";
import { Badge, Box, Center, Flex, Group, Image, List, ListItem, Pagination, Paper, SimpleGrid, Skeleton, Spoiler, Stack, Text, TextInput } from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks";
import { Badge, Box, Button, Center, Flex, Group, Image, List, ListItem, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from "@mantine/core";
import { useDebouncedValue, useShallowEffect } from "@mantine/hooks";
import { IconCalendar, IconInfoCircle, IconPhone, IconSearch } from "@tabler/icons-react";
import { useState } from "react";
import { useProxy } from "valtio/utils";
import BackButton from "../../desa/layanan/_com/BackButto";
import { useDebouncedValue } from "@mantine/hooks";
import { useTransitionRouter } from "next-view-transitions";
export default function Page() {
const state = useProxy(posyandustate);
const [search, setSearch] = useState("");
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const router = useTransitionRouter()
const { data, page, totalPages, loading, load } = state.findMany;
@@ -133,33 +134,41 @@ export default function Page() {
loading="lazy"
/>
</Center>
<Flex align="center" gap="xs">
<IconPhone size={18} stroke={1.5} />
<Text fz="sm" c="dimmed">
{v.nomor || "Tidak tersedia"}
</Text>
<Flex align="flex-start" gap="xs">
<IconPhone size={18} stroke={1.5} style={{ marginTop: 3 }} />
<Box>
<Text fz="sm" c="dimmed" lh={1.4}>
{v.nomor || "Tidak tersedia"}
</Text>
</Box>
</Flex>
<Flex align="center" gap="xs">
<IconCalendar size={18} stroke={1.5} />
<Text fz="sm" c="dimmed">
Jadwal:{" "}
<span style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: v.jadwalPelayanan }} />
</Text>
<Flex align="flex-start" gap="xs">
<IconCalendar size={18} stroke={1.5} style={{ marginTop: 3 }} />
<Box>
<Text fz="sm" c="dimmed" lh={1.4}>
<strong>Jadwal:</strong>{" "}
<span
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
dangerouslySetInnerHTML={{ __html: v.jadwalPelayanan }}
/>
</Text>
</Box>
</Flex>
<Spoiler
key={`spoiler-${v.id}`}
maxHeight={70}
showLabel="Lihat selengkapnya"
hideLabel="Sembunyikan"
transitionDuration={300}
>
<Flex align="flex-start" gap="xs">
<IconInfoCircle size={18} stroke={1.5} style={{ marginTop: 3 }} />
<Text
fz="sm"
lh={1.5}
c="dimmed"
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
lineClamp={3}
truncate="end"
/>
</Spoiler>
</Flex>
<Button radius="lg" size="md" variant="outline" onClick={() => router.push(`/darmasaba/kesehatan/posyandu/${v.id}`)}>Detail</Button>
</Stack>
</Paper>
))}

View File

@@ -126,17 +126,36 @@ export default function Page() {
className="hover-scale"
>
<Stack gap="md">
<Box h={180} w="100%">
<Image
src={v.image?.link}
alt={v.name}
radius="xl"
w="100%"
h="100%"
fit="cover"
loading="lazy"
/>
</Box>
<Center>
<Box
style={{
width: '100%',
height: 180, // 🔥 tinggi fix biar semua seragam
borderRadius: 12,
overflow: 'hidden',
position: 'relative',
backgroundColor: '#f0f2f5', // fallback kalau gambar loading
}}
>
<Image
src={v.image?.link || '/img/default.png'}
alt={v.name}
fit="cover"
width="100%"
height="100%"
loading="lazy"
style={{
objectFit: 'cover',
objectPosition: 'center',
transition: 'transform 0.4s ease',
}}
onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.05)')}
onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')}
/>
</Box>
</Center>
<Box px="lg" pb="lg">
<Text

View File

@@ -49,12 +49,10 @@ function Page() {
<Paper p={20} bg={colors['white-trans-1']} shadow="md" radius="md" style={{ width: '100%', display: 'flex', flexDirection: 'column' }}>
<Stack gap="md">
<Box>
<Tooltip label={tujuan.data?.judul} position="top" withArrow>
<Tooltip label={<Text fz={"sm"} c={"white"} dangerouslySetInnerHTML={{ __html: tujuan.data?.judul || '' }} /> } position="top" withArrow>
<Stack gap={4} align="center">
<IconLeaf size={28} color={colors['blue-button']} />
<Text fz="h3" fw="bold" c={colors['blue-button']} ta="center">
{tujuan.data?.judul}
</Text>
<Text dangerouslySetInnerHTML={{ __html: tujuan.data?.judul || '' }} fz="h3" fw="bold" c={colors['blue-button']} ta="center" />
</Stack>
</Tooltip>
</Box>

View File

@@ -43,15 +43,48 @@ export default function Page() {
const loadingGrid = state.findMany.loading;
const loadingFeatured = featured.loading;
// Load featured data once on component mount
useEffect(() => {
if (!featured.data && !loadingFeatured) {
gotongRoyongState.kegiatanDesa.findFirst.load();
let mounted = true;
const loadFeatured = async () => {
try {
if (!featured.data && !loadingFeatured) {
await gotongRoyongState.kegiatanDesa.findFirst.load();
}
} catch (error) {
console.error('Error loading featured data:', error);
}
};
if (mounted) {
loadFeatured();
}
}, [featured.data, loadingFeatured]);
return () => {
mounted = false;
};
}, []); // Empty dependency array to run only once on mount
useEffect(() => {
const limit = 3;
state.findMany.load(page, limit, search);
let mounted = true;
const loadData = async () => {
try {
const limit = 3;
await state.findMany.load(page, limit, search);
} catch (error) {
console.error('Error loading data:', error);
}
};
if (mounted) {
loadData();
}
return () => {
mounted = false;
};
}, [page, search]);
const handlePageChange = (newPage: number) => {
@@ -59,7 +92,9 @@ export default function Page() {
if (search) url.set('search', search);
if (newPage > 1) url.set('page', newPage.toString());
else url.delete('page');
router.replace(`?${url.toString()}`);
// Use push instead of replace to keep browser history
router.push(`?${url.toString()}`, { scroll: false });
};
const featuredData = featured.data;

View File

@@ -1,9 +1,9 @@
'use client'
import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah';
import colors from '@/con/colors';
import { Box, Center, Flex, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
import { Box, Center, Flex, Group, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { Icon, IconChartLine, IconClipboardTextFilled, IconLeaf, IconRecycle, IconScale, IconSearch, IconTent, IconTrashFilled, IconTrophy, IconTruckFilled } from '@tabler/icons-react';
import { Icon, IconChartLine, IconClipboardTextFilled, IconLeaf, IconRecycle, IconRoute, IconScale, IconSearch, IconTent, IconTrashFilled, IconTrophy, IconTruckFilled } from '@tabler/icons-react';
import React, { useState } from 'react';
import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto';
@@ -122,20 +122,28 @@ function Page() {
<Stack gap="md">
{data2?.map((v, k) => (
<Paper key={k} p="md" withBorder radius="md">
<Text fw="bold" fz="lg">{v.namaTempatMaps}</Text>
<Text c="dimmed" fz="sm" mb="sm">{v.alamat}</Text>
{v.lat && v.lng ? (
<a
href={`https://www.google.com/maps/dir/?api=1&destination=${v.lat},${v.lng}`}
target="_blank"
rel="noopener noreferrer"
style={{ color: colors['blue-button'], textDecoration: 'none' }}
>
<Text fz="sm">📌 Buka di Google Maps</Text>
</a>
) : (
<Text c="dimmed" fz="sm">Koordinat belum tersedia</Text>
)}
<Group justify='space-between'>
<Box>
<Text fw="bold" fz="lg">{v.namaTempatMaps}</Text>
<Text c="dimmed" fz="sm" mb="sm">{v.alamat}</Text>
</Box>
<Box>
<IconRoute color={colors['blue-button']} size={30} />
<Text fw={"bold"} fz="sm" c={colors['blue-button']}>Rute</Text>
</Box>
</Group>
{v.lat && v.lng ? (
<a
href={`https://www.google.com/maps/dir/?api=1&destination=${v.lat},${v.lng}`}
target="_blank"
rel="noopener noreferrer"
style={{ color: colors['blue-button'], textDecoration: 'none' }}
>
<Text fz="sm">📌 Lihat Peta Lebih Besar</Text>
</a>
) : (
<Text c="dimmed" fz="sm">Koordinat belum tersedia</Text>
)}
</Paper>
))}
</Stack>

View File

@@ -74,7 +74,7 @@ function Page() {
<Title fz={55} fw={900} c={colors['blue-button']}>
Wujudkan Mimpi Pendidikanmu di Desa Darmasaba
</Title>
<Text fz="lg" mt="md" c="dimmed">
<Text fz="lg" mt="md" fw={"bold"}>
Program beasiswa untuk mendukung pendidikan berkualitas bagi generasi muda Desa Darmasaba.
</Text>
<Group mt="xl">

View File

@@ -15,7 +15,7 @@ import {
Timeline,
Title
} from '@mantine/core';
import { IconArrowLeft } from '@tabler/icons-react';
import { IconArrowLeft, IconChecklist, IconInfoCircle, IconQuote, IconSchool, IconTimeline, IconUserPlus } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useDisclosure } from '@mantine/hooks';
import { useProxy } from 'valtio/utils';
@@ -69,10 +69,11 @@ export default function BeasiswaPage() {
{/* Hero Section */}
<Container size="lg" py="xl">
<Stack gap="md" maw={600}>
<Title order={2} c="blue.9">
Program Beasiswa Pendidikan Desa Darmasaba
</Title>
<Text c="dimmed">
<Group>
<IconSchool size={30} color={colors["blue-button"]} />
<Title order={2}>Program Beasiswa Pendidikan Desa Darmasaba</Title>
</Group>
<Text>
Program ini bertujuan untuk mendukung pendidikan generasi muda di Desa Darmasaba
agar dapat melanjutkan studi ke jenjang lebih tinggi dengan dukungan finansial dan pendampingan.
</Text>
@@ -81,21 +82,35 @@ export default function BeasiswaPage() {
{/* Tentang Program */}
<Container size="lg" py="xl">
<Title order={3} mb="sm">
Tentang Program
</Title>
<Group mb="sm">
<IconInfoCircle size={24} color={colors["blue-button"]} />
<Title order={3}>Tentang Program</Title>
</Group>
<Text>
Program Beasiswa Desa Darmasaba adalah inisiatif pemerintah desa untuk meningkatkan akses
pendidikan bagi siswa berprestasi dan kurang mampu. Melalui program ini, desa memberikan bantuan
biaya sekolah, bimbingan akademik, serta pelatihan soft skill bagi peserta terpilih.
</Text>
{/* Tambahkan info tahun berjalan di sini */}
<Paper mt="md" p="md" radius="lg" shadow="xs" bg="#f8fbff" withBorder>
<Text fw={500} c={colors["blue-button"]}>
📅 Periode Beasiswa Tahun 2025
</Text>
<Text fz="sm" c="dimmed">
Pendaftaran beasiswa dibuka mulai <strong>1 Januari 2025</strong> dan ditutup pada
<strong>31 Mei 2025</strong>.
Pengumuman hasil seleksi akan diumumkan pada pertengahan Juni 2025 melalui website resmi Desa Darmasaba.
</Text>
</Paper>
</Container>
{/* Syarat dan Ketentuan */}
<Container size="lg" py="xl">
<Title order={3} mb="sm">
Syarat Pendaftaran
</Title>
<Group mb="sm">
<IconChecklist size={24} color={colors["blue-button"]} />
<Title order={3}>Syarat Pendaftaran</Title>
</Group>
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="lg">
<Paper shadow="sm" p="md" radius="lg" withBorder>
@@ -123,42 +138,61 @@ export default function BeasiswaPage() {
{/* Proses Seleksi */}
<Container size="lg" py="xl">
<Title order={3} mb="sm">
Proses Seleksi
</Title>
<Group mb="sm">
<IconTimeline size={24} color={colors["blue-button"]} />
<Title order={3}>Proses Seleksi</Title>
</Group>
<Timeline active={4} bulletSize={24} lineWidth={2}>
<Timeline.Item title="Pendaftaran Online">
<Text c="dimmed" size="sm">
Calon peserta mengisi formulir pendaftaran dan mengunggah dokumen pendukung.
</Text>
<Text size="sm" fw={500} c={colors["blue-button"]} mt={4}>
Estimasi waktu: 1 Februari 31 Mei 2025
</Text>
</Timeline.Item>
<Timeline.Item title="Seleksi Administrasi">
<Text c="dimmed" size="sm">
Panitia memverifikasi kelengkapan dan validitas berkas.
</Text>
<Text size="sm" fw={500} c={colors["blue-button"]} mt={4}>
Estimasi waktu: 57 hari kerja setelah penutupan pendaftaran
</Text>
</Timeline.Item>
<Timeline.Item title="Wawancara dan Penilaian">
<Text c="dimmed" size="sm">
Peserta yang lolos administrasi akan diundang untuk wawancara langsung dengan tim seleksi.
</Text>
<Text size="sm" fw={500} c={colors["blue-button"]} mt={4}>
Estimasi waktu: 710 hari kerja setelah pengumuman seleksi administrasi
</Text>
</Timeline.Item>
<Timeline.Item title="Pengumuman Penerima">
<Text c="dimmed" size="sm">
Daftar penerima beasiswa diumumkan melalui website resmi Desa Darmasaba.
</Text>
<Text size="sm" fw={500} c={colors["blue-button"]} mt={4}>
Estimasi waktu: 5 hari kerja setelah tahap wawancara selesai
</Text>
</Timeline.Item>
</Timeline>
<Text c="dimmed" size="sm" mt="lg" ta="center">
🗓 Total estimasi keseluruhan proses: sekitar 34 minggu setelah penutupan pendaftaran
</Text>
</Container>
{/* Testimoni */}
<Container size="lg" py="xl">
<Title order={3} mb="sm">
Cerita Sukses Penerima Beasiswa
</Title>
<Group mb="sm">
<IconQuote size={24} color={colors["blue-button"]} />
<Title order={3}>Cerita Sukses Penerima Beasiswa</Title>
</Group>
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="lg">
<Paper shadow="md" p="lg" radius="lg">
@@ -183,7 +217,10 @@ export default function BeasiswaPage() {
{/* CTA Akhir */}
<Container size="lg" py="xl" ta="center">
<Title order={3}>Siap Bergabung dengan Program Ini?</Title>
<Group justify="center" mb="sm">
<IconUserPlus size={28} color={colors["blue-button"]} />
<Title order={3}>Siap Bergabung dengan Program Ini?</Title>
</Group>
<Text c="dimmed" mb="md">
Segera daftar dan wujudkan mimpimu bersama Desa Darmasaba.
</Text>

View File

@@ -92,7 +92,7 @@ function Page() {
cursor={{ fill: 'var(--mantine-color-gray-1)' }}
/>
<Legend />
<Bar dataKey="jumlah" fill={colors['blue-button']} name="Jumlah Pendidikan" radius={[8, 8, 0, 0]} />
<Bar dataKey="jumlah" fill={colors['blue-button']} name="Jumlah Penduduk dengan Pendidikan" radius={[8, 8, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</Paper>

View File

@@ -84,7 +84,7 @@ function Page({ params }: PageProps) {
<TableTr>
<TableTh w="30%">Nama Pengajar</TableTh>
<TableTh w="30%">Nama Lembaga</TableTh>
<TableTh w="40%">Jenjang Pendidikan</TableTh>
<TableTh w="40%">Mengajar Di Jenjang Pendidikan</TableTh>
</TableTr>
</TableThead>
<TableTbody>
@@ -95,7 +95,7 @@ function Page({ params }: PageProps) {
<TableTd>{item.lembaga.jenjangPendidikan?.nama || '-'}</TableTd>
</TableTr>
))}
</TableTbody>
</TableTbody>
</Table>
)}
</Paper>

View File

@@ -1,7 +1,7 @@
'use client'
import pendidikanNonFormalState from '@/app/admin/(dashboard)/_state/pendidikan/pendidikan-non-formal';
import colors from '@/con/colors';
import { Box, Paper, SimpleGrid, Skeleton, Stack, Text, Title, Tooltip } from '@mantine/core';
import { Box, Group, Paper, SimpleGrid, Skeleton, Stack, Text, Title, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useProxy } from 'valtio/utils';
import { IconMapPin, IconTarget, IconBook2 } from '@tabler/icons-react';
@@ -43,7 +43,7 @@ function Page() {
Pendidikan Non Formal
</Title>
<Text ta="center" fz="lg" lh={1.6} c="black" maw={800} mx="auto">
Bentuk pendidikan di luar sekolah yang terstruktur, bertujuan memberikan keterampilan, pengetahuan, dan pengembangan karakter masyarakat dari berbagai usia serta latar belakang.
Pendidikan non formal merupakan bentuk pendidikan di luar sekolah yang terstruktur, bertujuan untuk memberikan keterampilan, pengetahuan, serta pengembangan karakter masyarakat dari berbagai usia dan latar belakang.
</Text>
</Box>
<SimpleGrid
@@ -59,13 +59,17 @@ function Page() {
withBorder
>
<Stack>
<Tooltip label="Fokus utama program" withArrow>
<Title order={2} fw="bold" c={colors['blue-button']} mb="xs" flex="center">
<IconTarget size={28} style={{ marginRight: 8 }} />
<Group align="center" gap={8} wrap="nowrap">
<Tooltip label="Fokus utama program" withArrow>
<Box display="flex" style={{ alignItems: "center" }}>
<IconTarget color={colors['blue-button']} size={26} />
</Box>
</Tooltip>
<Text fw={700} fz="xl" c={colors['blue-button']}>
{stateTujuanPendidikanNonFormal.findById.data?.judul}
</Title>
</Tooltip>
<Text fz="md" lh={1.7} c="dark" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: stateTujuanPendidikanNonFormal.findById.data?.deskripsi }} />
</Text>
</Group>
<Text fz="md" lh={1.7} c="dark" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: stateTujuanPendidikanNonFormal.findById.data?.deskripsi }} />
</Stack>
</Paper>
<Paper
@@ -76,13 +80,17 @@ function Page() {
withBorder
>
<Stack>
<Tooltip label="Lokasi pelaksanaan kegiatan" withArrow>
<Title order={2} fw="bold" c={colors['blue-button']} mb="xs" flex="center">
<IconMapPin size={28} style={{ marginRight: 8 }} />
<Group align="center" gap={8} wrap="nowrap">
<Tooltip label="Lokasi pelaksanaan kegiatan" withArrow>
<Box display="flex" style={{ alignItems: "center" }}>
<IconMapPin color={colors['blue-button']} size={26} />
</Box>
</Tooltip>
<Text fw={700} fz="xl" c={colors['blue-button']}>
{stateTempatKegiatan.findById.data?.judul}
</Title>
</Tooltip>
<Text fz="md" style={{wordBreak: "break-word", whiteSpace: "normal"}} lh={1.7} c="dark" dangerouslySetInnerHTML={{ __html: stateTempatKegiatan.findById.data?.deskripsi }} />
</Text>
</Group>
<Text fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal" }} lh={1.7} c="dark" dangerouslySetInnerHTML={{ __html: stateTempatKegiatan.findById.data?.deskripsi }} />
</Stack>
</Paper>
</SimpleGrid>
@@ -95,13 +103,17 @@ function Page() {
withBorder
>
<Stack>
<Tooltip label="Ragam jenis program yang tersedia" withArrow>
<Title order={2} fw="bold" c={colors['blue-button']} mb="xs" flex="center">
<IconBook2 size={28} style={{ marginRight: 8 }} />
<Group align="center" gap={8} wrap="nowrap">
<Tooltip label="Ragam jenis program yang tersedia" withArrow>
<Box display="flex" style={{ alignItems: "center" }}>
<IconBook2 color={colors['blue-button']} size={26} />
</Box>
</Tooltip>
<Text fw={700} fz="xl" c={colors['blue-button']}>
{stateJenisProgram.findById.data?.judul}
</Title>
</Tooltip>
<Text fz="md" lh={1.7} c="dark" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: stateJenisProgram.findById.data?.deskripsi }} />
</Text>
</Group>
<Text fz="md" lh={1.7} c="dark" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: stateJenisProgram.findById.data?.deskripsi }} />
</Stack>
</Paper>
</Box>

View File

@@ -16,16 +16,11 @@ import {
TextInput,
} from '@mantine/core';
import { DateInput } from '@mantine/dates';
import {
IconArrowRight,
IconBook2,
IconUser
} from '@tabler/icons-react';
import { IconArrowRight, IconBook2, IconUser } from '@tabler/icons-react';
import { useEffect } from 'react';
import { toast } from 'react-toastify';
import { useSnapshot } from 'valtio';
export interface ModalPeminjamanProps {
opened: boolean;
onClose: () => void;
@@ -45,11 +40,12 @@ export default function ModalPeminjaman({
}: ModalPeminjamanProps) {
const snap = useSnapshot(perpustakaanDigitalState.peminjamanBuku);
// reset form setiap modal dibuka
const BATAS_HARI_PINJAM = 4;
// Reset form setiap modal dibuka
useEffect(() => {
if (opened && buku) {
perpustakaanDigitalState.peminjamanBuku.create.form = {
...perpustakaanDigitalState.peminjamanBuku.create.form,
bukuId: buku.id,
nama: '',
noTelp: '',
@@ -99,7 +95,14 @@ export default function ModalPeminjaman({
</Badge>
)}
<Text fz="sm" c="dimmed" lineClamp={3} dangerouslySetInnerHTML={{ __html: buku.deskripsi || 'Tidak ada deskripsi' }} />
<Text
fz="sm"
c="dimmed"
lineClamp={3}
dangerouslySetInnerHTML={{
__html: buku.deskripsi || 'Tidak ada deskripsi',
}}
/>
</Stack>
</Group>
@@ -112,7 +115,8 @@ export default function ModalPeminjaman({
leftSection={<IconUser size={16} />}
value={snap.create.form.nama}
onChange={(e) =>
(perpustakaanDigitalState.peminjamanBuku.create.form.nama = e.currentTarget.value)
(perpustakaanDigitalState.peminjamanBuku.create.form.nama =
e.currentTarget.value)
}
required
/>
@@ -123,7 +127,8 @@ export default function ModalPeminjaman({
leftSection={<IconUser size={16} />}
value={snap.create.form.noTelp}
onChange={(e) =>
(perpustakaanDigitalState.peminjamanBuku.create.form.noTelp = e.currentTarget.value)
(perpustakaanDigitalState.peminjamanBuku.create.form.noTelp =
e.currentTarget.value)
}
required
/>
@@ -134,11 +139,13 @@ export default function ModalPeminjaman({
leftSection={<IconUser size={16} />}
value={snap.create.form.alamat}
onChange={(e) =>
(perpustakaanDigitalState.peminjamanBuku.create.form.alamat = e.currentTarget.value)
(perpustakaanDigitalState.peminjamanBuku.create.form.alamat =
e.currentTarget.value)
}
required
/>
{/* === OTOMATIS SET BATAS DAN TANGGAL KEMBALI === */}
<DateInput
label="Tanggal Pinjam"
placeholder="Pilih tanggal pinjam"
@@ -148,64 +155,83 @@ export default function ModalPeminjaman({
: null
}
onChange={(date) => {
perpustakaanDigitalState.peminjamanBuku.create.form.tanggalPinjam =
date ? new Date(date).toISOString() : '';
if (date) {
const tanggalPinjam = new Date(date);
// simpan tanggal pinjam
perpustakaanDigitalState.peminjamanBuku.create.form.tanggalPinjam =
tanggalPinjam.toISOString();
// hitung batas +4 hari
const batasKembali = new Date(tanggalPinjam);
batasKembali.setDate(batasKembali.getDate() + BATAS_HARI_PINJAM);
// set batas & tanggal kembali otomatis
perpustakaanDigitalState.peminjamanBuku.create.form.batasKembali =
batasKembali.toISOString();
perpustakaanDigitalState.peminjamanBuku.create.form.tanggalKembali =
batasKembali.toISOString();
toast.info(
`Batas pengembalian otomatis diset ke ${batasKembali.toLocaleDateString('id-ID')} (+${BATAS_HARI_PINJAM} hari).`
);
} else {
perpustakaanDigitalState.peminjamanBuku.create.form.tanggalPinjam = '';
perpustakaanDigitalState.peminjamanBuku.create.form.batasKembali = '';
perpustakaanDigitalState.peminjamanBuku.create.form.tanggalKembali = '';
}
}}
required
/>
<Box>
<Text>Catatan</Text>
<Text fw={500}>Catatan</Text>
<CreateEditor
value={snap.create.form.catatan}
onChange={(e) =>
(perpustakaanDigitalState.peminjamanBuku.create.form.catatan = e)
onChange={(val) =>
(perpustakaanDigitalState.peminjamanBuku.create.form.catatan =
val)
}
/>
</Box>
<DateInput
label="Tanggal Kembali"
placeholder="Pilih tanggal kembali"
value={
snap.create.form.tanggalKembali
? new Date(snap.create.form.tanggalKembali)
: null
}
onChange={(date) => {
perpustakaanDigitalState.peminjamanBuku.create.form.tanggalKembali =
date ? new Date(date).toISOString() : '';
}}
required
/>
<DateInput
label="Batas Pengembalian"
placeholder="Pilih tanggal kembali"
placeholder="Otomatis diatur +4 hari dari tanggal pinjam"
value={
snap.create.form.batasKembali
? new Date(snap.create.form.batasKembali)
: null
}
onChange={(date) => {
perpustakaanDigitalState.peminjamanBuku.create.form.batasKembali =
date ? new Date(date).toISOString() : '';
}}
required
disabled
readOnly
/>
<DateInput
label="Tanggal Kembali"
placeholder="Otomatis sama dengan batas pengembalian"
value={
snap.create.form.tanggalKembali
? new Date(snap.create.form.tanggalKembali)
: null
}
disabled
readOnly
/>
<Button
onClick={handleSubmit}
loading={snap.create.loading}
disabled={
!snap.create.form.nama ||
!snap.create.form.tanggalPinjam ||
!snap.create.form.batasKembali ||
!snap.create.form.tanggalKembali
!snap.create.form.nama || !snap.create.form.tanggalPinjam
}
rightSection={<IconArrowRight size={16} />}
radius="xl"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Pinjam Buku
</Button>

View File

@@ -42,7 +42,7 @@ function Page() {
>
Dasar Hukum
</Text>
<Text ta="center" fz="sm" c="dimmed">
<Text ta="center" fz="md" >
Informasi regulasi dan kebijakan resmi yang menjadi dasar hukum
</Text>
</Stack>

View File

@@ -3,8 +3,8 @@
import indeksKepuasanState from "@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan";
import colors from "@/con/colors";
import { BarChart, PieChart } from '@mantine/charts';
import { Box, Button, Center, Container, Flex, Group, Modal, Paper, Select, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from "@mantine/core";
import { useDisclosure, useMediaQuery, useShallowEffect } from "@mantine/hooks";
import { Box, Button, Center, Container, Flex, Modal, Paper, Select, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from "@mantine/core";
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
import { useState } from "react";
import { useProxy } from "valtio/utils";
@@ -18,14 +18,13 @@ interface ChartDataItem {
function Kepuasan() {
const state = useProxy(indeksKepuasanState.responden);
const state = useProxy(indeksKepuasanState.responden);
const { data, loading } = state.findMany;
const [donutDataJenisKelamin, setDonutDataJenisKelamin] = useState<ChartDataItem[]>([]);
const [donutDataRating, setDonutDataRating] = useState<ChartDataItem[]>([]);
const [donutDataKelompokUmur, setDonutDataKelompokUmur] = useState<ChartDataItem[]>([]);
const [barChartData, setBarChartData] = useState<Array<{ month: string; count: number }>>([]);
const [opened, { open, close }] = useDisclosure(false);
const isMobile = useMediaQuery("(max-width: 768px)");
const [barChartData, setBarChartData] = useState<Array<{ month: string; Responden: number }>>([]);
const [opened, { open, close }] = useDisclosure(false)
const resetForm = () => {
state.create.form = {
@@ -122,18 +121,18 @@ function Kepuasan() {
// Convert map to array and sort by date
const barData = Array.from(monthYearMap.entries())
.map(([key, count]) => {
.map(([key, Responden]) => {
const [year, month] = key.split('-');
const monthName = new Date(Number(year), Number(month) - 1, 1)
.toLocaleString('id-ID', { month: 'long' });
return {
month: `${monthName} ${year}`,
count,
Responden,
sortKey: parseInt(`${year}${String(month).padStart(2, '0')}`, 10)
};
})
.sort((a, b) => a.sortKey - b.sortKey)
.map(({ month, count }) => ({ month, count }));
.map(({ month, Responden }) => ({ month, Responden }));
setBarChartData(barData);
}
@@ -141,12 +140,12 @@ function Kepuasan() {
if ((loading && !data) || !data) {
return (
<Stack py={10} px="sm">
<Skeleton height={200} mb="md" />
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="md">
<Skeleton height={200} />
<Skeleton height={200} />
<Skeleton height={200} />
<Stack py={10} px="xl">
<Skeleton height={300} mb="md" />
<SimpleGrid cols={{ base: 1, md: 3 }}>
<Skeleton height={300} />
<Skeleton height={300} />
<Skeleton height={300} />
</SimpleGrid>
</Stack>
);
@@ -157,10 +156,16 @@ function Kepuasan() {
<Stack p="sm">
<Container w={{ base: "100%", md: "80%" }} p={"xl"}>
<Center>
<Text ta={"center"} fz={{ base: "2.4rem", md: "3.4rem" }}>Indeks Kepuasan Masyarakat</Text>
<Text fw={"bold"} fz={{ base: "1.8rem", md: "3.4rem" }}>Indeks Kepuasan Masyarakat</Text>
</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}>
<Button radius={"lg"} bg={colors["blue-button"]} onClick={open}>Ajukan Responden</Button>
<Button
radius={"lg"}
onClick={open}
variant="gradient"
gradient={{ from: "#26667F", to: "#124170" }}
>Ajukan Responden</Button>
</Center>
</Container>
<Box px={"xl"}>
@@ -177,10 +182,10 @@ function Kepuasan() {
</Box>
</Flex>
<BarChart
h={300}
h={window.innerWidth < 480 ? 200 : 300}
data={barChartData}
dataKey="month"
series={[{ name: 'count', color: colors['blue-button'] }]}
series={[{ name: 'Responden', color: colors['blue-button'] }]}
tickLine="y"
xAxisLabel="Bulan"
yAxisLabel="Jumlah Responden"
@@ -191,12 +196,9 @@ function Kepuasan() {
</Paper>
<Box py={"xl"}>
<SimpleGrid
cols={{
base: 1,
md: 1,
lg: 1,
xl: 3
}}
cols={{ base: 1, sm: 2, lg: 3 }}
spacing="md"
verticalSpacing="md"
>
{/* Chart Jenis Kelamin */}
<Paper bg={colors['white-1']} p="md" radius="md">
@@ -215,7 +217,7 @@ function Kepuasan() {
withLabels
withTooltip
labelsType="percent"
size={200}
size={250} // Fixed size in pixels
data={donutDataJenisKelamin}
/>
</Center>
@@ -254,7 +256,7 @@ function Kepuasan() {
labelsPosition="outside"
labelsType="percent"
withLabelsLine
size={200}
size={250}
data={donutDataRating}
/>
</Center>
@@ -297,7 +299,7 @@ function Kepuasan() {
labelsPosition="outside"
labelsType="percent"
withLabelsLine
size={190}
size={250}
data={donutDataKelompokUmur}
/>
</Center>
@@ -330,7 +332,7 @@ function Kepuasan() {
<TextInput
label="Nama"
type='text'
placeholder="masukkan nama"
placeholder="Masukkan nama"
defaultValue={state.create.form.name}
onChange={(val) => {
state.create.form.name = val.currentTarget.value;
@@ -413,41 +415,57 @@ function Kepuasan() {
);
}
return (
<Stack p="sm">
<Container w={{ base: "100%", md: "80%" }} p={isMobile ? "md" : "xl"}>
<Stack gap="xs">
<Text ta="center" fz={{ base: "2rem", md: "3rem" }}>Indeks Kepuasan Masyarakat</Text>
<Group justify="center">
<Button radius="lg" bg={colors["blue-button"]} onClick={open}>
Ajukan Responden
</Button>
</Group>
</Stack>
<Stack p={"sm"}>
<Container size="lg" px="md">
<Center>
<Text ta={"center"} fz={{ base: "2.4rem", md: "3.4rem" }}>Indeks Kepuasan Masyarakat</Text>
</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>
</Center>
</Container>
<Box px={isMobile ? "sm" : "xl"}>
<Paper p="lg" bg={colors.Bg}>
<Paper p={isMobile ? "sm" : "lg"}>
<Stack gap="xs">
<Flex direction={isMobile ? "column" : "row"} justify="space-between" align={isMobile ? "start" : "center"}>
<Text fw="bold" mb={isMobile ? "sm" : 0}>Pelayanan Terhadap Publik Desa Darmasaba</Text>
<Box>
<Text fz="sm" fw="bold" c={colors["blue-button"]}>Total Responden</Text>
<Text ta="end" fz="h1" fw="bold" c={colors["blue-button"]}>
{state.findMany.total.toLocaleString("id-ID")}
<Box px={"xl"}>
<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" }}
>
<Text fw="bold" ta={{ base: "center", sm: "left" }}>
Pelayanan Terhadap Publik Desa Darmasaba
</Text>
<Box mt={{ base: "sm", sm: 0 }}>
<Text fz={"sm"} fw={"bold"} c={colors["blue-button"]}>Total Responden</Text>
<Text ta={"end"} fz={"h1"} fw={"bold"} c={colors["blue-button"]}>
{state.findMany.total.toLocaleString('id-ID')}
</Text>
</Box>
</Flex>
<BarChart
h={isMobile ? 200 : 300}
h={300}
data={barChartData}
dataKey="month"
series={[{ name: "count", color: colors["blue-button"] }]}
series={[{ name: 'Responden', color: colors['blue-button'] }]}
tickLine="y"
xAxisLabel="Bulan"
yAxisLabel="Jumlah Responden"
withTooltip
tooltipAnimationDuration={200}
/>
</Stack>
</Paper>
<Box py="xl">
<SimpleGrid cols={{ base: 1, sm: 2, xl: 3 }} spacing="lg">
<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>
@@ -457,17 +475,28 @@ function Kepuasan() {
Belum ada data untuk ditampilkan dalam grafik
</Text>
) : (
<Paper p="md" radius="md">
<Stack>
<Center>
<PieChart
size={isMobile ? 150 : 200}
withLabels
data={donutDataJenisKelamin}
withTooltip
/>
</Center>
</Stack>
<Paper p="md" radius="md" withBorder>
<Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<Box style={{ position: 'relative', width: '100%' }}>
<Center>
<PieChart
withLabels
withTooltip
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>
</Flex>
))}
</Stack>
</Box>
</Paper>
)}
</Stack>
@@ -482,18 +511,35 @@ function Kepuasan() {
Belum ada data untuk ditampilkan dalam grafik
</Text>
) : (
<Paper p="md" radius="md">
<Stack>
<Center>
<PieChart
size={isMobile ? 150 : 200}
withLabels
labelsPosition="outside"
withLabelsLine
data={donutDataRating}
/>
</Center>
</Stack>
<Paper p="md" radius="md" withBorder>
<Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<Box style={{ position: 'relative', width: '100%' }}>
<Center>
<PieChart
withTooltip
tooltipAnimationDuration={200}
withLabels
labelsPosition="outside"
labelsType="percent"
withLabelsLine
size={200}
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' }}>
{entry.name}: {entry.value}
</Text>
</Flex>
))}
</SimpleGrid>
</Box>
</Box>
</Paper>
)}
</Stack>
@@ -508,18 +554,35 @@ function Kepuasan() {
Belum ada data untuk ditampilkan dalam grafik
</Text>
) : (
<Paper p="md" radius="md">
<Stack>
<Center>
<PieChart
size={isMobile ? 150 : 200}
withLabels
labelsPosition="outside"
withLabelsLine
data={donutDataKelompokUmur}
/>
</Center>
</Stack>
<Paper p="md" radius="md" withBorder>
<Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<Box style={{ position: 'relative', width: '100%' }}>
<Center>
<PieChart
withTooltip
tooltipAnimationDuration={200}
withLabels
labelsPosition="outside"
labelsType="percent"
withLabelsLine
size={190}
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' }}>
{entry.name}: {entry.value}
</Text>
</Flex>
))}
</SimpleGrid>
</Box>
</Box>
</Paper>
)}
</Stack>
@@ -542,7 +605,7 @@ function Kepuasan() {
}}
/>
<TextInput
label="Tanggal"
label="Tanggal Pengisian"
type="date"
placeholder="masukkan tanggal"
defaultValue={state.create.form.tanggal}

View File

@@ -32,7 +32,7 @@ export default function JenisInformasiSelector({ onChange }: {
return (
<Group>
<Select
placeholder='pilih jenis informasi'
placeholder='Pilih jenis informasi'
label='Jenis Informasi'
data={data.map((item) => ({
value: item.id,

View File

@@ -28,7 +28,7 @@ function MemperolehInformasi({ onChange }: {
return (
<Group>
<Select
placeholder='pilih cara memperoleh informasi'
placeholder='Pilih cara memperoleh informasi'
label={"Cara Memperoleh Informasi"}
data={data.map((item) => ({
value: item.id,

View File

@@ -26,7 +26,7 @@ function MemperolehSalinan({ onChange }: {
return (
<Group>
<Select
placeholder='pilih cara memperoleh salinan informasi'
placeholder='Pilih cara memperoleh salinan informasi'
label={'Cara Memperoleh Salinan Informasi'}
data={data.map((item) => ({
value: item.id,

View File

@@ -178,7 +178,7 @@ function Page() {
<TextInput
label="Alamat Email"
placeholder="contoh: nama@email.com"
placeholder="Contoh: nama@email.com"
radius="md"
size="md"
type="email"
@@ -190,7 +190,7 @@ function Page() {
<TextInput
label="Nomor Telepon"
placeholder="contoh: 0812-3456-7890"
placeholder="Contoh: 0812-3456-7890"
radius="md"
size="md"
withAsterisk

View File

@@ -51,31 +51,37 @@ function Page() {
<Box key={item.id} px={{ base: "md", md: 100 }}>
<Paper p="xl" bg={colors['white-trans-1']} radius="lg" shadow="xl">
<Box px={{ base: "md", md: 100 }}>
<Flex align="center" gap={40} justify="center">
<Image loading='lazy' src="/darmasaba-icon.png" h={{ base: 70, md: 120 }} alt="Logo Desa" />
<Text fz={{ base: "1.5rem", md: "2rem", lg: "2.5rem", xl: "3rem" }} fw="bold">
Pejabat Pengelola Informasi Publik
</Text>
</Flex>
<Center>
<Image loading='lazy' src="/darmasaba-icon.png" h={{ base: 70, md: 120 }} w={{ base: 70, md: 120 }} alt="Logo Desa" />
</Center>
<Text ta="center" fz={{ base: "1.2rem", md: "2rem", lg: "2.5rem", xl: "3rem" }} fw="bold">
Pejabat Pengelola Informasi dan Dokumentasi
</Text>
</Box>
<Divider my="lg" />
<Box px={{ base: 0, md: 50 }} pb={40}>
<SimpleGrid cols={{ base: 1, xl: 2 }} spacing="xl">
<Box px={{ base: 0, md: 50 }}>
<Paper bg={colors['white-trans-1']} radius="lg" shadow="sm">
<Stack gap="md">
<Center>
<Image
loading='lazy'
src={item.image?.link ? `${item.image.link}?t=${Date.now()}` : "/perbekel.png"}
w={{ base: 220, md: 330 }}
alt="Foto Pimpinan"
radius="md"
/>
</Center>
<Paper bg={colors['blue-button']} py={25} radius="lg" className="glass3">
<Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "1.5rem", md: "2rem" }}>
<Paper bg={colors['white-trans-1']} radius="xl" shadow="md" withBorder>
<Stack gap={0}>
<Image
pt={{ base: 0, md: 100 }}
px="lg"
src={item.image?.link ? `${item.image.link}?t=${Date.now()}` : "/perbekel.png"}
alt="Foto Pimpinan"
radius="lg"
onError={(e) => e.currentTarget.src = "/perbekel.png"}
loading="lazy"
/>
<Paper
bg={colors['blue-button']}
px="lg"
radius="0 0 var(--mantine-radius-xl) var(--mantine-radius-xl)"
className="glass3"
py={{ base: 20, md: 50 }}
>
<Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "xl", md: "h2" }}>
{item.name}
</Text>
</Paper>

View File

@@ -233,21 +233,42 @@ function StrukturOrganisasiPPID() {
return (
<Stack align="center" mt="xl">
{/* 🔍 Search + Zoom + Fullscreen controls */}
<Group mb="md" justify="center" gap="sm">
<Group mb="md" justify="center" gap="sm" align="center">
<TextInput
placeholder="Cari nama atau jabatan..."
leftSection={<IconSearch size={16} />}
onChange={(e) => debouncedSearch(e.target.value)}
/>
<Button variant="light" size="sm" onClick={handleZoomOut}>
<IconZoomOut size={16} />
</Button>
<Button variant="light" size="sm" onClick={resetZoom}>
100%
</Button>
{/* 🔍 Tambahkan indikator zoom di sini */}
{/* Floating Zoom Indicator */}
<Box
bg="#C3D0E8"
c="blue"
px={9}
py={8}
style={{
fontSize: 14,
fontWeight: 600,
borderRadius: '5px',
}}
>
{Math.round(scale * 100)}%
</Box>
<Button variant="light" size="sm" onClick={handleZoomIn}>
<IconZoomIn size={16} />
</Button>
<Button variant="light" size="sm" onClick={resetZoom}>
Reset
</Button>
<Button
variant="light"
size="sm"
@@ -260,6 +281,7 @@ function StrukturOrganisasiPPID() {
</Button>
</Group>
{/* Chart Container */}
<Box
ref={chartContainerRef}

View File

@@ -54,12 +54,11 @@ function Page() {
ta="center"
fz={{ base: 28, md: 36 }}
fw={800}
variant="gradient"
gradient={{ from: colors['blue-button'], to: 'cyan', deg: 45 }}
c={colors['blue-button']}
>
Moto PPID Desa Darmasaba
</Text>
<Text ta="center" fz={{ base: 16, md: 20 }} mt="xs" c="dimmed">
<Text ta="center" fz={{ base: 16, md: 20 }} mt="xs">
Memberikan informasi yang cepat, mudah, tepat, dan transparan
</Text>
</Box>
@@ -88,7 +87,6 @@ function Page() {
<Text
fz={{ base: 'md', md: 'lg' }}
lh={1.7}
ta="center"
dangerouslySetInnerHTML={{ __html: item.misi }}
style={{wordBreak: "break-word", whiteSpace: "normal"}}
/>

View File

@@ -2,11 +2,11 @@
'use client';
import penghargaanState from "@/app/admin/(dashboard)/_state/desa/penghargaan";
import colors from "@/con/colors";
import { Carousel, CarouselSlide } from "@mantine/carousel";
import { Box, Button, Container, Group, Paper, Stack, Text, useMantineTheme, Skeleton } from "@mantine/core";
import { Carousel } from "@mantine/carousel";
import { Box, Button, Container, Group, Paper, Skeleton, Stack, Text, useMantineTheme } from "@mantine/core";
import { useMediaQuery } from "@mantine/hooks";
import { IconArrowRight, IconAward } from "@tabler/icons-react";
import Autoplay from "embla-carousel-autoplay";
import { IconAward, IconArrowRight } from "@tabler/icons-react";
import { useTransitionRouter } from "next-view-transitions";
import { useEffect, useRef } from "react";
import { useProxy } from "valtio/utils";
@@ -18,7 +18,8 @@ export default function Page() {
<Box px={{ base: "md", md: 100 }}>
<BackButton />
</Box>
<Container w={{ base: "100%", md: "60%" }}>
<Container w={{ base: "100%", md: "90%", lg: "60%" }}>
<Stack align="center" gap="sm">
<Group gap="xs">
<IconAward size={40} color={colors["blue-button"]} />
@@ -37,11 +38,10 @@ export default function Page() {
}
function Slider() {
const height = 500;
const width = 1200;
const theme = useMantineTheme();
const mobile = useMediaQuery(`(max-width: ${theme.breakpoints.sm})`);
const autoplay = useRef(Autoplay({ delay: 3000 }));
const tablet = useMediaQuery(`(max-width: ${theme.breakpoints.md})`);
const autoplay = useRef(Autoplay({ delay: 3000, stopOnInteraction: false }));
const state = useProxy(penghargaanState);
const router = useTransitionRouter();
@@ -54,7 +54,7 @@ function Slider() {
if (loading) {
return (
<Group justify="center" py="xl">
<Group justify="center" py="xl" gap="md">
<Skeleton w={300} h={200} radius="lg" />
<Skeleton w={300} h={200} radius="lg" visibleFrom="sm" />
<Skeleton w={300} h={200} radius="lg" visibleFrom="md" />
@@ -74,31 +74,49 @@ function Slider() {
}
const slides = data.map((item) => (
<CarouselSlide key={item.id}>
<Carousel.Slide key={item.id}>
<Paper
h="100%"
radius="lg"
shadow="md"
pos="relative"
style={{
height: "100%",
backgroundImage: `url(${item.image?.link})`,
backgroundSize: "cover",
backgroundPosition: "center",
transition: "transform 0.3s ease, box-shadow 0.3s ease",
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = "translateY(-4px)";
e.currentTarget.style.boxShadow = "0 8px 20px rgba(0,0,0,0.2)";
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = "translateY(0)";
e.currentTarget.style.boxShadow = "none";
}}
>
<Box
pos="absolute"
inset={0}
bg="linear-gradient(to top, rgba(0,0,0,0.7), rgba(0,0,0,0.3))"
bg="linear-gradient(to top, rgba(0,0,0,0.8), rgba(0,0,0,0.2))"
style={{ borderRadius: 16 }}
/>
<Stack justify="flex-end" h="100%" gap="sm" p="lg" pos="relative">
<Text fz="xl" fw={700} ta="center" c="white">
<Text
fz={{ base: "md", sm: "lg", md: "xl" }}
fw={700}
ta="center"
c="white"
lineClamp={3}
style={{ textShadow: "0 2px 4px rgba(0,0,0,0.6)" }}
>
{item.name}
</Text>
<Group justify="center">
<Button
onClick={() => router.push(`/darmasaba/penghargaan/${item.id}`)}
onClick={() =>
router.push(`/darmasaba/penghargaan/${item.id}`)
}
size="md"
radius="xl"
rightSection={<IconArrowRight size={18} />}
@@ -110,24 +128,83 @@ function Slider() {
</Group>
</Stack>
</Paper>
</CarouselSlide>
</Carousel.Slide>
));
return (
<Carousel
py="xl"
plugins={[autoplay.current]}
onMouseEnter={autoplay.current.stop}
onMouseLeave={autoplay.current.reset}
w={{ base: "100%", sm: "90%", md: "80%", lg: width }}
h={height}
slideSize={{ base: "100%", sm: "50%", md: "33.333333%" }}
slideGap="md"
loop
align="start"
slidesToScroll={mobile ? 1 : 2}
<Box
pos="relative"
w="100%"
mx="auto"
px={{ base: "md", sm: "xl", md: "2rem", lg: "3rem" }}
style={{
maxWidth: 1300,
}}
>
{slides}
</Carousel>
<Carousel
py="xl"
w="100%"
h={{ base: 320, sm: 380, md: 420, lg: 450 }}
slideSize={{
base: "100%", // Mobile: 1
sm: "50%", // Tablet kecil (≥768): 2
md: "50%", // 1024px: tetap 2
lg: "33.333%", // Desktop besar: 3
}}
slideGap={{ base: "md", sm: "md", md: "lg" }}
loop
align="start"
slidesToScroll={mobile ? 1 : tablet ? 2 : 3}
plugins={[autoplay.current]}
onMouseEnter={autoplay.current.stop}
onMouseLeave={autoplay.current.reset}
withControls={data.length > 3}
draggable={data.length > 1}
styles={{
root: {
position: "relative",
},
viewport: {
overflow: "hidden",
},
container: {
alignItems: "stretch",
},
control: {
zIndex: 20,
backgroundColor: "rgba(255,255,255,0.95)",
color: colors["blue-button"],
border: `2px solid ${colors["blue-button"]}`,
width: 46,
height: 46,
borderRadius: "50%",
boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
transition: "all 0.2s ease",
'&:hover': {
backgroundColor: colors["blue-button"],
color: "white",
transform: "scale(1.1)",
},
'&[data-inactive]': {
opacity: 0,
cursor: 'default',
},
},
controls: {
position: "absolute",
top: mobile ? "70%" : tablet ? "65%" : "60%",
transform: "translateY(-50%)",
width: mobile ? "100%" : tablet ? "calc(100% + 60px)" : "calc(100% + 100px)",
left: mobile ? "0" : tablet ? "-30px" : "-50px",
right: mobile ? "0" : tablet ? "-30px" : "-50px",
padding: "0",
justifyContent: "space-between",
zIndex: 30,
},
}}
>
{slides}
</Carousel>
</Box>
);
}
}

View File

@@ -1,6 +1,8 @@
'use client'
import { ActionIcon, Anchor, Box, Button, Center, Container, Divider, Flex, Group, Image, Paper, SimpleGrid, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconAt, IconBrandFacebook, IconBrandInstagram, IconBrandTiktok, IconBrandYoutube } from '@tabler/icons-react';
import { useRef } from 'react';
import { toast } from 'react-toastify';
const sosialMedia = [
{
@@ -60,6 +62,39 @@ const tautanPenting = [
]
function Footer() {
const emailRef = useRef<HTMLInputElement>(null)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const email = emailRef.current?.value.trim();
if (!email) return toast.error('Email wajib diisi!');
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) return toast.error('Format email tidak valid!');
try {
const res = await fetch('/api/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
});
const data = await res.json();
if (res.ok && data.success) {
toast.success('Berhasil! Cek email Anda untuk konfirmasi.');
emailRef.current!.value = '';
} else {
toast.error(data.message || 'Gagal berlangganan.');
}
} catch (err) {
console.error(err);
toast.error('Gagal menghubungi server. Coba lagi nanti.');
}
};
return (
<Stack bg="linear-gradient(180deg, #1C6EA4, #124170)" c="white">
<Box w="100%" p="xl">
@@ -166,8 +201,9 @@ function Footer() {
w="70%"
placeholder="Masukkan email Anda"
rightSection={<IconAt size={16} />}
ref={emailRef} // ini aja cukup
/>
<Button variant="gradient" gradient={{ from: 'blue', to: 'cyan' }} radius="md">Daftar</Button>
<Button onClick={handleSubmit} variant="gradient" gradient={{ from: 'blue', to: 'cyan' }} radius="md">Daftar</Button>
</Group>
</Stack>
</Box>

View File

@@ -7,17 +7,20 @@ import GlobalSearch from "./globalSearch";
export function NavbarSearch() {
const [isOpen, setIsOpen] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const isNavigatingRef = useRef(false);
// Close when clicking outside
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
const target = event.target as HTMLElement;
// Only close if clicking outside both the search input and results
if (
containerRef.current &&
!containerRef.current.contains(target) &&
!target.closest('.search-result-item') // Add a class to your search result items
) {
// Jangan close jika klik di search result item (biar handleSelect yang urus)
if (target.closest('.search-result-item')) {
return;
}
// Close jika klik di luar container
if (containerRef.current && !containerRef.current.contains(target)) {
setIsOpen(false);
stateNav.clear();
}
@@ -29,6 +32,13 @@ export function NavbarSearch() {
};
}, []);
// Reset navigation flag saat component unmount atau route change
useEffect(() => {
return () => {
isNavigatingRef.current = false;
};
}, []);
return (
<Box
ref={containerRef}

View File

@@ -5,7 +5,7 @@ import stateNav from "@/state/state-nav";
import { ActionIcon, Box, Burger, Group, Image, Paper, ScrollArea, Stack, Text, Tooltip } from "@mantine/core";
import { IconSquareArrowRight } from "@tabler/icons-react";
import { motion } from "framer-motion";
import { useRouter } from "next/navigation";
import { usePathname, useRouter } from "next/navigation";
import { useSnapshot } from "valtio";
import { MenuItem } from "../../../../types/menu-item";
import { NavbarMainMenu } from "./NavbarMainMenu";
@@ -19,14 +19,18 @@ export function Navbar() {
<Paper
radius="0"
className="glass2"
w="100%"
w="100vw"
pos="fixed"
top={0}
style={{ zIndex: 100 }}
>
<NavbarMainMenu listNavbar={navbarListMenu} />
{/* Desktop navbar (muncul mulai 992px ke atas) */}
<Box visibleFrom="md">
<NavbarMainMenu listNavbar={navbarListMenu} />
</Box>
<Box hiddenFrom="sm" bg={colors.grey[2]} px="md" py="sm">
{/* Mobile navbar (muncul di bawah 992px, termasuk iPad Mini) */}
<Box hiddenFrom="md" bg={colors.grey[2]} px="md" py="sm">
<Group justify="space-between" wrap="nowrap">
<ActionIcon
variant="transparent"
@@ -37,11 +41,22 @@ export function Navbar() {
stateNav.mobileOpen = false;
}}
>
<Tooltip label="Go to homepage" position="bottom" withArrow>
<Image src="/darmasaba-icon.png" alt="Village Logo" width={48} height={48} loading="lazy"/>
<Tooltip label="Kembali ke Beranda" position="bottom" withArrow>
<Image
src="/darmasaba-icon.png"
alt="Village Logo"
width={48}
height={48}
loading="lazy"
/>
</Tooltip>
</ActionIcon>
<Tooltip label={mobileOpen ? "Close menu" : "Open menu"} position="bottom" withArrow>
<Tooltip
label={mobileOpen ? "Close menu" : "Open menu"}
position="bottom"
withArrow
>
<Burger
opened={mobileOpen}
color={colors["blue-button"]}
@@ -50,12 +65,14 @@ export function Navbar() {
/>
</Tooltip>
</Group>
{mobileOpen && (
<Paper
component={motion.div}
initial={{ x: '100%' }}
bg="white"
initial={{ x: "100%" }}
animate={{ x: 0 }}
exit={{ x: '100%' }}
exit={{ x: "100%" }}
transition={{ duration: 0.2 }}
pos="absolute"
left={0}
@@ -63,12 +80,14 @@ export function Navbar() {
top="100%"
m={0}
radius={0}
shadow="md"
>
<NavbarMobile listNavbar={navbarListMenu} />
</Paper>
)}
</Box>
</Paper>
{(item || isSearch) && <Box className="glass" />}
</Box>
);
@@ -76,40 +95,105 @@ export function Navbar() {
function NavbarMobile({ listNavbar }: { listNavbar: MenuItem[] }) {
const router = useRouter();
const pathname = usePathname(); // 👈 untuk cek path aktif
// fungsi bantu: cek apakah path sekarang sama dengan menu / sub-menu
const isActive = (href?: string) => href && pathname.startsWith(href);
return (
<ScrollArea.Autosize mah="calc(100vh - 80px)" offsetScrollbars>
<Stack p="md" gap="xs">
{listNavbar.map((item, k) => (
<Box key={k}>
<Group
justify="space-between"
align="center"
p="xs"
onClick={() => {
if (item.href) {
router.push(item.href);
stateNav.mobileOpen = false;
}
}}
style={{
cursor: item.href ? "pointer" : "default",
opacity: item.href ? 1 : 0.8
}}
>
<Text c="dark.9" fw={600} fz="md">
{item.name}
</Text>
<IconSquareArrowRight size={18} />
</Group>
{item.children && (
<Box pl="md">
<NavbarMobile listNavbar={item.children} />
</Box>
)}
</Box>
))}
<ScrollArea.Autosize
mah="calc(100dvh - 80px)"
type="auto"
offsetScrollbars
>
<Stack p="sm" gap="xs">
{listNavbar.map((item, k) => {
const active = isActive(item.href);
return (
<Box key={k}>
<Paper
shadow={active ? "sm" : "xs"}
radius="md"
p="sm"
withBorder
bg={active ? "blue.0" : "gray.0"}
onClick={() => {
if (item.href) {
router.push(item.href);
stateNav.mobileOpen = false;
}
}}
style={{
cursor: item.href ? "pointer" : "default",
transition: "background 0.15s ease",
borderLeft: active ? "4px solid #1e66f5" : "4px solid transparent",
}}
>
<Group justify="space-between" align="center" wrap="nowrap">
<Text
fw={active ? 700 : 600}
fz="md"
c={active ? "blue.7" : "dark.9"}
>
{item.name}
</Text>
{item.href && (
<IconSquareArrowRight
size={18}
color={active ? "#1e66f5" : "inherit"}
/>
)}
</Group>
</Paper>
{/* Submenu */}
{item.children && (
<Box pl="md" mt={4}>
{item.children.map((child, j) => {
const childActive = isActive(child.href);
return (
<Group
key={j}
justify="space-between"
align="center"
p="xs"
onClick={() => {
if (child.href) {
router.push(child.href);
stateNav.mobileOpen = false;
}
}}
style={{
cursor: child.href ? "pointer" : "default",
opacity: child.href ? 1 : 0.8,
borderRadius: "0.5rem",
backgroundColor: childActive ? "#e7f0ff" : "transparent",
borderLeft: childActive ? "3px solid #1e66f5" : "3px solid transparent",
transition: "background 0.15s ease",
}}
>
<Text
fz="sm"
fw={childActive ? 600 : 400}
c={childActive ? "blue.7" : "dark.8"}
>
{child.name}
</Text>
<IconSquareArrowRight
size={14}
color={childActive ? "#1e66f5" : "inherit"}
/>
</Group>
);
})}
</Box>
)}
</Box>
);
})}
</Stack>
</ScrollArea.Autosize>
);
}

View File

@@ -26,7 +26,7 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
<Stack gap={0} visibleFrom="sm" bg={colors["white-trans-1"]}>
<Container pos="relative" w={{ base: '100%', md: '80%' }} fluid>
<Flex align="center" justify="space-between" wrap={{ base: "wrap", md: "nowrap" }}>
<Tooltip label="Go to Homepage" position="bottom" withArrow>
<Tooltip label="Kembali ke Beranda" position="bottom" withArrow>
<ActionIcon
radius="xl"
variant="transparent"
@@ -50,12 +50,12 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
<MenuItemCom
key={k}
item={item}
isActive={pathname === item.href ||
(item.children?.some(child => child.href === pathname))}
isActive={item.href && pathname.startsWith(item.href) ||
(item.children?.some(child => child.href && pathname.startsWith(child.href)))}
/>
))}
<Tooltip label="Search content" position="bottom" withArrow>
<Tooltip label="Cari Konten" position="bottom" withArrow>
<ActionIcon
variant="transparent"
c={isSearch ? 'gray' : colors["blue-button"]}
@@ -71,7 +71,7 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
{/* hanya tampil kalau role = admin */}
{stateAuth.role === "admin" && (
<Tooltip label="My Profile" position="bottom" withArrow>
<Tooltip label="Profil Saya" position="bottom" withArrow>
<ActionIcon
onClick={() => {
next.push("/admin/landing-page/profile/program-inovasi")

View File

@@ -38,7 +38,7 @@ export function NavbarSubMenu({ item }: { item: MenuItem[] | null }) {
justify="space-between"
size="lg"
radius="md"
color={pathname === link.href ? 'blue' : 'gray'}
color={link.href && pathname.startsWith(link.href) ? 'blue' : 'gray'}
onClick={() => {
if (link.href) {
router.push(link.href);
@@ -49,12 +49,12 @@ export function NavbarSubMenu({ item }: { item: MenuItem[] | null }) {
rightSection={<IconArrowRight size={18} />}
styles={(theme) => ({
root: {
background: pathname === link.href ? theme.colors.blue[0] : 'transparent',
color: pathname === link.href ? theme.colors.blue[7] : colors['blue-button'],
fontWeight: pathname === link.href ? 600 : 500,
background: link.href && pathname.startsWith(link.href) ? theme.colors.blue[0] : 'transparent',
color: link.href && pathname.startsWith(link.href) ? theme.colors.blue[7] : colors['blue-button'],
fontWeight: link.href && pathname.startsWith(link.href) ? 600 : 500,
transition: "all 0.2s ease",
"&:hover": {
background: pathname === link.href ? theme.colors.blue[1] : theme.colors.gray[0],
background: link.href && pathname.startsWith(link.href) ? theme.colors.blue[1] : theme.colors.gray[0],
}
},
})}

View File

@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client';
import searchState, { debouncedFetch } from '@/app/api/[[...slugs]]/_lib/search/searchState';
import { Box, Center, Loader, Popover, Text, TextInput } from '@mantine/core';
import { IconX } from '@tabler/icons-react';
@@ -10,36 +11,85 @@ import getDetailUrl from './searchUrl';
export default function GlobalSearch() {
const snap = useSnapshot(searchState);
const [opened, setOpened] = useState(false);
const [isNavigating, setIsNavigating] = useState(false);
// buka popover saat ada query
// Buka popover saat ada query
useEffect(() => {
setOpened(!!snap.query);
}, [snap.query]);
// infinite scroll
// Infinite scroll handler
useEffect(() => {
const handleScroll = () => {
const bottom = window.innerHeight + window.scrollY >= document.body.offsetHeight - 200;
if (bottom && !snap.loading) searchState.next();
const nearBottom = window.innerHeight + window.scrollY >= document.body.offsetHeight - 200;
if (nearBottom && !snap.loading) searchState.next();
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [snap.loading]);
const handleSelect = async (e: React.MouseEvent, item: any) => {
e.stopPropagation();
e.preventDefault();
e.stopPropagation();
if (isNavigating) return;
setIsNavigating(true);
try {
// 🔥 pastikan objek udah “dikeluarkan” dari Proxy valtio
const rawItem = JSON.parse(JSON.stringify(item));
// 🔥 pastikan type-nya string murni
const type = String(rawItem.type || '').trim().toLowerCase();
// 🔥 panggil getDetailUrl pakai type yang fix
let url = getDetailUrl({ ...rawItem, type });
// kalau hasil undefined atau default, fallback ke link eksternal
if (!url || url === '/darmasaba') {
if (rawItem.link && rawItem.link.startsWith('http')) {
url = rawItem.link;
}
}
if (!url) {
console.warn('URL tidak ditemukan untuk item:', rawItem);
setIsNavigating(false);
return;
}
console.log('Navigating to:', url);
// tutup popover dulu
setOpened(false);
searchState.query = '';
searchState.results = [];
searchState.loading = false;
// kasih delay biar UI nutup dulu
await new Promise((r) => setTimeout(r, 100));
// navigasi
if (url.startsWith('http')) {
window.location.href = url;
} else {
window.location.href = url;
}
} catch (err) {
console.error('Error saat navigasi:', err);
setIsNavigating(false);
}
};
const url = getDetailUrl(item);
if (!url) return;
// Immediately close the search dropdown
const clearSearch = () => {
searchState.query = '';
searchState.results = [];
searchState.page = 1;
searchState.nextPage = null;
setOpened(false);
searchState.results = []; // Clear results immediately
searchState.loading = false;
// Use window.location for navigation to ensure full page reload
window.location.href = url;
setIsNavigating(false);
};
return (
@@ -47,13 +97,7 @@ export default function GlobalSearch() {
<Popover
opened={opened && !!snap.query}
onChange={(isOpen) => {
if (!isOpen) {
// Clear search state when popover is closed
searchState.query = '';
searchState.results = [];
searchState.page = 1;
searchState.nextPage = null;
}
if (!isOpen) clearSearch();
setOpened(isOpen);
}}
width="target"
@@ -61,10 +105,14 @@ export default function GlobalSearch() {
shadow="md"
withinPortal
radius="md"
zIndex={1000} // Add this line to ensure it appears above other elements
zIndex={2000}
closeOnClickOutside={true}
closeOnEscape={true}
styles={{
dropdown: {
zIndex: 1000, // Add this to ensure the dropdown appears above other elements
zIndex: 2000,
borderRadius: 12,
overflow: 'hidden',
},
}}
>
@@ -83,13 +131,7 @@ export default function GlobalSearch() {
<IconX
size={16}
style={{ cursor: 'pointer' }}
onClick={() => {
searchState.query = '';
searchState.results = [];
searchState.page = 1;
searchState.nextPage = null;
setOpened(false);
}}
onClick={clearSearch}
/>
) : undefined
}
@@ -101,34 +143,32 @@ export default function GlobalSearch() {
style={{
maxHeight: 350,
overflowY: 'auto',
borderRadius: 12,
zIndex: 1000, // Add this line to ensure dropdown stays above other elements
position: 'relative', // Add this to contain child elements
backgroundColor: '#fff',
border: '1px solid #eee',
}}
>
{snap.results.length > 0 ? (
snap.results.map((item, i) => (
{[...snap.results].length > 0 ? (
[...snap.results].map((item: any, i: number) => (
<Box
key={i}
p="sm"
className="search-result-item" // Add this class
className="search-result-item" // Add class untuk prevent close
style={{
borderBottom: '1px solid #eee',
cursor: 'pointer',
borderBottom: '1px solid #f1f1f1',
cursor: isNavigating ? 'wait' : 'pointer',
background: 'white',
transition: 'background 0.2s',
position: 'relative', // Add this
zIndex: 1, // Add this to ensure proper stacking context
backgroundColor: 'white', // Ensure background is set
opacity: isNavigating ? 0.6 : 1,
}}
onMouseEnter={(e) => (e.currentTarget.style.background = '#f7f7f7')}
onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')}
onClick={(e) => handleSelect(e, item)} // Pass the event here
onMouseEnter={(e) => !isNavigating && (e.currentTarget.style.background = '#f9f9f9')}
onMouseLeave={(e) => (e.currentTarget.style.background = 'white')}
onClick={(e) => handleSelect(e, item)}
>
<Text size="sm" fw={500}>
{item.judul || item.namaPasar || item.nama || item.name}
<Text size="sm" fw={500} lineClamp={1}>
{item.name ?? item.nama ?? item.namaPasar ?? item.judul ?? '(Tanpa nama)'}
</Text>
<Text size="xs" c="dimmed">
dari modul: {item.type}
<Text size="xs" c="dimmed" lineClamp={1}>
dari modul: {item.type || '-'}
</Text>
</Box>
))
@@ -141,4 +181,4 @@ export default function GlobalSearch() {
</Popover>
</Box>
);
}
}

View File

@@ -34,13 +34,13 @@ function Apbdes() {
const data = (state.findMany.data || []).slice(0, 3)
return (
<Stack p="lg" gap="4rem" bg={colors.Bg}>
<Stack p="sm" gap="xl" bg={colors.Bg}>
<Box>
<Stack gap="sm">
<Text ta={"center"} fz={{ base: '2.4rem', sm: '4rem' }} fw="bold" lh={1.2}>
<Text c={colors["blue-button"]} ta={"center"} fw={"bold"} fz={{ base: "1.8rem", md: "3.4rem" }}>
{textHeading.title}
</Text>
<Text ta={"center"} fz={{ base: '1rem', sm: '1.3rem' }} c="dimmed">
<Text ta={"center"} fz={{ base: "1rem", md: "1.3rem" }}>
{textHeading.des}
</Text>
</Stack>
@@ -117,7 +117,7 @@ function Apbdes() {
)}
</SimpleGrid>
<Group pb={80} justify="center">
<Group justify="center" pb={10}>
<Button
component={Link}
href="/darmasaba/apbdes"

View File

@@ -30,9 +30,9 @@ function DesaAntiKorupsi() {
const data = (state.desaAntikorupsi.findMany.data || []).slice(0, 6);
return (
<Stack gap={"0"} bg={colors.Bg} p={"sm"}>
<Container w={{ base: "100%", md: "80%" }} p={"xl"} >
<Container w={{ base: "100%", md: "80%" }} p={"md"} >
<Center>
<Text fw={"bold"} fz={{ base: "1.8rem", md: "3.4rem" }}>Desa Anti Korupsi</Text>
<Text fw={"bold"} c={colors["blue-button"]} fz={{ base: "1.8rem", 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}>

View File

@@ -23,7 +23,7 @@ function Kepuasan() {
const [donutDataJenisKelamin, setDonutDataJenisKelamin] = useState<ChartDataItem[]>([]);
const [donutDataRating, setDonutDataRating] = useState<ChartDataItem[]>([]);
const [donutDataKelompokUmur, setDonutDataKelompokUmur] = useState<ChartDataItem[]>([]);
const [barChartData, setBarChartData] = useState<Array<{ month: string; count: number }>>([]);
const [barChartData, setBarChartData] = useState<Array<{ month: string; Responden: number }>>([]);
const [opened, { open, close }] = useDisclosure(false)
const resetForm = () => {
@@ -121,18 +121,18 @@ function Kepuasan() {
// Convert map to array and sort by date
const barData = Array.from(monthYearMap.entries())
.map(([key, count]) => {
.map(([key, Responden]) => {
const [year, month] = key.split('-');
const monthName = new Date(Number(year), Number(month) - 1, 1)
.toLocaleString('id-ID', { month: 'long' });
return {
month: `${monthName} ${year}`,
count,
Responden,
sortKey: parseInt(`${year}${String(month).padStart(2, '0')}`, 10)
};
})
.sort((a, b) => a.sortKey - b.sortKey)
.map(({ month, count }) => ({ month, count }));
.map(({ month, Responden }) => ({ month, Responden }));
setBarChartData(barData);
}
@@ -140,7 +140,7 @@ function Kepuasan() {
if ((loading && !data) || !data) {
return (
<Stack py={10} px="xl">
<Stack py={10} px="sm">
<Skeleton height={300} mb="md" />
<SimpleGrid cols={{ base: 1, md: 3 }}>
<Skeleton height={300} />
@@ -154,11 +154,11 @@ function Kepuasan() {
if (data.length === 0) {
return (
<Stack p="sm">
<Container w={{ base: "100%", md: "80%" }} p={"xl"}>
<Container w={{ base: "100%", md: "80%" }} p={"sm"}>
<Center>
<Text ta={"center"} fz={{ base: "2.4rem", md: "3.4rem" }}>Indeks Kepuasan Masyarakat</Text>
<Text fw={"bold"} c={colors["blue-button"]} fz={{ base: "1.8rem", md: "3.4rem" }}>Indeks Kepuasan Masyarakat</Text>
</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>
<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}>
<Button
radius={"lg"}
@@ -168,7 +168,7 @@ function Kepuasan() {
>Ajukan Responden</Button>
</Center>
</Container>
<Box px={"xl"}>
<Box px={"sm"}>
<Paper p={"lg"} bg={colors.Bg}>
<Paper p={"lg"}>
<Stack gap={"xs"}>
@@ -185,7 +185,7 @@ function Kepuasan() {
h={window.innerWidth < 480 ? 200 : 300}
data={barChartData}
dataKey="month"
series={[{ name: 'count', color: colors['blue-button'] }]}
series={[{ name: 'Responden', color: colors['blue-button'] }]}
tickLine="y"
xAxisLabel="Bulan"
yAxisLabel="Jumlah Responden"
@@ -416,16 +416,16 @@ function Kepuasan() {
}
return (
<Stack p={"sm"}>
<Container size="lg" px="md">
<Container size="lg" px="sm">
<Center>
<Text ta={"center"} fz={{ base: "2.4rem", md: "3.4rem" }}>Indeks Kepuasan Masyarakat</Text>
<Text ta={"center"} fz={{ base: "2.4rem", md: "3.4rem" }} c={colors["blue-button"]}>Indeks Kepuasan Masyarakat</Text>
</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>
</Center>
</Container>
<Box px={"xl"}>
<Box px={"md"}>
<Paper p={"lg"} bg={colors.Bg}>
<Paper p={"lg"}>
<Stack gap={"xs"}>
@@ -448,7 +448,7 @@ function Kepuasan() {
h={300}
data={barChartData}
dataKey="month"
series={[{ name: 'count', color: colors['blue-button'] }]}
series={[{ name: 'Responden', color: colors['blue-button'] }]}
tickLine="y"
xAxisLabel="Bulan"
yAxisLabel="Jumlah Responden"
@@ -605,7 +605,7 @@ function Kepuasan() {
}}
/>
<TextInput
label="Tanggal"
label="Tanggal Pengisian"
type="date"
placeholder="masukkan tanggal"
defaultValue={state.create.form.tanggal}

View File

@@ -29,42 +29,42 @@ function ModuleItem({ data }: { data: ProgramInovasiItem }) {
return (
<motion.div whileHover={{ scale: 1.03 }}>
<Paper
onClick={() => router.push(`/darmasaba/program-inovasi/${data.id}`)}
p="lg"
radius="xl"
shadow="sm"
role="button"
tabIndex={0}
className="cursor-pointer transition-all"
bg={isDark ? "dark.6" : "white"}
>
<Center h={160}>
{data.image?.link ? (
<Image
src={data.image.link}
alt={data.name}
radius="md"
fit="cover"
h={140}
w="100%"
loading="lazy"
/>
) : (
<Stack align="center" gap="xs">
<IconPhotoOff size={38} stroke={1.5} />
<Text size="sm" c="dimmed">
Belum ada gambar
</Text>
</Stack>
)}
</Center>
<Box mt="md">
<Text fw={600} ta="center" size="md">
{data.name}
</Text>
</Box>
</Paper>
<Paper
onClick={() => router.push(`/darmasaba/program-inovasi/${data.id}`)}
p="lg"
radius="xl"
shadow="sm"
role="button"
tabIndex={0}
className="cursor-pointer transition-all"
bg={isDark ? "dark.6" : "white"}
>
<Center h={160}>
{data.image?.link ? (
<Image
src={data.image.link}
alt={data.name}
radius="md"
fit="contain"
h={140}
w="100%"
style={{ objectPosition: "center" }}
/>
) : (
<Stack align="center" gap="xs">
<IconPhotoOff size={38} stroke={1.5} />
<Text size="sm" c="dimmed">
Belum ada gambar
</Text>
</Stack>
)}
</Center>
<Box mt="md">
<Text fw={600} ta="center" size="md">
{data.name}
</Text>
</Box>
</Paper>
</motion.div>
);
}
@@ -110,11 +110,11 @@ function ModuleView() {
viewport: { paddingRight: 8 }, // kasih jarak biar scroll nggak dempet
}}
>
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="lg" mt="lg">
{listImageState.findMany.data?.map((item) => (
<ModuleItem key={item.id} data={item} />
))}
</SimpleGrid>
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="lg" mt="lg">
{listImageState.findMany.data?.map((item) => (
<ModuleItem key={item.id} data={item} />
))}
</SimpleGrid>
</ScrollArea>
);
}

View File

@@ -30,20 +30,41 @@ export default function ProfileView({ data }: ProfileViewProps) {
justify="end"
align="end"
pos="relative"
w={{ base: '100%', md: '40%' }}
px="xl"
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%
}}
px={{ base: 'md', sm: 'lg', md: 'xl', xl: '2xl' }}
h={{ base: 'auto', sm: '500px', md: '600px', lg: '650px', xl: '700px' }}
>
{data.image?.link ? (
<Image
src={data.image.link}
alt={data.name || 'Foto profil'}
fit="contain"
radius="lg"
loading="lazy"
<Box
pos="relative"
w="100%"
h="100%"
style={{
objectPosition: 'bottom center',
display: 'flex',
alignItems: 'flex-end',
justifyContent: 'center',
}}
/>
>
<Image
src={data.image.link}
alt={data.name || 'Foto profil'}
fit="contain"
radius="lg"
loading="lazy"
w="100%"
h="100%"
style={{
objectPosition: 'center bottom',
}}
/>
</Box>
) : (
<Stack align="center" gap="xs" w="100%" py="xl">
<IconUserCircle size={96} stroke={1.5} />
@@ -53,36 +74,47 @@ export default function ProfileView({ data }: ProfileViewProps) {
</Stack>
)}
{/* Box nama dan jabatan - sedikit overlap dengan gambar */}
{/* Box nama dan jabatan - responsive positioning */}
<Box
pos="absolute"
bottom={-20} // bikin naik sedikit ke gambar
bottom={{ base: -30, sm: -25, md: -20 }}
right={0}
w="100%"
p={{ base: 'xs', md: 'md' }}
style={{ pointerEvents: 'none' }} // biar ga ganggu klik di gambar
w={{ base: '95%', sm: '100%' }}
px={{ base: 'xs', sm: 'sm', md: 'md' }}
style={{ pointerEvents: 'none' }}
>
<Card
px="lg"
py="sm"
px={{ base: 'md', sm: 'lg' }}
py={{ base: 'xs', sm: 'sm' }}
radius="lg"
withBorder
style={{
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
backdropFilter: 'blur(6px)',
pointerEvents: 'auto',
backgroundColor: 'rgba(255, 255, 255, 0.95)',
}}
>
<Tooltip label="Jabatan Resmi" withArrow>
<Text fz="sm" c="dimmed">
<Text
fz={{ base: 'xs', sm: 'sm' }}
c="dimmed"
lineClamp={1}
>
{data.position || 'Tidak ada jabatan'}
</Text>
</Tooltip>
<Text c={colors['blue-button']} fw={700} fz="xl" mt={4}>
<Text
c={colors['blue-button']}
fw={700}
fz={{ base: 'lg', sm: 'xl' }}
mt={4}
lineClamp={2}
>
{data.name}
</Text>
</Card>
</Box>
</Stack>
);
}
}

View File

@@ -1,26 +1,27 @@
"use client";
import colors from "@/con/colors";
import { Prisma } from "@prisma/client";
import {
Badge,
Box,
Card,
Skeleton,
Center,
Flex,
Grid,
GridCol,
Group,
Image,
Paper,
Skeleton,
Stack,
Text,
Center,
Tooltip,
Badge,
} from "@mantine/core";
import { Prisma } from "@prisma/client";
import { IconCalendarTime, IconInfoCircle } from "@tabler/icons-react";
import { useEffect, useState } from "react";
import ModuleView from "./ModuleView";
import SosmedView from "./SosmedView";
import ProfileView from "./ProfileView";
import SosmedView from "./SosmedView";
const getDayOfWeek = () => {
const days = ["Minggu", "Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu"];
@@ -120,23 +121,21 @@ function LandingPage() {
}, []);
return (
<Stack bg={colors.Bg} p="md" gap="xl">
<Stack bg={colors.Bg} p="md" gap="lg">
<Flex gap="lg" wrap={{ base: "wrap", md: "nowrap" }}>
<Stack w={{ base: "100%", md: "65%" }} gap="lg">
<Card radius="xl" bg={colors.grey[1]} p="lg" shadow="xl">
<Stack gap="xl">
<Flex gap="md" wrap="wrap">
<Group>
<Box bg="white" w={72} h={72} p="sm" style={{ borderRadius: 24 }}>
<Image loading="lazy" src="/darmasaba-icon.png" alt="Logo Darmasaba" fit="contain" />
</Box>
<Box bg="white" w={72} h={72} p="sm" style={{ borderRadius: 24 }}>
<Image loading="lazy" src="/pudak-icon.png" alt="Logo Pudak" fit="contain" />
</Box>
</Group>
<Grid w="100%">
<Grid.Col span={{ base: 3, sm: 2 }}>
<Box bg="white" w={72} h={72} p="sm" style={{ borderRadius: 24 }}>
<Image loading="lazy" src="/darmasaba-icon.png" alt="Logo Darmasaba" fit="contain" />
</Box>
</Grid.Col>
<Grid.Col span={{ base: 9, sm: 10 }}>
<Box bg="white" w={72} h={72} p="sm" style={{ borderRadius: 24 }}>
<Image loading="lazy" src="/pudak-icon.png" alt="Logo Pudak" fit="contain" />
</Box>
</Grid.Col>
<Grid.Col span={12}>
<Paper
bg={colors["blue-button"]}
@@ -199,7 +198,7 @@ function LandingPage() {
)}
<Text ta="center" c={colors.trans.dark[2]}>
Bagikan ide, kritik, atau saran Anda untuk mendukung pembangunan desa.
Bagikan ide, kritik, atau saran Anda untuk mendukung pembangunan desa.
Semua lebih mudah dengan fitur interaktif yang kami sediakan.
</Text>
</Stack>

View File

@@ -30,17 +30,13 @@ const textHeading = {
function Layanan() {
return (
<Stack pos={"relative"} bg={colors.grey[1]} gap={"42"} py={"xl"}>
<Container w={{ base: "100%", md: "50%" }} p={"xl"}>
<Stack pos={"relative"} bg={colors.grey[1]} gap={"xl"} py={"md"}>
<Container w={{ base: "100%", md: "80%" }} p={"md"} >
<Stack align="center" gap={"0"}>
<Text fz={"3.4rem"} fw={"bold"}>
<Text fw={"bold"} c={colors["blue-button"]} fz={{ base: "1.8rem", md: "3.4rem" }}>
{textHeading.title}
</Text>
<Text
style={{
textAlign: "center",
}}
>
<Text ta={"center"} fz={{ base: "1rem", md: "1.3rem" }}>
{textHeading.des}
</Text>
<Box p={"md"}>

View File

@@ -6,6 +6,7 @@ import {
BackgroundImage,
Box,
Button,
Container,
Divider,
Group,
Loader,
@@ -48,15 +49,15 @@ function Potensi() {
const data = (state.findMany.data || []).slice(0, 4);
return (
<Stack p="sm" gap="4rem">
<Box>
<Text ta={"center"} fz={{ base: "2.4rem", md: "3.4rem" }} fw={700} c={colors["blue-button"]}>
<Stack p="sm" gap="xl">
<Container w={{ base: "100%", md: "80%" }} p={"md"} >
<Text ta={"center"} fw={"bold"} c={colors["blue-button"]} fz={{ base: "1.8rem", md: "3.4rem" }}>
{textHeading.title}
</Text>
<Text ta={"center"} fz={{ base: "1.4rem", md: "1.6rem" }} c="black">
<Text ta={"center"} fz={{ base: "1rem", md: "1.3rem" }}>
{textHeading.des}
</Text>
</Box>
</Container>
{loading ? (
<Stack align="center" justify="center" h={300}>

View File

@@ -35,7 +35,7 @@ function Prestasi() {
<Stack align="center" gap="sm">
<Group gap="xs">
<IconTrophy size={36} color={colors["blue-button"]} />
<Text ta="center" fz={{ base: "2rem", md: "3.4rem" }} fw={700}>
<Text c={colors["blue-button"]} ta="center" fz={{ base: "2rem", md: "3.4rem" }} fw={700}>
Prestasi Desa
</Text>
</Group>
@@ -55,7 +55,7 @@ function Prestasi() {
</Stack>
</Container>
<Box py={50}>
<Box py="lg">
{loading ? (
<Center mih={200}>
<Loader color={colors["blue-button"]} size="xl" />

View File

@@ -5,6 +5,7 @@ import { useMediaQuery } from "@mantine/hooks"
import { Prisma } from "@prisma/client"
import Link from "next/link"
import { IconMoodSad } from "@tabler/icons-react"
import colors from "@/con/colors"
export default function SDGS() {
const theme = useMantineTheme()
@@ -41,20 +42,16 @@ export default function SDGS() {
order={1}
fz={{ base: "2.4rem", md: "3.6rem" }}
fw={900}
style={{
background: "linear-gradient(90deg, #1A5F7A, #159895)",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
}}
c={colors["blue-button"]}
>
SDGs Desa
</Title>
</Center>
<Text fz={{ base: "1rem", md: "1.2rem" }} ta="center" c="dimmed" mt="md" maw={820} mx="auto">
<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>
<Box py={50}>
<Box py="lg">
<Paper
p={{ base: "md", md: "xl" }}
radius="2xl"
@@ -156,7 +153,7 @@ export default function SDGS() {
href="/darmasaba/sdgs-desa"
radius="xl"
size="lg"
mt={40}
mt="md"
variant="gradient"
gradient={{ from: "#26667F", to: "#124170" }}
style={{ boxShadow: "0 6px 14px rgba(18,65,112,0.25)"}}

View File

@@ -1,91 +1,92 @@
const getDetailUrl = (item: { type?: string; id: string | number; [key: string]: unknown }) => {
const { type, id, kategori } = item;
const typeUrlMap: Record<string, string> = {
programinovasi: `/darmasaba/program-inovasi/${id}`,
desaantikorupsi: '/darmasaba/desa-anti-korupsi',
sdgsdesa: '/darmasaba/sdgs-desa',
apbdes: '/darmasaba/apbdes',
prestasidesa: '/darmasaba/prestasi-desa',
pejabatdesa: '/darmasaba/profile/pejabat-desa',
strukturppid: '/darmasaba/ppid/struktur-ppid',
visimisippid: '/darmasaba/ppid/visi-misi',
dasarhukumppid: '/darmasaba/ppid/dasar-hukum',
profileppid: '/darmasaba/ppid/profile',
daftarinformasipublik: '/darmasaba/ppid/daftar-informasi-publik',
perbekeldarmasaba: '/darmasaba/desa/profile',
berita: `/darmasaba/desa/berita/${kategori}/${id}`,
pengumuman: `/darmasaba/desa/pengumuman/${kategori}/${id}`,
sejarahdesa: '/darmasaba/desa/profile',
visimisidesa: '/darmasaba/desa/profile',
lambangdesa: '/darmasaba/desa/profile',
maskotdesa: '/darmasaba/desa/profile',
profilperbekel: '/darmasaba/desa/profile',
potensi: '/darmasaba/desa/potensi-desa',
galleryFoto: '/darmasaba/desa/gallery/foto',
galleryVideo: '/darmasaba/desa/gallery/video',
pelayananSuratKeterangan: '/darmasaba/desa/layanan',
pelayananPerizinanBerusaha: '/darmasaba/desa/layanan',
pelayananTelunjukSaktiDesa: '/darmasaba/desa/layanan',
pelayananPendudukNonPermanent: '/darmasaba/desa/layanan',
penghargaan: '/darmasaba/desa/penghargaan',
posyandu: '/darmasaba/kesehatan/posyandu',
fasilitasKesehatan: '/darmasaba/kesehatan/data-kesehatan-warga',
jadwalKegiatan: '/darmasaba/kesehatan/data-kesehatan-warga',
artikelKesehatan: '/darmasaba/kesehatan/data-kesehatan-warga',
puskesmas: '/darmasaba/kesehatan/puskesmas',
programKesehatan: '/darmasaba/kesehatan/program-kesehatan',
penangananDarurat: '/darmasaba/kesehatan/penanganan-darurat',
kontakDarurat: '/darmasaba/kesehatan/kontak-darurat',
infoWabahPenyakit: '/darmasaba/kesehatan/info-wabah-penyakit',
keamananLingkungan: '/darmasaba/keamanan/keamanan-lingkungan-pecalang-patwal',
polsekTerdekat: '/darmasaba/keamanan/polsek-terdekat',
kontakDaruratKeamanan: '/darmasaba/keamanan/kontak-darurat',
pencegahanKriminalitas: '/darmasaba/keamanan/pencegahan-kriminalitas',
laporanPublik: '/darmasaba/keamanan/laporan-publik',
tipsKeamanan: '/darmasaba/keamanan/tips-keamanan',
pasarDesa: '/darmasaba/ekonomi/pasar-desa',
lowonganKerjaLokal: '/darmasaba/ekonomi/lowongan-kerja-lokal',
strukturOrganisasi: '/darmasaba/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa',
jumlahPendudukUsiaKerjaYangMenganggurUsia: '/darmasaba/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur',
jumlahPendudukUsiaKerjaYangMenganggurPendidikan: '/darmasaba/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur',
jumlahPendudukMiskin: '/darmasaba/ekonomi/jumlah-penduduk-miskin',
programKemiskinan: '/darmasaba/ekonomi/program-kemiskinan',
sektorUnggulanDesa: '/darmasaba/ekonomi/sektor-unggulan-desa',
demografiPekerjaan: '/darmasaba/ekonomi/demografi-pekerjaan',
desaDigital: '/darmasaba/inovasi/desa-digital-smart-village',
programKreatif: '/darmasaba/inovasi/program-kreatif-desa',
kolaborasiInovasi: '/darmasaba/inovasi/kolaborasi-inovasi',
mitraKolaborasi: '/darmasaba/inovasi/kolaborasi-inovasi',
infoTekno: '/darmasaba/inovasi/info-teknologi-tepat-guna',
pengelolaanSampah: '/darmasaba/lingkungan/pengelolaan-sampah-bank-sampah',
keteranganBankSampahTerdekat: '/darmasaba/lingkungan/pengelolaan-sampah-bank-sampah',
programPenghijauan: '/darmasaba/lingkungan/program-penghijauan',
dataLingkunganDesa: '/darmasaba/lingkungan/data-lingkungan-desa',
gotongRoyong: '/darmasaba/lingkungan/gotong-royong',
tujuanEdukasiLingkungan: '/darmasaba/lingkungan/edukasi-lingkungan',
materiEdukasiLingkungan: '/darmasaba/lingkungan/edukasi-lingkungan',
contohEdukasiLingkungan: '/darmasaba/lingkungan/edukasi-lingkungan',
filosofiTriHita: '/darmasaba/lingkungan/konservasi-adat-bali',
bentukKonservasiBerdasarkanAdat: '/darmasaba/lingkungan/konservasi-adat-bali',
nilaiKonservasiAdat: '/darmasaba/lingkungan/konservasi-adat-bali',
jenjangPendidikan: '/darmasaba/pendidikan/info-sekolah/semua',
lembaga: '/darmasaba/pendidikan/info-sekolah/semua/lembaga',
siswa: '/darmasaba/pendidikan/info-sekolah/semua/siswa',
pengajar: '/darmasaba/pendidikan/info-sekolah/semua/pengajar',
keunggulanProgram: '/darmasaba/pendidikan/beasiswa-desa',
tujuanProgram: '/darmasaba/pendidikan/program-pendidikan-anak',
programUnggulan: '/darmasaba/pendidikan/program-pendidikan-anak',
lokasiJadwalBimbinganBelajarDesa: '/darmasaba/pendidikan/bimbingan-belajar-desa',
fasilitasBimbinganBelajarDesa: '/darmasaba/pendidikan/bimbingan-belajar-desa',
tujuanPendidikanNonFormal: '/darmasaba/pendidikan/pendidikan-non-formal',
tempatKegiatan: '/darmasaba/pendidikan/pendidikan-non-formal',
jenisProgramYangDiselenggarakan: '/darmasaba/pendidikan/pendidikan-non-formal',
dataPerpustakaan: '/darmasaba/pendidikan/perpustakaan-digital/semua',
dataPendidikan: '/darmasaba/pendidikan/data-pendidikan',
const map: Record<string, (id: string | number, kategori?: string) => string> = {
programinovasi: (id) => `/darmasaba/program-inovasi/${id}`,
desaantikorupsi: () => '/darmasaba/desa-anti-korupsi',
sdgsdesa: () => '/darmasaba/sdgs-desa',
apbdes: () => '/darmasaba/apbdes',
prestasidesa: () => '/darmasaba/prestasi-desa',
pejabatdesa: () => '/darmasaba/ppid/profile-ppid',
strukturppid: () => '/darmasaba/ppid/struktur-ppid',
visimisippid: () => '/darmasaba/ppid/visi-misi',
dasarhukumppid: () => '/darmasaba/ppid/dasar-hukum',
profileppid: () => '/darmasaba/ppid/profile',
daftarinformasipublik: () => '/darmasaba/ppid/daftar-informasi-publik',
perbekeldarmasaba: () => '/darmasaba/desa/profile',
berita: (id, kategori) => `/darmasaba/desa/berita/${kategori}/${id}`,
pengumuman: (id, kategori) => `/darmasaba/desa/pengumuman/${kategori}/${id}`,
sejarahdesa: () => '/darmasaba/desa/profile',
visimisidesa: () => '/darmasaba/desa/profile',
lambangdesa: () => '/darmasaba/desa/profile',
maskotdesa: () => '/darmasaba/desa/profile',
profilperbekel: () => '/darmasaba/desa/profile',
potensi: () => '/darmasaba/desa/potensi-desa',
galleryFoto: () => '/darmasaba/desa/gallery/foto',
galleryVideo: () => '/darmasaba/desa/gallery/video',
pelayananSuratKeterangan: () => '/darmasaba/desa/layanan',
pelayananPerizinanBerusaha: () => '/darmasaba/desa/layanan',
pelayananTelunjukSaktiDesa: () => '/darmasaba/desa/layanan',
pelayananPendudukNonPermanent: () => '/darmasaba/desa/layanan',
penghargaan: () => '/darmasaba/desa/penghargaan',
posyandu: (id) => `/darmasaba/kesehatan/posyandu/${id}`,
fasilitasKesehatan: () => '/darmasaba/kesehatan/data-kesehatan-warga',
jadwalKegiatan: () => '/darmasaba/kesehatan/data-kesehatan-warga',
artikelKesehatan: () => '/darmasaba/kesehatan/data-kesehatan-warga',
puskesmas: () => '/darmasaba/kesehatan/puskesmas',
programKesehatan: () => '/darmasaba/kesehatan/program-kesehatan',
penangananDarurat: () => '/darmasaba/kesehatan/penanganan-darurat',
kontakDarurat: () => '/darmasaba/kesehatan/kontak-darurat',
infoWabahPenyakit: () => '/darmasaba/kesehatan/info-wabah-penyakit',
keamananLingkungan: () => '/darmasaba/keamanan/keamanan-lingkungan-pecalang-patwal',
polsekTerdekat: () => '/darmasaba/keamanan/polsek-terdekat',
kontakDaruratKeamanan: () => '/darmasaba/keamanan/kontak-darurat',
pencegahanKriminalitas: () => '/darmasaba/keamanan/pencegahan-kriminalitas',
laporanPublik: () => '/darmasaba/keamanan/laporan-publik',
tipsKeamanan: () => '/darmasaba/keamanan/tips-keamanan',
pasarDesa: () => '/darmasaba/ekonomi/pasar-desa',
lowonganKerjaLokal: () => '/darmasaba/ekonomi/lowongan-kerja-lokal',
strukturOrganisasi: () => '/darmasaba/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa',
jumlahPendudukUsiaKerjaYangMenganggurUsia: () => '/darmasaba/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur',
jumlahPendudukUsiaKerjaYangMenganggurPendidikan: () => '/darmasaba/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur',
jumlahPendudukMiskin: () => '/darmasaba/ekonomi/jumlah-penduduk-miskin',
programKemiskinan: () => '/darmasaba/ekonomi/program-kemiskinan',
sektorUnggulanDesa: () => '/darmasaba/ekonomi/sektor-unggulan-desa',
demografiPekerjaan: () => '/darmasaba/ekonomi/demografi-pekerjaan',
desaDigital: () => '/darmasaba/inovasi/desa-digital-smart-village',
programKreatif: () => '/darmasaba/inovasi/program-kreatif-desa',
kolaborasiInovasi: () => '/darmasaba/inovasi/kolaborasi-inovasi',
mitraKolaborasi: () => '/darmasaba/inovasi/kolaborasi-inovasi',
infoTekno: () => '/darmasaba/inovasi/info-teknologi-tepat-guna',
pengelolaanSampah: () => '/darmasaba/lingkungan/pengelolaan-sampah-bank-sampah',
keteranganBankSampahTerdekat: () => '/darmasaba/lingkungan/pengelolaan-sampah-bank-sampah',
programPenghijauan: () => '/darmasaba/lingkungan/program-penghijauan',
dataLingkunganDesa: () => '/darmasaba/lingkungan/data-lingkungan-desa',
gotongRoyong: (id, kategori) => `/darmasaba/lingkungan/gotong-royong/${kategori}/${id}`,
tujuanEdukasiLingkungan: () => '/darmasaba/lingkungan/edukasi-lingkungan',
materiEdukasiLingkungan: () => '/darmasaba/lingkungan/edukasi-lingkungan',
contohEdukasiLingkungan: () => '/darmasaba/lingkungan/edukasi-lingkungan',
filosofiTriHita: () => '/darmasaba/lingkungan/konservasi-adat-bali',
bentukKonservasiBerdasarkanAdat: () => '/darmasaba/lingkungan/konservasi-adat-bali',
nilaiKonservasiAdat: () => '/darmasaba/lingkungan/konservasi-adat-bali',
jenjangPendidikan: () => '/darmasaba/pendidikan/info-sekolah/semua',
lembaga: () => '/darmasaba/pendidikan/info-sekolah/semua/lembaga',
siswa: () => '/darmasaba/pendidikan/info-sekolah/semua/siswa',
pengajar: () => '/darmasaba/pendidikan/info-sekolah/semua/pengajar',
keunggulanProgram: () => '/darmasaba/pendidikan/beasiswa-desa',
tujuanProgram: () => '/darmasaba/pendidikan/program-pendidikan-anak',
programUnggulan: () => '/darmasaba/pendidikan/program-pendidikan-anak',
lokasiJadwalBimbinganBelajarDesa: () => '/darmasaba/pendidikan/bimbingan-belajar-desa',
fasilitasBimbinganBelajarDesa: () => '/darmasaba/pendidikan/bimbingan-belajar-desa',
tujuanPendidikanNonFormal: () => '/darmasaba/pendidikan/pendidikan-non-formal',
tempatKegiatan: () => '/darmasaba/pendidikan/pendidikan-non-formal',
jenisProgramYangDiselenggarakan: () => '/darmasaba/pendidikan/pendidikan-non-formal',
dataPerpustakaan: () => '/darmasaba/pendidikan/perpustakaan-digital/semua',
dataPendidikan: () => '/darmasaba/pendidikan/data-pendidikan',
};
return typeUrlMap[type || ''] || '/darmasaba';
if (type && map[type]) return map[type](id, kategori as string | undefined);
return '/darmasaba';
};
export default getDetailUrl;

View File

@@ -17,7 +17,10 @@ import ScrollToTopButton from "./_com/scrollToTopButton";
export default function Page() {
return (
<Box>
<Stack bg={colors.grey[1]} gap={"4rem"}>
<Stack
bg={colors.grey[1]}
gap={"1.5rem"}
>
<LandingPage />
<Penghargaan />
<Layanan />

View File

@@ -1,6 +1,14 @@
const colors = {
"orange": "#FCAE00",
"blue-button": "#0A4E78",
"blue-button-1": "#E5F2FA",
"blue-button-2": "#B8DAEF",
"blue-button-3": "#8AC1E3",
"blue-button-4": "#5DA9D8",
"blue-button-5": "#2F91CC",
"blue-button-6": "#083F61",
"blue-button-7": "#062F49",
"blue-button-8": "#041F32",
"blue-button-trans": "#628EC6",
"white-1": "#FBFBFC",
"white-trans-1": "rgba(255, 255, 255, 0.5)",