Merge pull request 'nico/16-des-25' (#42) from nico/16-des-25 into staggingweb

Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/42
This commit is contained in:
2025-12-16 16:38:42 +08:00
73 changed files with 3167 additions and 1999 deletions

View File

@@ -1,14 +1,15 @@
module.exports = {
plugins: {
'postcss-preset-mantine': {},
'postcss-simple-vars': {
variables: {
'mantine-breakpoint-xs': '36em',
'mantine-breakpoint-sm': '48em',
'mantine-breakpoint-md': '62em',
'mantine-breakpoint-lg': '75em',
'mantine-breakpoint-xl': '88em',
},
plugins: {
'postcss-preset-mantine': {},
'postcss-simple-vars': {
variables: {
/* Mobile first */
'mantine-breakpoint-xs': '30em', // 480px → mobile kecilnormal
'mantine-breakpoint-sm': '48em', // 768px → tablet / mobile landscape
'mantine-breakpoint-md': '64em', // 1024px → laptop & desktop kecil
'mantine-breakpoint-lg': '80em', // 1280px → desktop standar
'mantine-breakpoint-xl': '90em', // 1440px+ → desktop besar
},
},
};
},
};

View File

@@ -44,18 +44,56 @@ function CreatePolsekTerdekat() {
};
};
const isValidGoogleMapsEmbed = (url: string): boolean => {
try {
const u = new URL(url);
return (
u.hostname === 'www.google.com' &&
u.pathname === '/maps/embed' &&
u.searchParams.has('pb')
);
} catch {
return false;
}
};
const handleSubmit = async () => {
try {
setIsSubmitting(true);
await polsekState.create.create();
resetForm();
router.push("/admin/keamanan/polsek-terdekat");
} catch (error) {
console.error(error)
toast.error("Gagal menambah polsek terdekat");
} finally {
setIsSubmitting(false);
const { embedMapUrl } = polsekState.create.form;
// ✅ Validasi Google Maps Embed URL (jika diisi)
if (embedMapUrl && !isValidGoogleMapsEmbed(embedMapUrl)) {
toast.error("URL embed peta tidak valid. Harap paste iframe dari Google Maps.");
return;
}
try {
setIsSubmitting(true);
await polsekState.create.create();
resetForm();
router.push("/admin/keamanan/polsek-terdekat");
} catch (error) {
console.error(error);
toast.error("Gagal menambah polsek terdekat");
} finally {
setIsSubmitting(false);
}
};
const extractEmbedUrl = (input: string): string => {
// Jika sudah berupa URL embed yang valid
if (input.startsWith('https://www.google.com/maps/embed?')) {
return input.trim();
}
// Coba parse sebagai HTML string (iframe)
const iframeRegex = /<iframe[^>]*src=["']([^"']*)["'][^>]*>/i;
const match = input.match(iframeRegex);
if (match && match[1]?.startsWith('https://www.google.com/maps/embed?')) {
return match[1].trim();
}
// Jika tidak cocok, kembalikan input asli (atau string kosong)
return input.trim();
};
const fetchLayanan = async () => {
@@ -190,9 +228,14 @@ function CreatePolsekTerdekat() {
/>
<TextInput
value={polsekState.create.form.embedMapUrl}
onChange={(val) => (polsekState.create.form.embedMapUrl = val.target.value)}
onChange={(e) => {
const rawValue = e.currentTarget.value;
const cleanUrl = extractEmbedUrl(rawValue);
polsekState.create.form.embedMapUrl = cleanUrl;
}}
description="Contoh: https://www.google.com/maps/embed?pb=..."
label={<Text fw="bold" fz="sm">Embed Map URL</Text>}
placeholder="Masukkan embed map url"
placeholder="Paste iframe dari Google Maps atau URL embed langsung"
/>
<TextInput
value={polsekState.create.form.namaTempatMaps}

View File

@@ -123,7 +123,7 @@ export default function EditKolaborasiInovasi() {
};
return (
<Box px={{ base: "sm", md: "lg" }} py="md">
<Box px={{ base: 0, md: 'lg' }} py="xs">
<Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors["blue-button"]} size={24} />

View File

@@ -42,7 +42,7 @@ function DetailSDGSDesa() {
const data = sdgsState.findUnique.data;
return (
<Box py={10}>
<Box px={{ base: 0, md: 'xs' }} py="xs">
<Button
variant="subtle"
onClick={() => router.back()}
@@ -54,7 +54,7 @@ function DetailSDGSDesa() {
<Paper
withBorder
w={{ base: '100%', md: '60%' }}
w={{ base: '100%', md: '70%' }}
bg={colors['white-1']}
p="lg"
radius="md"

View File

@@ -65,7 +65,7 @@ function CreateSDGsDesa() {
}
}
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box px={{ base: 0, md: 'lg' }} py="xs">
<Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />

View File

@@ -9,7 +9,6 @@ import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import sdgsDesa from '../../_state/landing-page/sdgs-desa';
function SdgsDesa() {
const [search, setSearch] = useState('');
return (
@@ -27,7 +26,7 @@ function SdgsDesa() {
}
function ListSdgsDesa({ search }: { search: string }) {
const listState = useProxy(sdgsDesa)
const listState = useProxy(sdgsDesa);
const router = useRouter();
const {
@@ -39,10 +38,10 @@ function ListSdgsDesa({ search }: { search: string }) {
} = listState.findMany;
useShallowEffect(() => {
load(page, 10, search)
}, [page, search])
load(page, 10, search);
}, [page, search]);
const filteredData = data || []
const filteredData = data || [];
// Handle loading state
if (loading || !data) {
@@ -53,79 +52,71 @@ function ListSdgsDesa({ search }: { search: string }) {
);
}
if (data.length === 0) {
return (
<Box py={10}>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Sdgs Desa</Title>
<Button
leftSection={<IconPlus size={18} />}
color={colors['blue-button']}
variant="light"
onClick={() => router.push('/admin/landing-page/SDGs/create')}
>
Tambah Baru
</Button>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover striped verticalSpacing="sm">
<TableThead>
<TableTr>
<TableTh style={{ width: '60%' }}>Nama Sdgs Desa</TableTh>
<TableTh style={{ width: '20%' }}>Jumlah</TableTh>
<TableTh style={{ width: '20%', textAlign: 'center' }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd colSpan={3} style={{ textAlign: 'center', padding: '2rem' }}>
<Text c="dimmed">Tidak ada data Sdgs Desa</Text>
</TableTd>
</TableTr>
</TableTbody>
</Table>
</Box>
</Paper>
</Box>
);
}
const isEmpty = data.length === 0;
return (
<Box py={10}>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Sdgs Desa</Title>
<Button leftSection={<IconPlus size={18} />} color="blue" variant="light"
onClick={() => router.push('/admin/landing-page/SDGs/create')}
>
Tambah Baru
</Button>
<Box py={{ base: 'sm', md: 'lg' }}>
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
<Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
<Title order={2} lh={1.2}>
Daftar Sdgs Desa
</Title>
<Button
leftSection={<IconPlus size={18} />}
color={colors['blue-button']}
variant="light"
onClick={() => router.push('/admin/landing-page/SDGs/create')}
>
Tambah Baru
</Button>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
{/* Desktop Table */}
<Box visibleFrom="md">
<Table highlightOnHover striped verticalSpacing="sm">
<TableThead>
<TableTr>
<TableTh style={{ width: '60%' }}>Nama Sdgs Desa</TableTh>
<TableTh style={{ width: '20%' }}>Jumlah</TableTh>
<TableTh style={{ width: '20%', textAlign: 'center' }}>Aksi</TableTh>
<TableTh style={{ width: '60%' }}>
<Text fz="sm" fw={600} c="dark.7" ta="left">
Nama Sdgs Desa
</Text>
</TableTh>
<TableTh style={{ width: '20%' }}>
<Text fz="sm" fw={600} c="dark.7" ta="left">
Jumlah
</Text>
</TableTh>
<TableTh style={{ width: '20%' }}>
<Text fz="sm" fw={600} c="dark.7" ta="center">
Aksi
</Text>
</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd style={{ width: '60%' }}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name}
{isEmpty ? (
<TableTr>
<TableTd colSpan={3} ta="center" py="xl">
<Text c="dimmed" fz="sm" lh={1.5}>
Tidak ada data Sdgs Desa
</Text>
</TableTd>
<TableTd style={{ width: '20%' }}>
<Text fz="sm" c="dimmed">
{item.jumlah || '0'}
</Text>
</TableTd>
<TableTd style={{ width: '20%', textAlign: 'center' }}>
<Button
</TableTr>
) : (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd style={{ width: '60%' }}>
<Text fz="md" fw={500} truncate="end" lineClamp={1} lh={1.5}>
{item.name}
</Text>
</TableTd>
<TableTd style={{ width: '20%' }}>
<Text fz="sm" c="dark.6" lh={1.5}>
{item.jumlah || '0'}
</Text>
</TableTd>
<TableTd style={{ width: '20%' }} ta="center">
<Button
size="xs"
radius="md"
variant="light"
@@ -135,27 +126,69 @@ function ListSdgsDesa({ search }: { search: string }) {
>
Detail
</Button>
</TableTd>
</TableTr>
))}
</TableTd>
</TableTr>
))
)}
</TableTbody>
</Table>
</Box>
{/* Mobile Cards */}
<Box hiddenFrom="md">
{isEmpty ? (
<Center py="xl">
<Text c="dimmed" fz="sm" lh={1.5} ta="center">
Tidak ada data Sdgs Desa
</Text>
</Center>
) : (
<Stack gap="sm">
{filteredData.map((item) => (
<Paper key={item.id} withBorder p="md" radius="md">
<Stack gap={4}>
<Text fz="sm" fw={600} lh={1.4}>
{item.name}
</Text>
<Text fz="xs" c="dark.6" lh={1.4}>
Jumlah: {item.jumlah || '0'}
</Text>
<Group justify="flex-end" mt="xs">
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() => router.push(`/admin/landing-page/SDGs/${item.id}`)}
>
Detail
</Button>
</Group>
</Stack>
</Paper>
))}
</Stack>
)}
</Box>
</Paper>
<Center mt="lg">
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={Math.max(1, totalPages)}
withEdges
radius="md"
/>
</Center>
{!isEmpty && (
<Center mt={{ base: 'md', md: 'lg' }}>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={Math.max(1, totalPages)}
withEdges
radius="md"
/>
</Center>
)}
</Box>
)
);
}
export default SdgsDesa;
export default SdgsDesa;

View File

@@ -204,7 +204,7 @@ function EditAPBDes() {
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box px={{ base: 0, md: 'lg' }} py="xs">
<Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
@@ -215,7 +215,7 @@ function EditAPBDes() {
</Group>
<Paper
w={{ base: '100%', md: '100%' }}
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"

View File

@@ -65,7 +65,7 @@ function DetailAPBDes() {
});
return (
<Box py={10}>
<Box px={{ base: 0, md: 'xs' }} py="xs">
<Button
variant="subtle"
onClick={() => router.back()}
@@ -77,7 +77,7 @@ function DetailAPBDes() {
<Paper
withBorder
w={{ base: '100%', md: '100%' }}
w={{ base: '100%', md: '70%' }}
bg={colors['white-1']}
p="lg"
radius="md"

View File

@@ -155,7 +155,7 @@ function CreateAPBDes() {
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box px={{ base: 0, md: 'lg' }} py="xs">
<Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />

View File

@@ -56,71 +56,85 @@ function ListAPBDes({ search }: { search: string }) {
if (loading || !data) {
return (
<Stack py={10}>
<Stack py={{ base: 'md', md: 'lg' }}>
<Skeleton height={600} radius="md" />
</Stack>
);
}
return (
<Box py={10}>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar APBDes</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/landing-page/apbdes/create')}
>
Tambah Baru
</Button>
</Group>
<Box py={{ base: 'md', md: 'lg' }}>
{/* Desktop Table */}
<Box visibleFrom="md">
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={2} size="lg" lh={1.2}>
Daftar APBDes
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/landing-page/apbdes/create')}
>
Tambah Baru
</Button>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '25%' }}>APBDes</TableTh>
<TableTh style={{ width: '25%' }}>Tahun</TableTh>
<TableTh style={{ width: '25%' }}>Dokumen</TableTh>
<TableTh style={{ width: '25%' }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd style={{ width: '25%' }}>
<Text fw={500} lineClamp={1}>
APBDes {item.tahun}
</Text>
</TableTd>
<TableTd style={{ width: '25%' }}>
<Text fw={500}>{item.tahun || '-'}</Text>
</TableTd>
<TableTd style={{ width: '25%' }}>
{item.file?.link ? (
<Button
component="a"
href={item.file.link}
target="_blank"
rel="noopener noreferrer"
variant="light"
leftSection={<IconFile size={16} />}
size="xs"
radius="sm"
>
Lihat Dokumen
</Button>
) : (
<Text c="dimmed" fz="sm">
Tidak ada dokumen
<Box>
<Table highlightOnHover miw={0}>
<TableThead>
<TableTr>
<TableTh fz="md" fw={600} ta="left" w="25%">
APBDes
</TableTh>
<TableTh fz="md" fw={600} ta="left" w="25%">
Tahun
</TableTh>
<TableTh fz="md" fw={600} ta="left" w="25%">
Dokumen
</TableTh>
<TableTh fz="md" fw={600} ta="left" w="25%">
Aksi
</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text fz="md" fw={500} lh={1.5} lineClamp={1}>
APBDes {item.tahun}
</Text>
)}
</TableTd>
<TableTd style={{ width: '25%' }}>
<Box w={100}>
</TableTd>
<TableTd>
<Text fz="md" fw={500} lh={1.5}>
{item.tahun || '-'}
</Text>
</TableTd>
<TableTd>
{item.file?.link ? (
<Button
component="a"
href={item.file.link}
target="_blank"
rel="noopener noreferrer"
variant="light"
leftSection={<IconFile size={16} />}
size="xs"
radius="sm"
fz="sm"
>
Lihat Dokumen
</Button>
) : (
<Text c="dimmed" fz="sm" lh={1.5}>
Tidak ada dokumen
</Text>
)}
</TableTd>
<TableTd>
<Button
size="xs"
radius="md"
@@ -128,29 +142,126 @@ function ListAPBDes({ search }: { search: string }) {
color="blue"
leftSection={<IconDeviceImacCog size={14} />}
onClick={() => router.push(`/admin/landing-page/apbdes/${item.id}`)}
fullWidth
fz="sm"
>
Detail
</Button>
</Box>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py="lg">
<Text c="dimmed" fz="sm" lh={1.5}>
Tidak ada data APBDes yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={5}>
<Center py={20}>
<Text color="dimmed">Tidak ada data APBDes yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper>
)}
</TableTbody>
</Table>
</Box>
</Paper>
</Box>
<Center mt="md">
{/* Mobile Cards */}
<Box hiddenFrom="md">
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={2} size="lg" lh={1.2}>
Daftar APBDes
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/landing-page/apbdes/create')}
>
Tambah Baru
</Button>
</Group>
<Stack gap="md">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper
key={item.id}
withBorder
bg={colors['white-1']}
p="md"
shadow="sm"
radius="md"
>
<Stack gap="xs">
<Text fz="sm" fw={600} lh={1.4}>
APBDes {item.tahun}
</Text>
<Group justify="space-between" wrap="nowrap">
<Text fz="sm" c="dimmed" lh={1.4}>
Tahun
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.tahun || '-'}
</Text>
</Group>
<Group justify="space-between" wrap="nowrap">
<Text fz="sm" c="dimmed" lh={1.4}>
Dokumen
</Text>
{item.file?.link ? (
<Button
component="a"
href={item.file.link}
target="_blank"
rel="noopener noreferrer"
variant="light"
leftSection={<IconFile size={14} />}
size="xs"
radius="sm"
fz="xs"
lh={1.4}
>
Lihat
</Button>
) : (
<Text fz="xs" c="dimmed" lh={1.4}>
Tidak ada
</Text>
)}
</Group>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={14} />}
onClick={() => router.push(`/admin/landing-page/apbdes/${item.id}`)}
mt="sm"
fz="xs"
lh={1.4}
>
Detail
</Button>
</Stack>
</Paper>
))
) : (
<Paper withBorder bg={colors['white-1']} p="md" radius="md">
<Center py="lg">
<Text c="dimmed" fz="xs" lh={1.4}>
Tidak ada data APBDes yang cocok
</Text>
</Center>
</Paper>
)}
</Stack>
</Paper>
</Box>
<Center mt="xl">
<Pagination
value={page}
onChange={(newPage) => {

View File

@@ -3,6 +3,7 @@
import colors from "@/con/colors";
import {
Box,
ScrollArea,
Stack,
Tabs,
@@ -68,37 +69,76 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
keepMounted={false}
>
{/* ✅ Scroll horizontal wrapper */}
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}}
>
{tabs.map((tab, i) => (
<TabsTab
key={i}
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
flexShrink: 0, // ✅ mencegah tab mengecil
}}
>
{tab.label}
</TabsTab>
))}
</TabsList>
</ScrollArea>
<Box visibleFrom='md' pb={10}>
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}}
>
{tabs.map((tab, i) => (
<TabsTab
key={i}
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
}}
>
{tab.label}
</TabsTab>
))}
</TabsList>
</ScrollArea>
</Box>
<Box hiddenFrom='md' pb={10}>
<ScrollArea
type="auto"
offsetScrollbars={false}
w="100%"
>
<TabsList
p="xs" // lebih kecil
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
width: "max-content", // ⬅️ kunci
maxWidth: "100%", // ⬅️ penting
}}
>
{tabs.map((tab, i) => (
<TabsTab
key={i}
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
paddingInline: "0.75rem", // ⬅️ lebih ramping
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
}}
>
{tab.label}
</TabsTab>
))}
</TabsList>
</ScrollArea>
</Box>
{tabs.map((tab, i) => (
<TabsPanel
key={i}

View File

@@ -82,7 +82,7 @@ export default function EditKategoriDesaAntiKorupsi() {
// 🧩 UI
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box px={{ base: 0, md: 'lg' }} py="xs">
<Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />

View File

@@ -43,7 +43,7 @@ export default function CreateKategoriDesaAntiKorupsi() {
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box px={{ base: 0, md: 'lg' }} py="xs">
<Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />

View File

@@ -1,6 +1,23 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import {
Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
@@ -10,9 +27,8 @@ import HeaderSearch from '../../../_com/header';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import korupsiState from '../../../_state/landing-page/desa-anti-korupsi';
function KategoriDesaAntiKorupsi() {
const [search, setSearch] = useState("")
const [search, setSearch] = useState('');
return (
<Box>
<HeaderSearch
@@ -28,126 +44,188 @@ function KategoriDesaAntiKorupsi() {
}
function ListKategoriKegiatan({ search }: { search: string }) {
const stateKategori = useProxy(korupsiState.kategoriDesaAntiKorupsi)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter()
const stateKategori = useProxy(korupsiState.kategoriDesaAntiKorupsi);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const router = useRouter();
const {
data,
page,
totalPages,
loading,
load,
} = stateKategori.findMany;
const { data, page, totalPages, loading, load } = stateKategori.findMany;
const handleHapus = () => {
if (selectedId) {
stateKategori.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
stateKategori.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
}
}
};
useShallowEffect(() => {
load(page, 10, search)
}, [page, search])
load(page, 10, search);
}, [page, search]);
const filteredData = data || []
const filteredData = data || [];
if (loading || !data) {
return (
<Stack py={10}>
<Stack py="xl">
<Skeleton height={600} radius="md" />
</Stack>
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Kategori Kegiatan</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/create')}
>
Tambah Baru
</Button>
</Group>
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover striped verticalSpacing="sm">
<TableThead>
<TableTr>
<TableTh>Nama Kategori</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Hapus</TableTh>
// Mobile cards
const renderMobileCards = () => (
<Stack gap="md">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} p="md" withBorder>
<Group justify="space-between" align="flex-start">
<Box flex={1}>
<Text fw={500} fz={{ base: 'sm', md: 'md' }} lh={1.45} lineClamp={2}>
{item.name}
</Text>
</Box>
<Group gap="xs" wrap="nowrap">
<Button
variant="light"
color="green"
size="xs"
onClick={() => router.push(`/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/${item.id}`)}
>
<IconEdit size={16} />
</Button>
<Button
variant="light"
color="red"
size="xs"
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={16} />
</Button>
</Group>
</Group>
</Paper>
))
) : (
<Paper p="xl" ta="center">
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data kategori yang ditemukan
</Text>
</Paper>
)}
</Stack>
);
// Desktop table
const renderDesktopTable = () => (
<Box>
<Table highlightOnHover striped verticalSpacing="sm" miw={300}>
<TableThead>
<TableTr>
<TableTh>
<Text fw={600} fz="sm" c="dimmed">
Nama Kategori
</Text>
</TableTh>
<TableTh>
<Text fw={600} fz="sm" c="dimmed">
Edit
</Text>
</TableTh>
<TableTh>
<Text fw={600} fz="sm" c="dimmed">
Hapus
</Text>
</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text fw={500} fz="md" lh={1.45} lineClamp={1}>
{item.name}
</Text>
</TableTd>
<TableTd w={60}>
<Button
variant="light"
color="green"
size="sm"
onClick={() => router.push(`/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/${item.id}`)}
>
<IconEdit size={18} />
</Button>
</TableTd>
<TableTd w={60}>
<Button
variant="light"
color="red"
size="sm"
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={18} />
</Button>
</TableTd>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={200}>
<Text fw={500} lineClamp={1}>{item.name}</Text>
</Box>
</TableTd>
<TableTd>
<Button
variant="light"
color="green"
size="sm"
onClick={() => router.push(`/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/${item.id}`)}
>
<IconEdit size={18} />
</Button>
</TableTd>
<TableTd>
<Button
variant="light"
color="red"
size="sm"
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={18} />
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={2}>
<Center py={20}>
<Text c="dimmed">Tidak ada data kategori yang ditemukan</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
))
) : (
<TableTr>
<TableTd colSpan={3} ta="center" py="xl">
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data kategori yang ditemukan
</Text>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
);
return (
<Box py={{ base: 'xl', md: 'xl' }}>
<Paper bg={colors['white-1']} p={{ base: 'md', md: 'xl' }} shadow="md" radius="md">
<Group justify="space-between" mb={{ base: 'md', md: 'lg' }}>
<Title order={2} lh={1.2}>
Daftar Kategori Kegiatan
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/create')}
>
Tambah Baru
</Button>
</Group>
<Box visibleFrom="md">{renderDesktopTable()}</Box>
<Box hiddenFrom="md">{renderMobileCards()}</Box>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
{/* Modal Konfirmasi Hapus */}
{totalPages > 1 && (
<Center mt="xl">
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
color="blue"
radius="md"
/>
</Center>
)}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
@@ -158,4 +236,4 @@ function ListKategoriKegiatan({ search }: { search: string }) {
);
}
export default KategoriDesaAntiKorupsi
export default KategoriDesaAntiKorupsi;

View File

@@ -150,7 +150,7 @@ export default function EditDesaAntiKorupsi() {
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box px={{ base: 0, md: 'lg' }} py="xs">
<Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />

View File

@@ -42,7 +42,7 @@ export default function DetailKegiatanDesa() {
const data = detailState.findUnique.data;
return (
<Box py={10}>
<Box px={{ base: 0, md: 'xs' }} py="xs">
<Button
variant="subtle"
onClick={() => router.back()}
@@ -53,7 +53,7 @@ export default function DetailKegiatanDesa() {
</Button>
<Paper
w={{ base: "100%", md: "50%" }}
w={{ base: "100%", md: "70%" }}
bg={colors['white-1']}
p="lg"
radius="md"

View File

@@ -85,7 +85,7 @@ export default function CreateDesaAntiKorupsi() {
}
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box px={{ base: 0, md: 'lg' }} py="xs">
<Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />

View File

@@ -38,7 +38,7 @@ function ListDesaAntiKorupsi({ search }: { search: string }) {
if (loading || !data) {
return (
<Stack py="md">
<Stack py={{ base: 'sm', md: 'md' }}>
<Skeleton height={650} radius="lg" />
</Stack>
);
@@ -46,11 +46,13 @@ function ListDesaAntiKorupsi({ search }: { search: string }) {
if (data.length === 0) {
return (
<Box py="md">
<Paper p="lg" radius="lg" shadow="md" withBorder>
<Box py={{ base: 'sm', md: 'md' }}>
<Paper p={{ base: 'md', md: 'lg' }} radius="lg" shadow="md" withBorder>
<Stack align="center" gap="sm">
<Title order={4}>Data Program Desa Anti Korupsi</Title>
<Text c="dimmed" ta="center">
<Title order={2} lh={1.2}>
Data Program Desa Anti Korupsi
</Title>
<Text c="dimmed" ta="center" fz={{ base: 'xs', md: 'sm' }} lh={1.5}>
Belum ada data program yang tersedia
</Text>
</Stack>
@@ -61,48 +63,56 @@ function ListDesaAntiKorupsi({ search }: { search: string }) {
return (
<Box>
<Stack gap="md">
<Paper p="lg" radius="lg" shadow="md" withBorder>
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Program Desa Anti Korupsi</Title>
<Button leftSection={<IconPlus size={18} />} color="blue" variant="light"
onClick={() => router.push('/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/create')}
>
Tambah Baru
</Button>
<Stack gap={'md'}>
<Paper p={{ base: 'md', md: 'lg' }} radius="lg" shadow="md" withBorder>
<Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
<Title order={2} lh={1.2}>
Daftar Program Desa Anti Korupsi
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push('/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/create')
}
>
Tambah Baru
</Button>
</Group>
<Box style={{ overflowX: 'auto' }}>
{/* Desktop Table */}
<Box visibleFrom="md">
<Table
striped
highlightOnHover
withRowBorders
verticalSpacing="sm"
>
<TableThead>
<TableTr>
<TableTh style={{ width: '50%' }}>Nama Program</TableTh>
<TableTh style={{ width: '30%' }}>Kategori</TableTh>
<TableTh style={{ width: '20%', textAlign: 'center' }}>Aksi</TableTh>
<TableTh w="50%">Nama Program</TableTh>
<TableTh w="30%">Kategori</TableTh>
<TableTh w="20%" ta="center">
Aksi
</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd style={{ width: '50%' }}>
<Text fw={500} lineClamp={1}>
<TableTd w="50%">
<Text fw={500} lineClamp={1} fz="md" lh={1.5}>
{item.name || '-'}
</Text>
</TableTd>
<TableTd style={{ width: '30%' }}>
<Box w={200}>
<Text fz="sm" c="dimmed" lineClamp={1}>
<TableTd w="30%">
<Text fz="sm" c="dimmed" lineClamp={1} lh={1.5}>
{item.kategori?.name || '-'}
</Text>
</Box>
</TableTd>
<TableTd style={{ width: '20%', textAlign: 'center' }}>
<TableTd w="20%" ta="center">
<Button
size="xs"
radius="md"
@@ -123,7 +133,7 @@ function ListDesaAntiKorupsi({ search }: { search: string }) {
) : (
<TableTr>
<TableTd colSpan={3}>
<Text ta="center" c="dimmed">
<Text ta="center" c="dimmed" fz="sm" lh={1.5}>
Tidak ditemukan data dengan kata kunci pencarian
</Text>
</TableTd>
@@ -132,6 +142,48 @@ function ListDesaAntiKorupsi({ search }: { search: string }) {
</TableTbody>
</Table>
</Box>
{/* Mobile Cards */}
<Box hiddenFrom="md">
<Stack gap="xs">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} p="sm" radius="md" withBorder shadow="xs">
<Stack gap="xs">
<Text fw={500} fz="sm" lh={1.5} lineClamp={1}>
{item.name || '-'}
</Text>
<Text fz="xs" c="dimmed" lh={1.5} lineClamp={1}>
Kategori: {item.kategori?.name || '-'}
</Text>
<Group justify="flex-end">
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() =>
router.push(
`/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/${item.id}`
)
}
>
Detail
</Button>
</Group>
</Stack>
</Paper>
))
) : (
<Paper p="sm" radius="md" withBorder>
<Text ta="center" c="dimmed" fz="xs" lh={1.5}>
Tidak ditemukan data dengan kata kunci pencarian
</Text>
</Paper>
)}
</Stack>
</Box>
</Paper>
<Center>
@@ -144,7 +196,6 @@ function ListDesaAntiKorupsi({ search }: { search: string }) {
}}
size="md"
radius="md"
mt="md"
/>
</Center>
</Stack>
@@ -152,4 +203,4 @@ function ListDesaAntiKorupsi({ search }: { search: string }) {
);
}
export default DesaAntiKorupsi;
export default DesaAntiKorupsi;

View File

@@ -1,7 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { Box, ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { IconChartBar, IconUsers } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
@@ -53,36 +53,41 @@ function LayoutTabsKepuasan({ children }: { children: React.ReactNode }) {
radius="lg"
keepMounted={false}
>
{/* ✅ Scroll horizontal wrapper */}
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}}
>
{tabs.map((e, i) => (
<TabsTab
key={i}
value={e.value}
leftSection={e.icon}
style={{
fontWeight: 500,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{e.label}
</TabsTab>
))}
</TabsList>
</ScrollArea>
<Box>
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}}
>
{tabs.map((tab, i) => (
<TabsTab
key={i}
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
}}
>
{tab.label}
</TabsTab>
))}
</TabsList>
</ScrollArea>
</Box>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value}>
<></>

View File

@@ -149,7 +149,7 @@ function EditResponden() {
);
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box px={{ base: 0, md: 'lg' }} py="xs">
<Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />

View File

@@ -38,7 +38,7 @@ export default function DetailResponden() {
)
}
return (
<Box py={10}>
<Box px={{ base: 0, md: 'xs' }} py="xs">
<Button
variant="subtle"
onClick={() => router.back()}
@@ -50,7 +50,7 @@ export default function DetailResponden() {
<Paper
withBorder
w={{ base: "100%", md: "60%" }}
w={{ base: "100%", md: "70%" }}
bg={colors['white-1']}
p="lg"
radius="md"

View File

@@ -60,7 +60,7 @@ function ListResponden({ search }: ListRespondenProps) {
if (loading || !data) {
return (
<Stack py="md">
<Stack py={{ base: 'md', md: 'lg' }}>
<Skeleton height={650} radius="lg" />
</Stack>
);
@@ -68,11 +68,13 @@ function ListResponden({ search }: ListRespondenProps) {
if (data.length === 0) {
return (
<Box py="md">
<Paper p="lg" radius="lg" shadow="md" withBorder>
<Box py={{ base: 'md', md: 'lg' }}>
<Paper p={{ base: 'md', md: 'lg' }} radius="lg" shadow="md" withBorder>
<Stack align="center" gap="sm">
<Title order={4}>Data Responden</Title>
<Text c="dimmed" ta="center">
<Title order={2} lh={1.2}>
Data Responden
</Title>
<Text c="dimmed" ta="center" fz={{ base: 'sm', md: 'md' }} lh={1.5}>
Belum ada data responden yang tersedia
</Text>
</Stack>
@@ -83,12 +85,13 @@ function ListResponden({ search }: ListRespondenProps) {
return (
<Box>
<Stack gap="md">
<Paper p="lg" radius="lg" shadow="md" withBorder>
<Title order={4} mb="sm">
Daftar Responden
</Title>
<Box style={{ overflowX: 'auto' }}>
<Stack gap={'lg'}>
{/* Desktop Table */}
<Box visibleFrom="md">
<Paper p="lg" radius="lg" shadow="md" withBorder>
<Title order={2} size="lg" mb="md" lh={1.2}>
Daftar Responden
</Title>
<Table
striped
highlightOnHover
@@ -97,18 +100,18 @@ function ListResponden({ search }: ListRespondenProps) {
>
<TableThead>
<TableTr>
<TableTh style={{ width: '5%' }}>No</TableTh>
<TableTh style={{ width: '25%' }}>Nama</TableTh>
<TableTh style={{ width: '20%' }}>Tanggal</TableTh>
<TableTh style={{ width: '20%' }}>Jenis Kelamin</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
<TableTh fz="sm" fw={600} w={60}>No</TableTh>
<TableTh fz="sm" fw={600}>Nama</TableTh>
<TableTh fz="sm" fw={600}>Tanggal</TableTh>
<TableTh fz="sm" fw={600}>Jenis Kelamin</TableTh>
<TableTh fz="sm" fw={600} w={120}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length === 0 ? (
<TableTr>
<TableTd colSpan={5}>
<Text ta="center" c="dimmed">
<Text ta="center" c="dimmed" fz="sm" lh={1.5}>
Tidak ditemukan data dengan kata kunci pencarian
</Text>
</TableTd>
@@ -116,24 +119,18 @@ function ListResponden({ search }: ListRespondenProps) {
) : (
filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd>{index + 1}</TableTd>
<TableTd>{item.name}</TableTd>
<TableTd>
<Box w={150}>
<TableTd fz="md" lh={1.5}>{index + 1}</TableTd>
<TableTd fz="md" lh={1.5}>{item.name}</TableTd>
<TableTd fz="md" lh={1.5}>
{item.tanggal
? new Date(item.tanggal).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric',
})
day: '2-digit',
month: 'long',
year: 'numeric',
})
: '-'}
</Box>
</TableTd>
<TableTd>
<Box w={100}>
{item.jenisKelamin.name}
</Box>
</TableTd>
<TableTd fz="md" lh={1.5}>{item.jenisKelamin.name}</TableTd>
<TableTd>
<Button
size="xs"
@@ -155,8 +152,64 @@ function ListResponden({ search }: ListRespondenProps) {
)}
</TableTbody>
</Table>
</Box>
</Paper>
</Paper>
</Box>
{/* Mobile Cards */}
<Box hiddenFrom="md">
<Stack gap="sm">
<Title order={2} size="md" lh={1.2} px="md">
Daftar Responden
</Title>
{filteredData.length === 0 ? (
<Paper p="md" radius="lg" shadow="sm" mx="md">
<Text ta="center" c="dimmed" fz="sm" lh={1.5}>
Tidak ditemukan data dengan kata kunci pencarian
</Text>
</Paper>
) : (
filteredData.map((item) => (
<Paper key={item.id} p="md" radius="lg" shadow="sm" mx="md">
<Stack gap={4}>
<Text fz="sm" c="dimmed" lh={1.4}>Nama</Text>
<Text fz="md" lh={1.5}>{item.name}</Text>
<Text fz="sm" c="dimmed" lh={1.4}>Tanggal</Text>
<Text fz="md" lh={1.5}>
{item.tanggal
? new Date(item.tanggal).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric',
})
: '-'}
</Text>
<Text fz="sm" c="dimmed" lh={1.4}>Jenis Kelamin</Text>
<Text fz="md" lh={1.5}>{item.jenisKelamin.name}</Text>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImac size={16} />}
onClick={() =>
router.push(
`/admin/landing-page/indeks-kepuasan-masyarakat/responden/${item.id}`
)
}
mt="xs"
>
Detail
</Button>
</Stack>
</Paper>
))
)}
</Stack>
</Box>
<Center>
<Pagination
value={page}
@@ -167,7 +220,7 @@ function ListResponden({ search }: ListRespondenProps) {
}}
size="md"
radius="md"
mt="md"
mt={{ base: 'md', md: 'lg' }}
/>
</Center>
</Stack>
@@ -175,4 +228,4 @@ function ListResponden({ search }: ListRespondenProps) {
);
}
export default Responden;
export default Responden;

View File

@@ -56,6 +56,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
radius="lg"
keepMounted={false}
>
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
@@ -63,6 +64,10 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}}
>
{tabs.map((tab, i) => (
@@ -74,6 +79,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
}}
>
{tab.label}

View File

@@ -78,7 +78,7 @@ function EditKategoriPrestasi() {
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box px={{ base: 0, md: 'lg' }} py="xs">
<Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />

View File

@@ -40,7 +40,7 @@ function CreateKategoriPrestasi() {
}
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box px={{ base: 0, md: 'lg' }} py="xs">
<Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />

View File

@@ -57,7 +57,7 @@ function ListKategoriPrestasi({ search }: { search: string }) {
if (loading || !data) {
return (
<Stack py={10}>
<Stack py="md">
<Skeleton h={500} />
</Stack>
)
@@ -65,28 +65,33 @@ function ListKategoriPrestasi({ search }: { search: string }) {
return (
<Box>
{/* DESKTOP: Table */}
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm" withBorder>
<Group justify="space-between" mb="md">
<Title order={4} c="dark">List Kategori Prestasi</Title>
<Button leftSection={<IconPlus size={18} />} color="blue" variant="light" onClick={() => router.push('/admin/landing-page/prestasi-desa/kategori-prestasi-desa/create')}>
Tambah Baru
</Button>
<Group justify="space-between" mb="xl">
<Title order={2} size="lg" lh={1.2}>List Kategori Prestasi</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/landing-page/prestasi-desa/kategori-prestasi-desa/create')}
>
Tambah Baru
</Button>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Box visibleFrom="md">
<Table verticalSpacing="sm" highlightOnHover>
<TableThead>
<TableTr>
<TableTh>Nama Kategori</TableTh>
<TableTh style={{ width: '120px' }} ta={'center'}>Edit</TableTh>
<TableTh ta={'center'} style={{ width: '120px' }}>Delete</TableTh>
<TableTh><Text fz="sm" fw={600} c="dark">Nama Kategori</Text></TableTh>
<TableTh w={120} ta="center"><Text fz="sm" fw={600} c="dark">Edit</Text></TableTh>
<TableTh w={120} ta="center"><Text fz="sm" fw={600} c="dark">Delete</Text></TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length === 0 ? (
<TableTr>
<TableTd colSpan={2} style={{ textAlign: 'center' }}>
<Text py="md" c="dimmed">
<TableTd colSpan={3} ta="center">
<Text py="md" c="dimmed" fz="sm" lh={1.5}>
{search ? 'Tidak ada hasil yang cocok' : 'Belum ada data kategori'}
</Text>
</TableTd>
@@ -95,68 +100,130 @@ function ListKategoriPrestasi({ search }: { search: string }) {
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={200}>
<Text truncate="end" fz={"sm"}>{item.name}</Text>
</Box>
<Text truncate="end" fz="md" lh={1.5} c="dark">
{item.name}
</Text>
</TableTd>
<TableTd style={{ textAlign: 'center', width: '120px' }}>
<Button
variant="light"
color="green"
size="sm"
onClick={() => router.push(`/admin/landing-page/prestasi-desa/kategori-prestasi-desa/${item.id}`)}
>
<IconEdit size={18} />
</Button>
<TableTd ta="center" w={120}>
<Button
variant="light"
color="green"
size="sm"
onClick={() => router.push(`/admin/landing-page/prestasi-desa/kategori-prestasi-desa/${item.id}`)}
>
<IconEdit size={16} />
</Button>
</TableTd>
<TableTd style={{ textAlign: 'center', width: '120px' }}>
<Button
variant="light"
color="red"
size="sm"
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={18} />
</Button>
<TableTd ta="center" w={120}>
<Button
variant="light"
color="red"
size="sm"
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={16} />
</Button>
</TableTd>
</TableTr>
))
)}
</TableTbody>
</Table>
{totalPages > 1 && (
<Center mt="xl">
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
total={totalPages}
withEdges
size="sm"
styles={{
control: {
'&[data-active]': {
background: `${colors['blue-button']} !important`,
},
},
}}
/>
</Center>
)}
</Box>
{totalPages > 1 && (
<Center mt="lg">
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
total={totalPages}
withEdges
size="sm"
styles={{
control: {
'&[data-active]': {
background: `${colors['blue-button']} !important`,
},
},
}}
/>
</Center>
)}
{/* MOBILE: Card */}
<Box hiddenFrom="md">
<Stack gap="md">
{filteredData.length === 0 ? (
<Paper p="lg" ta="center">
<Text c="dimmed" fz="sm" lh={1.5}>
{search ? 'Tidak ada hasil yang cocok' : 'Belum ada data kategori'}
</Text>
</Paper>
) : (
filteredData.map((item) => (
<Paper key={item.id} p="md" withBorder bg={colors['white-1']}>
<Stack gap="xs">
<Text fz="sm" lh={1.5} fw={600} c="dark">{item.name}</Text>
<Group justify="flex-end" gap="xs">
<Button
variant="light"
color="green"
size="xs"
onClick={() => router.push(`/admin/landing-page/prestasi-desa/kategori-prestasi-desa/${item.id}`)}
>
<IconEdit size={14} />
</Button>
<Button
variant="light"
color="red"
size="xs"
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={14} />
</Button>
</Group>
</Stack>
</Paper>
))
)}
{totalPages > 1 && (
<Center py="lg">
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
total={totalPages}
withEdges
size="xs"
styles={{
control: {
'&[data-active]': {
background: `${colors['blue-button']} !important`,
},
},
}}
/>
</Center>
)}
</Stack>
</Box>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus kategori prestasi ini?'
/>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus kategori prestasi ini?'
/>
</Box >
</Box>
);
}
export default KategoriPrestasiDesa
export default KategoriPrestasiDesa

View File

@@ -128,7 +128,7 @@ export default function EditPrestasiDesa() {
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box px={{ base: 0, md: 'lg' }} py="xs">
<Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />

View File

@@ -41,7 +41,7 @@ function DetailPrestasiDesa() {
}
return (
<Box py={10}>
<Box px={{ base: 0, md: 'xs' }} py="xs">
<Button
variant="subtle"
onClick={() => router.back()}
@@ -53,7 +53,7 @@ function DetailPrestasiDesa() {
<Paper
withBorder
w={{ base: "100%", md: "60%" }}
w={{ base: "100%", md: "70%" }}
bg={colors['white-1']}
p="lg"
radius="md"

View File

@@ -69,7 +69,7 @@ function CreatePrestasiDesa() {
}
}
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box px={{ base: 0, md: 'lg' }} py="xs">
<Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />

View File

@@ -1,7 +1,7 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useMediaQuery, useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
@@ -28,6 +28,7 @@ function ListPrestasiDesa() {
function ListPrestasi({ search }: { search: string }) {
const listState = useProxy(prestasiState.prestasiDesa)
const router = useRouter();
const isMobile = useMediaQuery('(max-width: 768px)');
const {
data,
@@ -39,60 +40,65 @@ function ListPrestasi({ search }: { search: string }) {
useShallowEffect(() => {
load(page, 10, search);
}, [page, search]);
}, []);
const filteredData = data || []
if (loading || !data) {
return (
<Stack py={10}>
<Stack py={{ base: 'sm', md: 'md' }}>
<Skeleton height={600} radius="md" />
</Stack>
);
}
return (
<Box py={10}>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Prestasi Desa</Title>
<Box py={{ base: 'sm', md: 'md' }}>
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
<Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
<Title order={2} size={isMobile ? 'md' : 'lg'} lh={1.2}>
Daftar Prestasi Desa
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/landing-page/prestasi-desa/list-prestasi-desa/create')}
size={isMobile ? 'xs' : 'sm'}
>
Tambah Baru
</Button>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
{/* Desktop Table */}
<Box visibleFrom="md">
<Table highlightOnHover miw={0}>
<TableThead>
<TableTr>
<TableTh style={{ width: '25%' }}>Nama Prestasi</TableTh>
<TableTh style={{ width: '25%' }}>Deskripsi</TableTh>
<TableTh style={{ width: '25%' }}>Kategori</TableTh>
<TableTh style={{ width: '25%', textAlign: 'center' }}>Aksi</TableTh>
<TableTh w="25%">Nama Prestasi</TableTh>
<TableTh w="25%">Deskripsi</TableTh>
<TableTh w="25%">Kategori</TableTh>
<TableTh w="25%" ta="center">Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd style={{ width: '25%' }}>
<Box w={100}>
<Text truncate="end" fz={"sm"}>{item.name}</Text>
</Box>
<TableTd w="25%">
<Text truncate="end" fz="md" lh={1.5}>
{item.name}
</Text>
</TableTd>
<TableTd style={{ width: '25%' }}>
<Text lineClamp={1} fz="sm" c="dimmed" dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
<TableTd w="25%">
<Text lineClamp={1} fz="md" c="dimmed" lh={1.5} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd>
<TableTd style={{ width: '25%' }}>
<Box w={150}>
<Text truncate="end" fz={"sm"}>{item.kategori?.name || 'Tidak ada kategori'}</Text>
</Box>
<TableTd w="25%">
<Text truncate="end" fz="md" lh={1.5}>
{item.kategori?.name || 'Tidak ada kategori'}
</Text>
</TableTd>
<TableTd style={{ width: '25%', textAlign: 'center' }}>
<TableTd w="25%" ta="center">
<Button
size="xs"
radius="md"
@@ -108,23 +114,63 @@ function ListPrestasi({ search }: { search: string }) {
))
) : (
<TableTr>
<TableTd colSpan={4} style={{ textAlign: 'center' }}>
<Text c="dimmed" py="md">Tidak ada data prestasi</Text>
<TableTd colSpan={4} ta="center">
<Text c="dimmed" py="md" fz="sm" lh={1.4}>
Tidak ada data prestasi
</Text>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
{/* Mobile Cards */}
<Stack hiddenFrom="md" gap="xs">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} withBorder p="sm" radius="sm">
<Stack gap={4}>
<Text fz="sm" fw={600} lh={1.4}>
{item.name}
</Text>
<Text fz="xs" c="dimmed" lh={1.5} lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
<Text fz="xs" c="dimmed" lh={1.4}>
Kategori: {item.kategori?.name || 'Tidak ada kategori'}
</Text>
<Group justify="flex-end" mt="xs">
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={14} />}
onClick={() => router.push(`/admin/landing-page/prestasi-desa/list-prestasi-desa/${item.id}`)}
>
Detail
</Button>
</Group>
</Stack>
</Paper>
))
) : (
<Center py="md">
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data prestasi
</Text>
</Center>
)}
</Stack>
</Paper>
{totalPages > 1 && (
<Center mt="lg">
<Center mt={{ base: 'md', md: 'lg' }}>
<Pagination
value={page}
onChange={load}
total={totalPages}
withEdges
size="sm"
size={isMobile ? 'xs' : 'sm'}
/>
</Center>
)}
@@ -132,4 +178,4 @@ function ListPrestasi({ search }: { search: string }) {
)
}
export default ListPrestasiDesa;
export default ListPrestasiDesa;

View File

@@ -2,6 +2,7 @@
'use client'
import colors from '@/con/colors';
import {
Box,
ScrollArea,
Stack,
Tabs,
@@ -74,36 +75,76 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
keepMounted={false}
>
{/* ✅ Scroll horizontal wrapper */}
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}}
<Box visibleFrom='md' pb={10}>
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}}
>
{tabs.map((tab, i) => (
<TabsTab
key={i}
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
}}
>
{tab.label}
</TabsTab>
))}
</TabsList>
</ScrollArea>
</Box>
<Box hiddenFrom='md' pb={10}>
<ScrollArea
type="auto"
offsetScrollbars={false}
w="100%"
>
{tabs.map((tab, i) => (
<TabsTab
key={i}
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
}}
>
{tab.label}
</TabsTab>
))}
</TabsList>
</ScrollArea>
<TabsList
p="xs" // lebih kecil
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
width: "max-content", // ⬅️ kunci
maxWidth: "100%", // ⬅️ penting
}}
>
{tabs.map((tab, i) => (
<TabsTab
key={i}
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
paddingInline: "0.75rem", // ⬅️ lebih ramping
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
}}
>
{tab.label}
</TabsTab>
))}
</TabsList>
</ScrollArea>
</Box>
{tabs.map((tab, i) => (
<TabsPanel

View File

@@ -177,7 +177,7 @@ function EditMediaSosial() {
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box px={{ base: 0, md: 'xs' }} py="xs">
<Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />

View File

@@ -50,7 +50,7 @@ function DetailMediaSosial() {
const data = stateMediaSosial.findUnique.data;
return (
<Box py={10}>
<Box px={{ base: 0, md: 'xs' }} py="xs">
<Button
variant="subtle"
onClick={() => router.back()}
@@ -62,7 +62,7 @@ function DetailMediaSosial() {
<Paper
withBorder
w={{ base: "100%", md: "50%" }}
w={{ base: "100%", md: "70%" }}
bg={colors['white-1']}
p="lg"
radius="md"

View File

@@ -25,7 +25,6 @@ import { useProxy } from 'valtio/utils';
import profileLandingPageState from '../../../../_state/landing-page/profile';
import SelectSosialMedia from '@/app/admin/(dashboard)/_com/selectSocialMedia';
// ⭐ Tambah type SosmedKey
type SosmedKey =
| 'facebook'
@@ -88,7 +87,6 @@ export default function CreateMediaSosial() {
stateMediaSosial.create.form.imageId = null;
stateMediaSosial.create.form.icon = sosmedMap[selectedSosmed].src!;
await stateMediaSosial.create.create();
resetForm();
router.push('/admin/landing-page/profil/media-sosial');
@@ -129,13 +127,13 @@ export default function CreateMediaSosial() {
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box px={{ base: 0, md: 'lg' }} py="xs">
{/* Header */}
<Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">
<Title order={2} ml="sm" c="dark" lh={1.2} fz={{ base: 'md', md: 'lg' }}>
Tambah Media Sosial
</Title>
</Group>
@@ -155,7 +153,7 @@ export default function CreateMediaSosial() {
{/* Custom icon uploader */}
{selectedSosmed === 'custom' && (
<Box>
<Text fw="bold" fz="sm" mb={6}>
<Text fw="bold" fz={{ base: 'sm', md: 'md' }} lh={1.45} mb={6}>
Upload Custom Icon
</Text>
@@ -185,8 +183,10 @@ export default function CreateMediaSosial() {
</Dropzone.Idle>
<Stack align="center" gap="xs">
<Text fw={500}>Seret gambar atau klik untuk pilih</Text>
<Text size="sm" c="dimmed">
<Text fw={500} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
Seret gambar atau klik untuk pilih
</Text>
<Text fz={{ base: 12, md: 'sm' }} c="dimmed" lh={1.4}>
Maksimal 5MB, format .png, .jpg, .jpeg, webp
</Text>
</Stack>
@@ -229,7 +229,11 @@ export default function CreateMediaSosial() {
{/* Input name */}
<TextInput
label="Nama Media Sosial"
label={
<Text fw={500} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
Nama Media Sosial
</Text>
}
placeholder="Masukkan nama media sosial"
value={stateMediaSosial.create.form.name ?? ''}
onChange={(e) => (stateMediaSosial.create.form.name = e.target.value)}
@@ -238,7 +242,11 @@ export default function CreateMediaSosial() {
{/* Input link */}
<TextInput
label="Link / Kontak"
label={
<Text fw={500} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
Link / Kontak
</Text>
}
placeholder="Masukkan link atau nomor"
value={stateMediaSosial.create.form.iconUrl ?? ''}
onChange={(e) => (stateMediaSosial.create.form.iconUrl = e.target.value)}
@@ -266,4 +274,4 @@ export default function CreateMediaSosial() {
</Paper>
</Box>
);
}
}

View File

@@ -1,7 +1,25 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Group, Image, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import {
Box,
Button,
Center,
Group,
Image,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
@@ -28,11 +46,11 @@ function MediaSosial() {
}
function ListMediaSosial({ search }: { search: string }) {
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial)
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial);
const router = useRouter();
const getIconSource = (item: any) => {
if (item.image?.link) return item.image.link;
if (item.image?.link) return item.image.link;
if (item.icon && sosmedMap[item.icon as keyof typeof sosmedMap]?.src) {
return sosmedMap[item.icon as keyof typeof sosmedMap].src;
}
@@ -48,101 +66,201 @@ function ListMediaSosial({ search }: { search: string }) {
} = stateMediaSosial.findMany;
useShallowEffect(() => {
load(page, 10, search)
}, [page, search])
load(page, 10, search);
}, [page, search]);
const filteredData = data || []
const filteredData = data || [];
if (loading || !data) {
return (
<Stack py={10}>
<Stack py={{ base: 'sm', sm: 'md' }}>
<Skeleton height={600} radius="md" />
</Stack>
);
}
return (
<Box py={10}>
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Media Sosial</Title>
<Button leftSection={<IconPlus size={18} />} color="blue" variant="light" onClick={() => router.push('/admin/landing-page/profil/media-sosial/create')}>
<Box py={{ base: 'sm', sm: 'md' }}>
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', sm: 'lg' }} shadow="md" radius="md">
<Group justify="space-between" mb={{ base: 'sm', sm: 'md' }}>
<Title order={4} lh={1.15}>
Daftar Media Sosial
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/landing-page/profil/media-sosial/create')}
fz={{ base: 'xs', sm: 'sm' }}
>
Tambah Baru
</Button>
</Group>
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '25%' }}>Nama Media Sosial / Kontak</TableTh>
<TableTh style={{ width: '20%' }}>Gambar</TableTh>
<TableTh style={{ width: '20%' }}>Link / No. Telepon</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd style={{ width: '25%', }}>
<Text fw={500} truncate="end" lineClamp={1}>{item.name}</Text>
</TableTd>
<TableTd style={{ width: '20%' }}>
<Box w={50} h={50} style={{ borderRadius: 8, overflow: 'hidden' }}>
{(() => {
const src = getIconSource(item);
if (src) {
return (
<Image
loading="lazy"
src={src}
alt={item.name}
fit={item.image?.link ? "cover" : "contain"}
/>
);
}
return <Box bg={colors['blue-button']} w="100%" h="100%" />;
})()}
</Box>
</TableTd>
<TableTd style={{ width: '20%', }}>
<Box w={250}>
<Text truncate fz="sm" c="dimmed" lineClamp={1}>
{item.iconUrl || item.noTelp || '-'}
<Box>
{/* Desktop: Table | Mobile: Card-based vertical layout */}
<Box visibleFrom="md">
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '25%' }}>
<Text fw={600} fz="md" lh={1.45}>
Nama Media Sosial / Kontak
</Text>
</TableTh>
<TableTh style={{ width: '20%' }}>
<Text fw={600} fz="md" lh={1.45}>
Gambar
</Text>
</TableTh>
<TableTh style={{ width: '20%' }}>
<Text fw={600} fz="md" lh={1.45}>
Link / No. Telepon
</Text>
</TableTh>
<TableTh style={{ width: '15%' }}>
<Text fw={600} fz="md" lh={1.45}>
Aksi
</Text>
</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd style={{ width: '25%' }}>
<Text fw={500} fz="md" lh={1.5} truncate="end" lineClamp={1}>
{item.name}
</Text>
</Box>
</TableTd>
<TableTd style={{ width: '15%' }}>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() => router.push(`/admin/landing-page/profil/media-sosial/${item.id}`)}
>
Detail
</Button>
</TableTd>
<TableTd style={{ width: '20%' }}>
<Box w={50} h={50} style={{ borderRadius: 8, overflow: 'hidden' }}>
{(() => {
const src = getIconSource(item);
if (src) {
return (
<Image
loading="lazy"
src={src}
alt={item.name}
fit={item.image?.link ? 'cover' : 'contain'}
/>
);
}
return <Box bg={colors['blue-button']} w="100%" h="100%" />;
})()}
</Box>
</TableTd>
<TableTd style={{ width: '20%' }}>
<Box w={250}>
<Text truncate fz="sm" lh={1.5} c="dimmed" lineClamp={1}>
{item.iconUrl || item.noTelp || '-'}
</Text>
</Box>
</TableTd>
<TableTd style={{ width: '15%' }}>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() =>
router.push(`/admin/landing-page/profil/media-sosial/${item.id}`)
}
>
Detail
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text c="dimmed" fz="md" lh={1.5}>
Tidak ada data media sosial yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">Tidak ada data media sosial yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
)}
</TableTbody>
</Table>
</Box>
{/* Mobile layout */}
<Stack hiddenFrom="md" gap="xs">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} withBorder p="sm" radius="md">
<Group justify="space-between" wrap="nowrap" align='center'>
<Box>
<Text fw={600} fz="sm" lh={1.45}>
{item.name}
</Text>
</Box>
<Box w={40} h={40} style={{ borderRadius: 6, overflow: 'hidden' }}>
{(() => {
const src = getIconSource(item);
if (src) {
return (
<Image
loading="lazy"
src={src}
alt={item.name}
fit={item.image?.link ? 'cover' : 'contain'}
/>
);
}
return <Box bg={colors['blue-button']} w="100%" h="100%" />;
})()}
</Box>
</Group>
<Box>
<a
href={item.link}
target="_blank"
rel="noopener noreferrer"
style={{ textDecoration: 'underline' }}
>
<Text
fz="sm"
c="blue"
truncate
>
{item.iconUrl || item.noTelp || '-'}
</Text>
</a>
</Box>
<Group mt="sm" justify="flex-end">
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() =>
router.push(`/admin/landing-page/profil/media-sosial/${item.id}`)
}
>
Detail
</Button>
</Group>
</Paper>
))
) : (
<Center py={24}>
<Text c="dimmed" fz="sm" lh={1.5}>
Tidak ada data media sosial yang cocok
</Text>
</Center>
)}
</Stack>
</Box>
</Paper>
<Center>
<Pagination
value={page}
@@ -161,4 +279,4 @@ function ListMediaSosial({ search }: { search: string }) {
);
}
export default MediaSosial;
export default MediaSosial;

View File

@@ -178,7 +178,7 @@ function EditPejabatDesa() {
}
return (
<Box>
<Box px={{ base: 0, md: 'lg' }} py="xs">
<Stack gap="xs">
<Group mb="md">
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">

View File

@@ -3,7 +3,6 @@ import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page
import colors from '@/con/colors';
import { Box, Button, Center, Divider, Grid, GridCol, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
@@ -35,15 +34,15 @@ function Page() {
<Title order={3} c={colors['blue-button']}>Preview Pejabat Desa</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button
c="green"
variant="light"
leftSection={<IconEdit size={18} stroke={2} />}
radius="md"
onClick={() => router.push(`/admin/landing-page/profil/pejabat-desa/${allList.findUnique.data?.id}`)}
>
Edit
</Button>
<Button
style={{fontSize: 15, fontWeight: "bold"}}
c="green"
variant="light"
radius="md"
onClick={() => router.push(`/admin/landing-page/profil/pejabat-desa/${allList.findUnique.data?.id}`)}
>
Edit
</Button>
</GridCol>
</Grid>
{dataArray.map((item) => (
@@ -52,7 +51,7 @@ function Page() {
<Grid>
<GridCol span={12}>
<Center>
<Image src="/darmasaba-icon.png" w={{ base: 100, md: 150 }} alt="Logo Desa" loading="lazy"/>
<Image src="/darmasaba-icon.png" w={{ base: 100, md: 150 }} alt="Logo Desa" loading="lazy" />
</Center>
</GridCol>
<GridCol span={12}>
@@ -93,7 +92,7 @@ function Page() {
</Paper>
<Box mt="lg">
<Text fz={{ base: "1.125rem", md: "1.5rem" }} fw="bold" mb={4}>Jabatan</Text>
<Text fz={{ base: "1rem", md: "1.4rem" }} ta="justify" c={colors['blue-button']}>
<Text fz={{ base: "1rem", md: "1.4rem" }} ta="left" c={colors['blue-button']}>
{item.position}
</Text>
</Box>

View File

@@ -130,7 +130,7 @@ function EditProgramInovasi() {
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box px={{ base: 0, md: 'lg' }} py="xs">
<Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />

View File

@@ -40,13 +40,15 @@ function DetailProgramInovasi() {
const data = stateProgramInovasi.findUnique.data
return (
<Box px={{ base: 'md', md: 'xl' }} py="lg">
<Button variant="subtle" onClick={() => router.back()} leftSection={<IconArrowBack size={22} color={colors['blue-button']} />}>
Kembali
</Button>
<Box px={{ base: 0, md: 'xs' }} py="xs">
<Box pb="20">
<Button variant="subtle" onClick={() => router.back()} leftSection={<IconArrowBack size={22} color={colors['blue-button']} />}>
Kembali
</Button>
</Box>
<Paper
w={{ base: "100%", md: "60%" }}
w={{ base: "100%", md: "70%" }}
bg={colors['white-1']}
p="lg"
radius="md"
@@ -68,9 +70,9 @@ function DetailProgramInovasi() {
<Box>
<Text fz="lg" fw="bold">Gambar</Text>
{data.image?.link ? (
<Image
src={data.image.link}
alt="Gambar Program"
<Image
src={data.image.link}
alt="Gambar Program"
radius="md"
style={{ maxWidth: '100%', maxHeight: 300, objectFit: 'contain' }}
loading="lazy"
@@ -106,28 +108,28 @@ function DetailProgramInovasi() {
</Box>
<Group gap="sm">
<Button
color="red"
onClick={() => {
setSelectedId(data.id);
setModalHapus(true);
}}
variant="light"
radius="md"
size="md"
>
<IconTrash size={20} />
</Button>
<Button
color="red"
onClick={() => {
setSelectedId(data.id);
setModalHapus(true);
}}
variant="light"
radius="md"
size="md"
>
<IconTrash size={20} />
</Button>
<Button
color="green"
onClick={() => router.push(`/admin/landing-page/profil/program-inovasi/${data.id}/edit`)}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
<Button
color="green"
onClick={() => router.push(`/admin/landing-page/profil/program-inovasi/${data.id}/edit`)}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Group>
</Stack>
</Paper>

View File

@@ -76,7 +76,7 @@ function CreateProgramInovasi() {
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box px={{ base: 0, md: 'lg' }} py="xs">
<Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />

View File

@@ -13,7 +13,7 @@ function ProgramInovasi() {
const [search, setSearch] = useState("");
return (
<Box px="md" py="lg">
<Box px={{base: 0, md: "md"}} py="lg">
<HeaderSearch
title="Program Inovasi"
placeholder="Cari program inovasi..."
@@ -52,75 +52,137 @@ function ListProgramInovasi({ search }: { search: string }) {
<Group justify='space-between'>
<Title order={4}>Daftar Program Inovasi</Title>
<Button
color="blue"
leftSection={<IconPlus size={18} />}
variant="light"
radius="md"
onClick={() => router.push('/admin/landing-page/profil/program-inovasi/create')}
>
Tambah Program
</Button>
color="blue"
leftSection={<IconPlus size={18} />}
variant="light"
radius="md"
onClick={() => router.push('/admin/landing-page/profil/program-inovasi/create')}
>
Tambah Program
</Button>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover striped verticalSpacing="sm">
<TableThead>
<TableTr>
<TableTh>Nama Program</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Link</TableTh>
<TableTh>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length === 0 ? (
<Box visibleFrom='md'>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover striped verticalSpacing="sm">
<TableThead>
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">Belum ada data program inovasi</Text>
</Center>
</TableTd>
<TableTh>Nama Program</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Link</TableTh>
<TableTh>Aksi</TableTh>
</TableTr>
) : (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text fw={500}>{item.name}</Text>
</TableTd>
<TableTd style={{ maxWidth: 250 }}>
<Text fz="sm" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.description || '-' }}></Text>
</TableTd>
<TableTd style={{ maxWidth: 250 }}>
<Tooltip label="Buka tautan program" position="top" withArrow>
<a
href={item.link}
target="_blank"
rel="noopener noreferrer"
style={{ color: colors['blue-button'], textDecoration: 'underline' }}
>
<Text truncate fz="sm">{item.link}</Text>
</a>
</Tooltip>
</TableTd>
<TableTd>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() =>
router.push(`/admin/landing-page/profil/program-inovasi/${item.id}`)
}
>
Detail
</Button>
</TableThead>
<TableTbody>
{filteredData.length === 0 ? (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">Belum ada data program inovasi</Text>
</Center>
</TableTd>
</TableTr>
))
)}
</TableTbody>
</Table>
) : (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text fw={500}>{item.name}</Text>
</TableTd>
<TableTd style={{ maxWidth: 250 }}>
<Text fz="sm" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.description || '-' }}></Text>
</TableTd>
<TableTd style={{ maxWidth: 250 }}>
<Tooltip label="Buka tautan program" position="top" withArrow>
<a
href={item.link}
target="_blank"
rel="noopener noreferrer"
style={{ color: colors['blue-button'], textDecoration: 'underline' }}
>
<Text truncate fz="sm">{item.link}</Text>
</a>
</Tooltip>
</TableTd>
<TableTd>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() =>
router.push(`/admin/landing-page/profil/program-inovasi/${item.id}`)
}
>
Detail
</Button>
</TableTd>
</TableTr>
))
)}
</TableTbody>
</Table>
</Box>
</Box>
<Box hiddenFrom="md" pt={20}>
<Stack gap="sm">
{filteredData.map((item) => (
<Paper
key={item.id}
withBorder
radius="md"
p="md"
shadow="xs"
>
<Stack gap={6}>
{/* Title */}
<Text fw={600}>{item.name}</Text>
{/* Description */}
<Text fz="sm" c="gray.7" lineClamp={2}>
{item.description || '-'}
</Text>
{/* Link */}
<Box>
<a
href={item.link}
target="_blank"
rel="noopener noreferrer"
style={{ textDecoration: 'underline' }}
>
<Text
fz="sm"
c="blue"
truncate
>
{item.link}
</Text>
</a>
</Box>
{/* Action */}
<Group justify="flex-end" mt="xs">
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() =>
router.push(
`/admin/landing-page/profil/program-inovasi/${item.id}`
)
}
>
Detail
</Button>
</Group>
</Stack>
</Paper>
))}
</Stack>
</Box>
{filteredData.length > 0 && (
<Center mt="md">
<Pagination

View File

@@ -1,399 +1,3 @@
// 'use client'
// import colors from "@/con/colors";
// import { authStore } from "@/store/authStore";
// import {
// ActionIcon,
// AppShell,
// AppShellHeader,
// AppShellMain,
// AppShellNavbar,
// Burger,
// Center,
// Flex,
// Group,
// Image,
// Loader,
// NavLink,
// ScrollArea,
// Text,
// Tooltip,
// rem
// } from "@mantine/core";
// import { useDisclosure } from "@mantine/hooks";
// import {
// IconChevronLeft,
// IconChevronRight,
// IconLogout2
// } from "@tabler/icons-react";
// import _ from "lodash";
// import Link from "next/link";
// import { useRouter, useSelectedLayoutSegments } from "next/navigation";
// import { useEffect, useState } from "react";
// // import { useSnapshot } from "valtio";
// import { getNavbar } from "./(dashboard)/user&role/_com/dynamicNavbar";
// export default function Layout({ children }: { children: React.ReactNode }) {
// const [opened, { toggle }] = useDisclosure();
// const [loading, setLoading] = useState(true);
// const [isLoggingOut, setIsLoggingOut] = useState(false);
// const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
// const router = useRouter();
// const segments = useSelectedLayoutSegments().map((s) => _.lowerCase(s));
// // const { user } = useSnapshot(authStore);
// // console.log("Current user in store:", user);
// // ✅ FIX: Selalu fetch user data setiap kali komponen mount
// useEffect(() => {
// const fetchUser = async () => {
// try {
// const res = await fetch('/api/auth/me');
// const data = await res.json();
// if (data.user) {
// // ✅ Check if user is NOT active → redirect to waiting room
// if (!data.user.isActive) {
// authStore.setUser(null);
// router.replace('/waiting-room');
// return;
// }
// // ✅ Fetch menuIds
// const menuRes = await fetch(`/api/admin/user-menu-access?userId=${data.user.id}`);
// const menuData = await menuRes.json();
// const menuIds = menuData.success && Array.isArray(menuData.menuIds)
// ? [...menuData.menuIds]
// : null;
// // ✅ Set user dengan menuIds yang fresh
// authStore.setUser({
// id: data.user.id,
// name: data.user.name,
// roleId: Number(data.user.roleId),
// menuIds,
// isActive: data.user.isActive
// });
// // ✅ TAMBAHKAN INI: Redirect ke dashboard sesuai roleId
// const currentPath = window.location.pathname;
// const expectedPath = getRedirectPath(Number(data.user.roleId));
// // Jika user di halaman /admin tapi bukan di path yang sesuai roleId
// if (currentPath === '/admin' || !currentPath.startsWith(expectedPath)) {
// router.replace(expectedPath);
// }
// } else {
// authStore.setUser(null);
// router.replace('/login');
// }
// } catch (error) {
// console.error('Gagal memuat data pengguna:', error);
// authStore.setUser(null);
// router.replace('/login');
// } finally {
// setLoading(false);
// }
// };
// fetchUser();
// }, [router]);
// // ✅ Fungsi helper untuk get redirect path
// const getRedirectPath = (roleId: number): string => {
// switch (roleId) {
// case 0: // DEVELOPER
// case 1: // SUPERADMIN
// case 2: // ADMIN_DESA
// return '/admin/landing-page/profil/program-inovasi';
// case 3: // ADMIN_KESEHATAN
// return '/admin/kesehatan/posyandu';
// case 4: // ADMIN_PENDIDIKAN
// return '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
// default:
// return '/admin';
// }
// };
// if (loading) {
// return (
// <AppShell>
// <AppShellMain>
// <Center h="100vh">
// <Loader />
// </Center>
// </AppShellMain>
// </AppShell>
// );
// }
// // ✅ Ambil menu berdasarkan roleId dan menuIds
// const currentNav = authStore.user
// ? getNavbar({ roleId: authStore.user.roleId, menuIds: authStore.user.menuIds })
// : [];
// const handleLogout = async () => {
// try {
// setIsLoggingOut(true);
// // ✅ Panggil API logout untuk clear session di server
// const response = await fetch('/api/auth/logout', { method: 'POST' });
// const result = await response.json();
// if (result.success) {
// // Clear user data dari store
// authStore.setUser(null);
// // Clear localStorage
// localStorage.removeItem('auth_nomor');
// localStorage.removeItem('auth_kodeId');
// // Force reload untuk reset semua state
// window.location.href = '/login';
// } else {
// console.error('Logout failed:', result.message);
// // Tetap redirect meskipun gagal
// authStore.setUser(null);
// window.location.href = '/login';
// }
// } catch (error) {
// console.error('Error during logout:', error);
// // Tetap clear store dan redirect jika error
// authStore.setUser(null);
// window.location.href = '/login';
// } finally {
// setIsLoggingOut(false);
// }
// };
// return (
// <AppShell
// suppressHydrationWarning
// header={{ height: 64 }}
// navbar={{
// width: { base: 260, sm: 280, lg: 300 },
// breakpoint: 'sm',
// collapsed: {
// mobile: !opened,
// desktop: !desktopOpened,
// },
// }}
// padding="md"
// >
// <AppShellHeader
// style={{
// background: "linear-gradient(90deg, #ffffff, #f9fbff)",
// borderBottom: `1px solid ${colors["blue-button"]}20`,
// padding: '0 16px',
// }}
// px={{ base: 'sm', sm: 'md' }}
// py={{ base: 'xs', sm: 'sm' }}
// >
// <Group w="100%" h="100%" justify="space-between" wrap="nowrap">
// <Flex align="center" gap="sm">
// <Image
// src="/assets/images/darmasaba-icon.png"
// alt="Logo Darmasaba"
// w={{ base: 32, sm: 40 }}
// h={{ base: 32, sm: 40 }}
// radius="md"
// loading="lazy"
// style={{
// minWidth: '32px',
// height: 'auto',
// }}
// />
// <Text
// fw={700}
// c={colors["blue-button"]}
// fz={{ base: 'md', sm: 'xl' }}
// >
// Admin Darmasaba
// </Text>
// </Flex>
// <Group gap="xs">
// {!desktopOpened && (
// <Tooltip label="Buka Navigasi" position="bottom" withArrow>
// <ActionIcon
// variant="light"
// radius="xl"
// size="lg"
// onClick={toggleDesktop}
// color={colors["blue-button"]}
// >
// <IconChevronRight />
// </ActionIcon>
// </Tooltip>
// )}
// <Burger
// opened={opened}
// onClick={toggle}
// hiddenFrom="sm"
// size="md"
// color={colors["blue-button"]}
// mr="xs"
// />
// <Tooltip label="Kembali ke Website Desa" position="bottom" withArrow>
// <ActionIcon
// onClick={() => {
// router.push("/darmasaba");
// }}
// color={colors["blue-button"]}
// radius="xl"
// size="lg"
// variant="gradient"
// gradient={{ from: colors["blue-button"], to: "#228be6" }}
// >
// <Image
// src="/assets/images/darmasaba-icon.png"
// alt="Logo Darmasaba"
// w={20}
// h={20}
// radius="md"
// loading="lazy"
// style={{
// minWidth: '20px',
// height: 'auto',
// }}
// />
// </ActionIcon>
// </Tooltip>
// <Tooltip label="Keluar" position="bottom" withArrow>
// <ActionIcon
// onClick={handleLogout}
// color={colors["blue-button"]}
// radius="xl"
// size="lg"
// variant="gradient"
// gradient={{ from: colors["blue-button"], to: "#228be6" }}
// loading={isLoggingOut}
// disabled={isLoggingOut}
// >
// <IconLogout2 size={22} />
// </ActionIcon>
// </Tooltip>
// </Group>
// </Group>
// </AppShellHeader>
// <AppShellNavbar
// component={ScrollArea}
// style={{
// background: "#ffffff",
// borderRight: `1px solid ${colors["blue-button"]}20`,
// }}
// p={{ base: 'xs', sm: 'sm' }}
// >
// <AppShell.Section p="sm">
// {currentNav.map((v, k) => {
// const isParentActive = segments.includes(_.lowerCase(v.name));
// return (
// <NavLink
// key={k}
// defaultOpened={isParentActive}
// c={isParentActive ? colors["blue-button"] : "gray"}
// label={
// <Text fw={isParentActive ? 600 : 400} fz="sm">
// {v.name}
// </Text>
// }
// style={{
// borderRadius: rem(10),
// marginBottom: rem(4),
// transition: "background 150ms ease",
// }}
// styles={{
// root: {
// '&:hover': {
// backgroundColor: 'rgba(25, 113, 194, 0.05)',
// },
// },
// }}
// variant="light"
// active={isParentActive}
// >
// {v.children.map((child, key) => {
// const isChildActive = segments.includes(
// _.lowerCase(child.name)
// );
// return (
// <NavLink
// key={key}
// href={child.path}
// c={isChildActive ? colors["blue-button"] : "gray"}
// label={
// <Text fw={isChildActive ? 600 : 400} fz="sm">
// {child.name}
// </Text>
// }
// styles={{
// root: {
// borderRadius: rem(8),
// marginBottom: rem(2),
// transition: 'background 150ms ease',
// padding: '6px 12px',
// '&:hover': {
// backgroundColor: isChildActive ? 'rgba(25, 113, 194, 0.15)' : 'rgba(25, 113, 194, 0.05)',
// },
// ...(isChildActive && {
// backgroundColor: 'rgba(25, 113, 194, 0.1)',
// }),
// },
// }}
// active={isChildActive}
// component={Link}
// />
// );
// })}
// </NavLink>
// );
// })}
// </AppShell.Section>
// <AppShell.Section py="md">
// <Group justify="end" pr="sm">
// <Tooltip
// label={desktopOpened ? "Tutup Navigasi" : "Buka Navigasi"}
// position="top"
// withArrow
// >
// <ActionIcon
// variant="light"
// radius="xl"
// size="lg"
// onClick={toggleDesktop}
// color={colors["blue-button"]}
// >
// <IconChevronLeft />
// </ActionIcon>
// </Tooltip>
// </Group>
// </AppShell.Section>
// </AppShellNavbar>
// <AppShellMain
// style={{
// background: "linear-gradient(180deg, #fdfdfd, #f6f9fc)",
// minHeight: "100vh",
// }}
// >
// {children}
// </AppShellMain>
// </AppShell>
// );
// }
// app/admin/layout.tsx
'use client'
import colors from "@/con/colors";
@@ -429,7 +33,7 @@ import { useEffect, useState } from "react";
import { getNavbar } from "./(dashboard)/user&role/_com/dynamicNavbar";
export default function Layout({ children }: { children: React.ReactNode }) {
const [opened, { toggle }] = useDisclosure();
const [opened, { toggle, close }] = useDisclosure(); // ✅ Tambahkan 'close'
const [loading, setLoading] = useState(true);
const [isLoggingOut, setIsLoggingOut] = useState(false);
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
@@ -441,21 +45,19 @@ export default function Layout({ children }: { children: React.ReactNode }) {
const fetchUser = async () => {
try {
const res = await fetch('/api/auth/me', {
credentials: 'include' // ✅ ADD credentials
credentials: 'include'
});
const data = await res.json();
if (data.user) {
// ✅ Check if user is NOT active → redirect to waiting room
if (!data.user.isActive) {
authStore.setUser(null);
router.replace('/waiting-room');
return;
}
// ✅ Fetch menuIds
const menuRes = await fetch(`/api/admin/user-menu-access?userId=${data.user.id}`, {
credentials: 'include' // ✅ ADD credentials
credentials: 'include'
});
const menuData = await menuRes.json();
@@ -463,7 +65,6 @@ export default function Layout({ children }: { children: React.ReactNode }) {
? [...menuData.menuIds]
: null;
// ✅ Set user dengan menuIds yang fresh
authStore.setUser({
id: data.user.id,
name: data.user.name,
@@ -472,7 +73,6 @@ export default function Layout({ children }: { children: React.ReactNode }) {
isActive: data.user.isActive
});
// ✅ IMPROVED: Redirect ONLY if di root /admin
const currentPath = window.location.pathname;
if (currentPath === '/admin') {
@@ -480,7 +80,6 @@ export default function Layout({ children }: { children: React.ReactNode }) {
console.log('🔄 Redirecting from /admin to:', expectedPath);
router.replace(expectedPath);
}
// ✅ Jangan redirect jika user sudah di path yang valid
} else {
authStore.setUser(null);
@@ -496,17 +95,17 @@ export default function Layout({ children }: { children: React.ReactNode }) {
};
fetchUser();
}, [router]); // ✅ Only depend on router
}, [router]);
const getRedirectPath = (roleId: number): string => {
switch (roleId) {
case 0: // DEVELOPER
case 1: // SUPERADMIN
case 2: // ADMIN_DESA
case 0:
case 1:
case 2:
return '/admin/landing-page/profil/program-inovasi';
case 3: // ADMIN_KESEHATAN
case 3:
return '/admin/kesehatan/posyandu';
case 4: // ADMIN_PENDIDIKAN
case 4:
return '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
default:
return '/admin';
@@ -535,7 +134,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
const response = await fetch('/api/auth/logout', {
method: 'POST',
credentials: 'include' // ✅ ADD credentials
credentials: 'include'
});
const result = await response.json();
@@ -559,6 +158,12 @@ export default function Layout({ children }: { children: React.ReactNode }) {
}
};
// ✅ Handler untuk menutup mobile menu saat navigasi
const handleNavClick = (path: string) => {
router.push(path);
close(); // Tutup mobile menu
};
return (
<AppShell
suppressHydrationWarning
@@ -573,7 +178,6 @@ export default function Layout({ children }: { children: React.ReactNode }) {
}}
padding="md"
>
{/* ... rest of your JSX (Header, Navbar, Main) sama seperti sebelumnya ... */}
<AppShellHeader
style={{
background: "linear-gradient(90deg, #ffffff, #f9fbff)",
@@ -626,16 +230,48 @@ export default function Layout({ children }: { children: React.ReactNode }) {
</AppShellHeader>
<AppShellNavbar component={ScrollArea} style={{ background: "#ffffff", borderRight: `1px solid ${colors["blue-button"]}20` }} p={{ base: 'xs', sm: 'sm' }}>
{/* ... Navbar content sama seperti sebelumnya ... */}
<AppShell.Section p="sm">
{currentNav.map((v, k) => {
const isParentActive = segments.includes(_.lowerCase(v.name));
return (
<NavLink key={k} defaultOpened={isParentActive} c={isParentActive ? colors["blue-button"] : "gray"} label={<Text fw={isParentActive ? 600 : 400} fz="sm">{v.name}</Text>} style={{ borderRadius: rem(10), marginBottom: rem(4), transition: "background 150ms ease" }} styles={{ root: { '&:hover': { backgroundColor: 'rgba(25, 113, 194, 0.05)' } } }} variant="light" active={isParentActive}>
<NavLink
key={k}
defaultOpened={isParentActive}
c={isParentActive ? colors["blue-button"] : "gray"}
label={<Text fw={isParentActive ? 600 : 400} fz="sm">{v.name}</Text>}
style={{ borderRadius: rem(10), marginBottom: rem(4), transition: "background 150ms ease" }}
styles={{ root: { '&:hover': { backgroundColor: 'rgba(25, 113, 194, 0.05)' } } }}
variant="light"
active={isParentActive}
>
{v.children.map((child, key) => {
const isChildActive = segments.includes(_.lowerCase(child.name));
return (
<NavLink key={key} href={child.path} c={isChildActive ? colors["blue-button"] : "gray"} label={<Text fw={isChildActive ? 600 : 400} fz="sm">{child.name}</Text>} styles={{ root: { borderRadius: rem(8), marginBottom: rem(2), transition: 'background 150ms ease', padding: '6px 12px', '&:hover': { backgroundColor: isChildActive ? 'rgba(25, 113, 194, 0.15)' : 'rgba(25, 113, 194, 0.05)' }, ...(isChildActive && { backgroundColor: 'rgba(25, 113, 194, 0.1)' }) } }} active={isChildActive} component={Link} />
<NavLink
key={key}
// ✅ PERBAIKAN: Gunakan onClick untuk handle navigasi dan close menu
onClick={(e) => {
e.preventDefault();
handleNavClick(child.path);
}}
href={child.path}
c={isChildActive ? colors["blue-button"] : "gray"}
label={<Text fw={isChildActive ? 600 : 400} fz="sm">{child.name}</Text>}
styles={{
root: {
borderRadius: rem(8),
marginBottom: rem(2),
transition: 'background 150ms ease',
padding: '6px 12px',
'&:hover': {
backgroundColor: isChildActive ? 'rgba(25, 113, 194, 0.15)' : 'rgba(25, 113, 194, 0.05)'
},
...(isChildActive && { backgroundColor: 'rgba(25, 113, 194, 0.1)' })
}
}}
active={isChildActive}
component={Link}
/>
);
})}
</NavLink>

View File

@@ -28,7 +28,7 @@ export default async function grafikJumlahPendudukMiskinFindMany(
where,
skip,
take: limit,
orderBy: { createdAt: "desc" },
orderBy: { year: "asc" },
}),
prisma.grafikJumlahPendudukMiskin.count({
where,

View File

@@ -1,6 +1,6 @@
'use client'
import colors from '@/con/colors';
import { Stack, Box, Paper, Text, ColorSwatch, Flex, Skeleton } from '@mantine/core';
import { Stack, Box, Paper, Text, ColorSwatch, Flex, Skeleton, Title } from '@mantine/core';
import React from 'react';
import BackButton from '../../desa/layanan/_com/BackButto';
import { BarChart } from '@mantine/charts';
@@ -32,23 +32,47 @@ function Page() {
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }} >
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
<Box px={{ base: 'md', md: 100 }}>
<Title
order={1}
ta="center"
c={colors["blue-button"]}
fw="bold"
lh={1.2}
style={{ lineHeight: 1.2 }}
>
Demografi Pekerjaan
</Title>
<Text
ta="center"
fz={{ base: 'sm', md: 'md' }}
lh={1.5}
c="black"
style={{ lineHeight: 1.5 }}
>
Desa Darmasaba memiliki komposisi penduduk yang beragam dalam sektor pekerjaan
</Text>
<Text ta={'center'} fz={'h4'}>Desa Darmasaba memiliki komposisi penduduk yang beragam dalam sektor pekerjaan</Text>
</Box>
<Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'} justify='center'>
<Paper p={'xl'}>
<Box style={{overflowX: 'scroll'}}>
<Text pb={5} fw={'bold'} fz={'h4'}>Statistik Demografi Pekerjaan Di Desa Darmasaba</Text>
<Box style={{ overflowX: 'auto' }} w={"100%"}>
<Text
pb={5}
fw={'bold'}
fz={{ base: 'md', md: 'lg' }}
lh={1.2}
c="black"
style={{ lineHeight: 1.2 }}
>
Statistik Demografi Pekerjaan Di Desa Darmasaba
</Text>
<BarChart
type='stacked'
p={10}
mb={50}
h={400}
w={Math.max(data.length * 120, 800)} // auto lebar sesuai jumlah data
w={Math.max(data.length * 120, 800)}
data={data.map((item) => ({
id: item.id,
Pekerjaan: item.pekerjaan,
@@ -62,28 +86,45 @@ function Page() {
]}
tickLine="y"
xAxisProps={{
angle: -45, // Rotate labels by -45 degrees
textAnchor: 'end', // Anchor text to the end for better alignment
height: 100, // Increase height for rotated labels
interval: 0, // Show all labels
angle: -45,
textAnchor: 'end',
height: 100,
interval: 0,
style: {
fontSize: '12px', // Adjust font size if needed
fontSize: '12px',
overflow: 'visible',
whiteSpace: 'nowrap'
whiteSpace: 'nowrap',
lineHeight: 1.4,
}
}}
/>
</Box>
<Flex pb={30} justify={'center'} gap={'xl'} align={'center'}>
<Box>
<Flex gap={{base: 7, md: 5}} align={'center'}>
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Laki-Laki</Text>
<Flex gap={{ base: 7, md: 5 }} align={'center'}>
<Text
fw={'bold'}
fz={{ base: 'sm', md: 'md' }}
lh={1.2}
c="black"
style={{ lineHeight: 1.2 }}
>
Laki-Laki
</Text>
<ColorSwatch color="#5082EE" size={30} />
</Flex>
</Box>
<Box>
<Flex gap={{base: 7, md: 5}} align={'center'}>
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Perempuan</Text>
<Flex gap={{ base: 7, md: 5 }} align={'center'}>
<Text
fw={'bold'}
fz={{ base: 'sm', md: 'md' }}
lh={1.2}
c="black"
style={{ lineHeight: 1.2 }}
>
Perempuan
</Text>
<ColorSwatch color="#6EDF9C" size={30} />
</Flex>
</Box>
@@ -95,4 +136,4 @@ function Page() {
);
}
export default Page;
export default Page;

View File

@@ -2,7 +2,7 @@
import jumlahPendudukMiskin from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-penduduk-miskin';
import colors from '@/con/colors';
import { BarChart } from '@mantine/charts';
import { Box, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { Box, Paper, Skeleton, Stack, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
@@ -17,13 +17,10 @@ function Page() {
const state = useProxy(jumlahPendudukMiskin)
const [chartData, setChartData] = useState<JPMGrafik[]>([])
useShallowEffect(() => {
state.findMany.load()
}, [])
useEffect(() => {
if (state.findMany.data) {
setChartData(state.findMany.data.map((item) => ({
@@ -48,20 +45,30 @@ function Page() {
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }} >
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
<Title
order={1}
ta={"center"}
c={colors["blue-button"]}
fw={"bold"}
lh={1.1}
>
Jumlah Penduduk Miskin
</Text>
</Title>
</Box>
<Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'} justify='center'>
<Paper p={'xl'}>
<Text fz={'h3'}>Jumlah Data Penduduk Miskin</Text>
<Text fw={"bold"} fz={'h1'}>
<Title order={3} fw={'normal'} lh={1.1}>
Jumlah Data Penduduk Miskin
</Title>
<Title order={2} fw={"bold"} lh={1.1}>
{state.findMany.data?.reduce((sum, item) => sum + (Number(item.totalPoorPopulation) || 0), 0).toLocaleString()} Orang
</Text>
</Title>
</Paper>
<Paper p={'xl'}>
<Text pb={10} fw={'bold'} fz={'h4'}>Jumlah Penduduk Miskin Per Tahun</Text>
<Title order={3} pb={10} fw={'bold'} lh={1.1}>
Jumlah Penduduk Miskin Per Tahun
</Title>
<BarChart
h={300}
data={chartData}
@@ -79,4 +86,4 @@ function Page() {
);
}
export default Page;
export default Page;

View File

@@ -3,7 +3,7 @@
import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
import colors from '@/con/colors';
import { PieChart } from '@mantine/charts';
import { Box, Center, ColorSwatch, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { Box, Center, ColorSwatch, Flex, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
@@ -56,7 +56,7 @@ function Page() {
}
}, [stateGrafikNganggurPendidikan.findMany.data])
if (!stateGrafikNganggur.findMany.data) {
if (!stateGrafikNganggur.findMany.data || !stateGrafikNganggurPendidikan.findMany.data) {
return (
<Box>
<Skeleton h={500} />
@@ -64,114 +64,151 @@ function Page() {
)
}
if (!stateGrafikNganggur.findMany.data) {
return (
<Box>
<Skeleton h={500} />
</Box>
)
}
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22" style={{ overflow: 'auto' }}>
<Box px={{ base: 'md', md: 50, lg: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 50, lg: 100 }} >
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
<Box px={{ base: 'md', md: 50, lg: 100 }}>
<Title
order={1}
ta="center"
c={colors["blue-button"]}
fw="bold"
style={{ lineHeight: 1.15 }}
>
Jumlah Penduduk Usia Kerja Yang Menganggur
</Text>
</Title>
</Box>
<Box px={{ base: "md", md: 50, lg: 100 }}>
<Stack gap={'lg'} justify='center'>
<Paper p={'lg'}>
<Text fw={'bold'} fz={'h3'}>Pengangguran Berdasarkan Usia</Text>
{mounted && donutGrafikNganggurData.length > 0 ? (<Box style={{ width: '100%', height: 'auto', minHeight: 200 }}>
<Box w="100%" maw={{ base: '100%', md: 400 }} mx="auto">
<PieChart
w="100%"
h={250} // lebih kecil biar aman di mobile
withLabelsLine
labelsPosition="outside"
labelsType="percent"
withLabels
data={donutGrafikNganggurData}
withTooltip
tooltipDataSource="segment"
/>
<Title
order={2}
fw="bold"
style={{ lineHeight: 1.2 }}
>
Pengangguran Berdasarkan Usia
</Title>
{mounted && donutGrafikNganggurData.length > 0 ? (
<Box style={{ width: '100%', height: 'auto', minHeight: 200 }}>
<Box w="100%" maw={{ base: '100%', md: 400 }} mx="auto">
<PieChart
w="100%"
h={250}
withLabelsLine
labelsPosition="outside"
labelsType="percent"
withLabels
data={donutGrafikNganggurData}
withTooltip
tooltipDataSource="segment"
/>
</Box>
</Box>
</Box>) : <Skeleton h={500} />}
) : (
<Skeleton h={500} />
)}
<Flex pb={30} justify={'center'} gap={'xl'} align={'center'} wrap="wrap">
<Box>
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>18-25</Text>
<Text fw={'bold'} fz={{ base: 'sm', md: 'md' }} style={{ lineHeight: 1.45 }}>
18-25
</Text>
<ColorSwatch color="#4b6Ef5" size={30} />
</Flex>
</Box>
<Box>
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>26-35</Text>
<Text fw={'bold'} fz={{ base: 'sm', md: 'md' }} style={{ lineHeight: 1.45 }}>
26-35
</Text>
<ColorSwatch color="#14b885" size={30} />
</Flex>
</Box>
<Box>
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>36-45</Text>
<Text fw={'bold'} fz={{ base: 'sm', md: 'md' }} style={{ lineHeight: 1.45 }}>
36-45
</Text>
<ColorSwatch color="#E6A03B" size={30} />
</Flex>
</Box>
<Box>
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>46+</Text>
<Text fw={'bold'} fz={{ base: 'sm', md: 'md' }} style={{ lineHeight: 1.45 }}>
46+
</Text>
<ColorSwatch color="#DB524D" size={30} />
</Flex>
</Box>
</Flex>
</Paper>
<Paper p={'lg'}>
<Text fw={'bold'} fz={'h3'}>Pengangguran Berdasarkan Pendidikan</Text>
{mounted2 && donutGrafikNganggurDataPendidikan.length > 0 ? (<Center>
<Box w="100%" style={{ maxWidth: 400, margin: "0 auto" }}>
<PieChart
w="100%"
h="min(250px, 50vh)" // lebih kecil biar aman di mobile
withLabelsLine
labelsPosition="outside"
labelsType="percent"
withLabels
data={donutGrafikNganggurDataPendidikan}
withTooltip
tooltipDataSource="segment"
/>
</Box>
</Center>) : <Skeleton h={500} />}
<Title
order={2}
fw="bold"
style={{ lineHeight: 1.2 }}
>
Pengangguran Berdasarkan Pendidikan
</Title>
{mounted2 && donutGrafikNganggurDataPendidikan.length > 0 ? (
<Center>
<Box w="100%" style={{ maxWidth: 400, margin: "0 auto" }}>
<PieChart
w="100%"
h="min(250px, 50vh)"
withLabelsLine
labelsPosition="outside"
labelsType="percent"
withLabels
data={donutGrafikNganggurDataPendidikan}
withTooltip
tooltipDataSource="segment"
/>
</Box>
</Center>
) : (
<Skeleton h={500} />
)}
<Flex pb={30} justify={'center'} gap={'xl'} align={'center'} wrap="wrap">
<Box>
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>SD</Text>
<Text fw={'bold'} fz={{ base: 'sm', md: 'md' }} style={{ lineHeight: 1.45 }}>
SD
</Text>
<ColorSwatch color="#4b6Ef5" size={30} />
</Flex>
</Box>
<Box>
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>SMP</Text>
<Text fw={'bold'} fz={{ base: 'sm', md: 'md' }} style={{ lineHeight: 1.45 }}>
SMP
</Text>
<ColorSwatch color="#14b885" size={30} />
</Flex>
</Box>
<Box>
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>SMA/SMK</Text>
<Text fw={'bold'} fz={{ base: 'sm', md: 'md' }} style={{ lineHeight: 1.45 }}>
SMA/SMK
</Text>
<ColorSwatch color="#E6A03B" size={30} />
</Flex>
</Box>
<Box>
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>D3</Text>
<Text fw={'bold'} fz={{ base: 'sm', md: 'md' }} style={{ lineHeight: 1.45 }}>
D3
</Text>
<ColorSwatch color="#DB524D" size={30} />
</Flex>
</Box>
<Box>
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>S1</Text>
<Text fw={'bold'} fz={{ base: 'sm', md: 'md' }} style={{ lineHeight: 1.45 }}>
S1
</Text>
<ColorSwatch color="#1018A8FF" size={30} />
</Flex>
</Box>
@@ -183,4 +220,4 @@ function Page() {
);
}
export default Page;
export default Page;

View File

@@ -36,7 +36,6 @@ function Page() {
useEffect(() => {
setMounted(true);
if (state.findMany.data) {
// Set chart data
setChartData(state.findMany.data.map((item) => ({
id: item.id,
bulan: item.month,
@@ -44,7 +43,6 @@ function Page() {
takberpendidikan: Number(item.uneducatedUnemployment),
})));
// Calculate yearly totals
const currentYearData = state.findMany.data.filter(item => item.year === currentYear);
if (currentYearData.length > 0) {
const yearlyTotal = {
@@ -72,30 +70,37 @@ function Page() {
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }} >
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
<Box px={{ base: 'md', md: 100 }}>
<Title
order={1}
ta={"center"}
c={colors["blue-button"]}
fw={"bold"}
lh={1.2}
>
Jumlah Pengangguran
</Text>
</Title>
<Group py={20} align='center' justify='space-between'>
<Text fz={'h4'} fw={"bold"}>DATA PENGANGGURAN DESA</Text>
<Title order={2} fw={"bold"} lh={1.2}>
DATA PENGANGGURAN DESA
</Title>
</Group>
</Box>
<Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'} justify='center'>
<SimpleGrid
cols={1}
pb={20}
>
<SimpleGrid cols={1} pb={20}>
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="md">
{/* Total Unemployment Card */}
<Paper px={25} py={'lg'} bg={colors['white-1']} shadow="md">
<Flex direction="column" gap="md">
<IconUserOff size={35} color={colors['blue-button']} />
<Text fz="h4" fw={600}>Total Pengangguran</Text>
<Text fz="h2" fw={700} c={colors['blue-button']}>
<Title order={3} fw={600} lh={1.2}>
Total Pengangguran
</Title>
<Text fz={{ base: 'lg', md: 'xl' }} fw={700} c={colors['blue-button']} lh={1.2}>
{yearlyData?.total.toLocaleString() || 0} Orang
</Text>
<Text fz="sm" c="dimmed">
<Text fz={{ base: 'xs', md: 'sm' }} c="dimmed" lh={1.4}>
Total data tahun {currentYear}
</Text>
</Flex>
@@ -105,11 +110,13 @@ function Page() {
<Paper px={25} py={'lg'} bg={colors['white-1']} shadow="md">
<Flex direction="column" gap="md">
<IconSchool size={35} color="#5082EE" />
<Text fz="h4" fw={600}>Pengangguran Terdidik</Text>
<Text fz="h2" fw={700} c="#5082EE">
<Title order={3} fw={600} lh={1.2}>
Pengangguran Terdidik
</Title>
<Text fz={{ base: 'lg', md: 'xl' }} fw={700} c="#5082EE" lh={1.2}>
{yearlyData?.educated.toLocaleString() || 0} Orang
</Text>
<Text fz="sm" c="dimmed">
<Text fz={{ base: 'xs', md: 'sm' }} c="dimmed" lh={1.4}>
{yearlyData ?
<>
{((yearlyData.educated / yearlyData.total) * 100).toFixed(1)}%
@@ -123,11 +130,13 @@ function Page() {
<Paper px={25} py={'lg'} bg={colors['white-1']} shadow="md">
<Flex direction="column" gap="md">
<IconSchoolOff size={35} color="#DA524C" />
<Text fz="h4" fw={600}>Pengangguran Tidak Terdidik</Text>
<Text fz="h2" fw={700} c="#DA524C">
<Title order={3} fw={600} lh={1.2}>
Pengangguran Tidak Terdidik
</Title>
<Text fz={{ base: 'lg', md: 'xl' }} fw={700} c="#DA524C" lh={1.2}>
{yearlyData?.uneducated.toLocaleString() || 0} Orang
</Text>
<Text fz="sm" c="dimmed">
<Text fz={{ base: 'xs', md: 'sm' }} c="dimmed" lh={1.4}>
{yearlyData ?
<>
{((yearlyData.uneducated / yearlyData.total) * 100).toFixed(1)}%
@@ -142,13 +151,17 @@ function Page() {
<Flex pb={30} justify={'flex-end'} gap={'xl'} align={'center'}>
<Box>
<Flex gap={{ base: 0, md: 5 }} align={'center'}>
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Pengangguran Berpendidikan</Text>
<Text fw={'bold'} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
Pengangguran Berpendidikan
</Text>
<ColorSwatch color="#5082EE" size={30} />
</Flex>
</Box>
<Box>
<Flex gap={{ base: 0, md: 5 }} align={'center'}>
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Pengangguran Tak Berpendidikan</Text>
<Text fw={'bold'} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
Pengangguran Tak Berpendidikan
</Text>
<ColorSwatch color="#DA524C" size={30} />
</Flex>
</Box>
@@ -156,15 +169,24 @@ function Page() {
{!mounted || chartData.length === 0 ? (
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}>
<Paper bg={colors['white-1']} p={'md'}>
<Title pb={10} order={3}>Data Pengangguran Terdidik dan Tidak Terdidik</Title>
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text>
<Title order={3} pb={10} lh={1.2}>
Data Pengangguran Terdidik dan Tidak Terdidik
</Title>
<Text c='dimmed' fz={{ base: 'xs', md: 'sm' }} lh={1.4}>
Belum ada data untuk ditampilkan dalam grafik
</Text>
</Paper>
</Box>
) : (
<Box style={{ width: '100%', minWidth: 300, height: 550, minHeight: 300 }}>
<Paper bg={colors['white-1']} p={'md'}>
<Title pb={10} order={4}>Data Pengangguran Terdidik dan Tidak Terdidik</Title>
<Box w={{ base: '100%', md: '70%' }}>
<Title order={3} pb={10} lh={1.2}>
Data Pengangguran Terdidik dan Tidak Terdidik
</Title>
<Box
w={{ base: '100%', md: '70%' }}
style={{ overflowX: "auto" }}
>
<BarChart
h={450}
data={chartData}
@@ -178,32 +200,55 @@ function Page() {
</Paper>
</Box>
)}
</Paper>
<Paper p={'lg'}>
<Text fw={'bold'} fz={'h4'}>Detail Data Pengangguran</Text>
<Table striped highlightOnHover>
<TableThead>
<TableTr>
<TableTh ta={'center'}>Bulan</TableTh>
<TableTh ta={'center'}>Total</TableTh>
<TableTh ta={'center'}>Terdidik</TableTh>
<TableTh ta={'center'}>Tidak Terdidik</TableTh>
<TableTh ta={'center'}>Perubahan</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{state.findMany.data?.map((item, index) => (
<TableTr key={item?.id ? String(item.id) : `row-${index}`}>
<TableTd ta={'center'}>{item.month}</TableTd>
<TableTd ta={'center'}>{item.totalUnemployment}</TableTd>
<TableTd ta={'center'}>{item.educatedUnemployment}</TableTd>
<TableTd ta={'center'}>{item.uneducatedUnemployment}</TableTd>
<TableTd ta={'center'}>{item.percentageChange}%</TableTd>
<Title order={2} fw={'bold'} fz={{ base: 'md', md: 'lg' }} lh={1.2}>
Detail Data Pengangguran
</Title>
<Box style={{ overflowX: 'auto' }}>
<Table striped highlightOnHover>
<TableThead>
<TableTr>
<TableTh ta={'center'} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
Bulan
</TableTh>
<TableTh ta={'center'} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
Total
</TableTh>
<TableTh ta={'center'} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
Terdidik
</TableTh>
<TableTh ta={'center'} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
Tidak Terdidik
</TableTh>
<TableTh ta={'center'} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
Perubahan
</TableTh>
</TableTr>
))}
</TableTbody>
</Table>
</TableThead>
<TableTbody>
{state.findMany.data?.map((item, index) => (
<TableTr key={item?.id ? String(item.id) : `row-${index}`}>
<TableTd ta={'center'} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
{item.month}
</TableTd>
<TableTd ta={'center'} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
{item.totalUnemployment}
</TableTd>
<TableTd ta={'center'} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
{item.educatedUnemployment}
</TableTd>
<TableTd ta={'center'} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
{item.uneducatedUnemployment}
</TableTd>
<TableTd ta={'center'} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
{item.percentageChange}%
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</Paper>
</Stack>
</Box>
@@ -211,4 +256,4 @@ function Page() {
);
}
export default Page;
export default Page;

View File

@@ -2,7 +2,7 @@
import lowonganKerjaState from '@/app/admin/(dashboard)/_state/ekonomi/lowongan-kerja';
import colors from '@/con/colors';
import { Box, Button, Center, Group, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { Box, Button, Center, Group, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconBrandWhatsapp, IconBriefcase, IconCurrencyDollar, IconMapPin, IconPhone } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -33,18 +33,25 @@ function DetailLowonganKerjaUser() {
);
}
const formatRupiah = (value: number) =>
new Intl.NumberFormat("id-ID", {
style: "currency",
currency: "IDR",
minimumFractionDigits: 0,
}).format(value);
return (
<Stack bg={colors.Bg} py="xl" px={{ base: 'md', md: 100 }} align="center">
<Box w={{ base: '100%', md: '70%' }}>
<Button
variant="subtle"
color="blue"
leftSection={<IconArrowBack size={20} />}
mb="md"
onClick={() => router.back()}
>
Kembali
</Button>
<Button
variant="subtle"
color="blue"
leftSection={<IconArrowBack size={20} />}
mb="md"
onClick={() => router.back()}
>
Kembali
</Button>
<Paper
radius="lg"
@@ -54,11 +61,17 @@ function DetailLowonganKerjaUser() {
bg={colors['white-1']}
>
<Stack gap="lg">
{/* Judul */}
<Text fz={{ base: '1.6rem', md: '2rem' }} fw={700} c={colors['blue-button']}>
{/* Judul Posisi - H1 */}
<Title
order={1}
c={colors['blue-button']}
style={{ lineHeight: 1.15 }}
>
{data.posisi}
</Text>
<Text c="dimmed" fz="sm">
</Title>
{/* Tanggal Posting - Caption */}
<Text c="dimmed" fz={{ base: 12, md: 'sm' }} lh={1.4}>
Diposting: {new Date(data.createdAt).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
@@ -70,44 +83,72 @@ function DetailLowonganKerjaUser() {
<Stack gap="sm" mt="md">
<Group gap="xs">
<IconBriefcase size={20} color={colors['blue-button']} />
<Text fz="md" fw={600}>{data.namaPerusahaan}</Text>
<Text
fz={{ base: 'sm', md: 'md' }}
fw={600}
lh={1.5}
>
{data.namaPerusahaan}
</Text>
</Group>
<Group gap="xs">
<IconMapPin size={20} color={colors['blue-button']} />
<Text fz="md">{data.lokasi}</Text>
<Text
fz={{ base: 'sm', md: 'md' }}
lh={1.5}
>
{data.lokasi}
</Text>
</Group>
<Group gap="xs">
<IconPhone size={20} color={colors['blue-button']} />
<Text fz="md">{data.notelp}</Text>
<Text
fz={{ base: 'sm', md: 'md' }}
lh={1.5}
>
{data.notelp}
</Text>
</Group>
<Group gap="xs">
<IconCurrencyDollar size={20} color={colors['blue-button']} />
<Text fz="md">{data.gaji || '-'}</Text>
<Text
fz={{ base: 'sm', md: 'md' }}
lh={1.5}
>
{formatRupiah(Number(data.gaji)) || '-'}
</Text>
</Group>
<Group gap="xs">
<IconBriefcase size={20} color={colors['blue-button']} />
<Text fz="md">{data.tipePekerjaan}</Text>
<Text
fz={{ base: 'sm', md: 'md' }}
lh={1.5}
>
{data.tipePekerjaan}
</Text>
</Group>
</Stack>
{/* Deskripsi Pekerjaan - H2 */}
<Box>
<Text fw={600} fz="lg" mb={4}>
<Title order={2} mb={8} style={{ lineHeight: 1.2 }}>
Deskripsi Pekerjaan
</Text>
</Title>
<Text
fz="sm"
fz={{ base: 'xs', md: 'sm' }}
lh={1.6}
style={{ wordBreak: 'break-word' }}
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
/>
</Box>
{/* Kualifikasi - H2 */}
<Box>
<Text fw={600} fz="lg" mb={4}>
<Title order={2} mb={8} style={{ lineHeight: 1.2 }}>
Kualifikasi
</Text>
</Title>
<Text
fz="sm"
fz={{ base: 'xs', md: 'sm' }}
lh={1.6}
style={{ wordBreak: 'break-word' }}
dangerouslySetInnerHTML={{ __html: data.kualifikasi || '-' }}

View File

@@ -1,7 +1,7 @@
'use client'
import lowonganKerjaState from '@/app/admin/(dashboard)/_state/ekonomi/lowongan-kerja';
import colors from '@/con/colors';
import { Box, Button, Center, Flex, Group, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
import { Box, Button, Center, Flex, Group, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from '@mantine/core';
import { useDebouncedValue } from '@mantine/hooks';
import { IconBriefcase, IconClock, IconMapPin, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
@@ -53,17 +53,19 @@ function Page() {
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }} pb={80}>
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
<Title order={1} ta="center" c={colors["blue-button"]} fw="bold" lh={1.15}>
Lowongan Kerja Lokal
</Text>
</Title>
<Group justify='center'>
<TextInput
radius={'xl'}
w={{ base: 500, md: 700 }}
w={{ base: '100%', md: 700 }}
placeholder='Cari Pekerjaan'
leftSection={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
fz={{ base: 'sm', md: 'md' }}
lh={1.5}
/>
</Group>
</Box>
@@ -80,30 +82,42 @@ function Page() {
<Paper key={k} p={'xl'}>
<Stack gap={'md'}>
<Box>
<Flex gap={'xl'} align={'center'}>
<IconBriefcase color={colors['blue-button']} size={50} />
<Flex gap={{ base: 'md', md: 'xl' }} align={'center'}>
<IconBriefcase color={colors['blue-button']} size={40} />
<Box>
<Text fw={'bold'} fz={'h4'} c={colors['blue-button']}>{v.posisi}</Text>
<Text fz={'h4'}>{v.namaPerusahaan}</Text>
<Text fw={'bold'} fz={{ base: 'lg', md: 'h4' }} c={colors['blue-button']} lh={1.3}>
{v.posisi}
</Text>
<Text fz={{ base: 'md', md: 'h4' }} lh={1.5}>
{v.namaPerusahaan}
</Text>
</Box>
</Flex>
</Box>
<Box>
<Flex gap={'xl'} align={'center'}>
<IconMapPin color={colors['blue-button']} size={50} />
<Text fz={'h4'}>{v.lokasi}</Text>
<Flex gap={{ base: 'md', md: 'xl' }} align={'center'}>
<IconMapPin color={colors['blue-button']} size={40} />
<Text fz={{ base: 'md', md: 'h4' }} lh={1.5}>
{v.lokasi}
</Text>
</Flex>
</Box>
<Box>
<Flex gap={'xl'} align={'center'}>
<IconClock color={colors['blue-button']} size={50} />
<Flex gap={{ base: 'md', md: 'xl' }} align={'center'}>
<IconClock color={colors['blue-button']} size={40} />
<Box>
<Text fw={'bold'} fz={'h4'} c={colors['blue-button']}>Full Time</Text>
<Text fz={'h4'}>{formatCurrency(v.gaji)}</Text>
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }} c={colors['blue-button']} lh={1.3}>
Full Time
</Text>
<Text fz={{ base: 'sm', md: 'h4' }} lh={1.5}>
{formatCurrency(v.gaji)}
</Text>
</Box>
</Flex>
</Box>
<Button onClick={() => router.push(`/darmasaba/ekonomi/lowongan-kerja-lokal/${v.id}`)}>Detail</Button>
<Button onClick={() => router.push(`/darmasaba/ekonomi/lowongan-kerja-lokal/${v.id}`)} fz={{ base: 'sm', md: 'md' }} lh={1.5}>
Detail
</Button>
</Stack>
</Paper>
)
@@ -123,4 +137,4 @@ function Page() {
);
}
export default Page;
export default Page;

View File

@@ -1,7 +1,7 @@
'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 { Box, Button, Paper, Stack, Text, Image, Skeleton, Group, Badge, Divider, Title } from '@mantine/core';
import { IconArrowBack, IconBrandWhatsapp, IconMapPin, IconPhone, IconStar } from '@tabler/icons-react';
import { useRouter, useParams } from 'next/navigation';
import React from 'react';
import { useProxy } from 'valtio/utils';
@@ -31,14 +31,16 @@ function DetailProdukPasarUser() {
<Box py={20}>
{/* Tombol kembali */}
<Box px={{ base: 'md', md: 100 }}>
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={20} color={colors['blue-button']} />}
mb={15}
>
Kembali ke daftar produk
</Button>
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={20} color={colors['blue-button']} />}
mb={15}
>
<Text fz={{ base: 'md', md: 'lg' }} lh={1.5}>
Kembali ke daftar produk
</Text>
</Button>
</Box>
<Paper
@@ -65,26 +67,31 @@ function DetailProdukPasarUser() {
<Box
h={300}
bg="gray.1"
display="flex"
style={{ alignItems: 'center', justifyContent: 'center', borderRadius: 'md' }}
style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 'var(--mantine-radius-md)' }}
>
<Text c="dimmed">Tidak ada gambar</Text>
<Text c="dimmed" fz={{ base: 'sm', md: 'md' }} lh={1.5}>
Tidak ada gambar
</Text>
</Box>
)}
{/* Detail Produk */}
<Stack gap="xs">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
<Title order={2} lh={1.1} c={colors['blue-button']}>
{data.nama || 'Produk Tanpa Nama'}
</Text>
</Title>
<Group>
<Badge color="green" size="lg" radius="md">
Rp {data.harga?.toLocaleString('id-ID')}
<Text c={"white"} fz={{ base: 'sm', md: 'md' }} fw={600} lh={1.4}>
Rp {data.harga?.toLocaleString('id-ID')}
</Text>
</Badge>
{data.rating && (
<Group gap={4}>
<IconStar size={18} color="#FFD43B" />
<Text fz="md" fw={500}>{data.rating}</Text>
<Text fz={{ base: 'sm', md: 'md' }} fw={500} lh={1.5}>
{data.rating}
</Text>
</Group>
)}
</Group>
@@ -95,16 +102,20 @@ function DetailProdukPasarUser() {
{/* Info Tambahan */}
<Stack gap="sm">
<Box>
<Text fz="lg" fw={600}>Kategori</Text>
<Title order={3} lh={1.15}>
Kategori
</Title>
<Group gap="xs" mt={4}>
{data.KategoriToPasar && data.KategoriToPasar.length > 0 ? (
data.KategoriToPasar.map((kategori) => (
<Badge key={kategori.id} color="blue" variant="light">
<Badge key={kategori.id} color="blue" variant="light" fz={{ base: 'xs', md: 'sm' }} lh={1.4}>
{kategori.kategori.nama}
</Badge>
))
) : (
<Text fz="sm" c="dimmed">Tidak ada kategori</Text>
<Text fz={{ base: 'xs', md: 'sm' }} c="dimmed" lh={1.5}>
Tidak ada kategori
</Text>
)}
</Group>
</Box>
@@ -112,14 +123,18 @@ function DetailProdukPasarUser() {
{data.alamatUsaha && (
<Group gap={6}>
<IconMapPin size={18} color={colors['blue-button']} />
<Text fz="md">{data.alamatUsaha}</Text>
<Text fz={{ base: 'sm', md: 'md' }} lh={1.5}>
{data.alamatUsaha}
</Text>
</Group>
)}
{data.kontak && (
<Group gap={6}>
<IconPhone size={18} color={colors['blue-button']} />
<Text fz="md">{data.kontak}</Text>
<Text fz={{ base: 'sm', md: 'md' }} lh={1.5}>
{data.kontak}
</Text>
</Group>
)}
</Stack>
@@ -128,8 +143,10 @@ function DetailProdukPasarUser() {
{/* Deskripsi */}
<Box>
<Text fz="lg" fw={600}>Deskripsi Produk</Text>
<Text fz="md" c="dimmed" mt={4}>
<Title order={3} lh={1.15}>
Deskripsi Produk
</Title>
<Text fz={{ base: 'sm', md: 'md' }} c="dimmed" mt={4} lh={1.5}>
Tidak ada deskripsi.
</Text>
</Box>
@@ -144,8 +161,11 @@ function DetailProdukPasarUser() {
component="a"
href={`https://wa.me/${data.kontak.replace(/[^0-9]/g, '')}`}
target="_blank"
leftSection={<IconBrandWhatsapp/>}
>
Hubungi Penjual via WhatsApp
<Text c={"white"} fz={{ base: 'sm', md: 'md' }} lh={1.5}>
Hubungi Penjual via WhatsApp
</Text>
</Button>
)}
</Stack>
@@ -154,4 +174,4 @@ function DetailProdukPasarUser() {
);
}
export default DetailProdukPasarUser;
export default DetailProdukPasarUser;

View File

@@ -7,7 +7,7 @@ import { IconBrandWhatsapp, IconMapPinFilled, IconSearch, IconStarFilled } from
import { motion } from 'motion/react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto';
function Page() {
@@ -30,8 +30,8 @@ function Page() {
const filteredData = selectedCategory
? data?.filter(item =>
item.KategoriToPasar?.some(kategori => kategori.kategoriId === selectedCategory)
)
item.KategoriToPasar?.some(kategori => kategori.kategoriId === selectedCategory)
)
: data;
useShallowEffect(() => {
@@ -55,7 +55,7 @@ function Page() {
<Box>
<Grid align="center" px={{ base: 'md', md: 100 }}>
<GridCol span={{ base: 12, md: 9 }}>
<Title order={1} c={colors["blue-button"]} fw="bold">
<Title order={1} c={colors["blue-button"]} fw="bold" lh={1.15}>
Pasar Desa
</Title>
</GridCol>
@@ -71,7 +71,14 @@ function Page() {
</GridCol>
</Grid>
<Text px={{ base: 'md', md: 100 }} pt={20} ta="justify" fz={{ base: 'sm', md: 'md' }}>
<Text
px={{ base: 'md', md: 100 }}
pt={20}
ta="justify"
fz={{ base: 'sm', md: 'md' }}
lh={{ base: 1.5, md: 1.55 }}
c="black"
>
Pasar Desa Online adalah media promosi untuk membantu warga memasarkan dan memperkenalkan produk mereka.
</Text>
</Box>
@@ -92,6 +99,9 @@ function Page() {
searchable
nothingFoundMessage="Tidak ada kategori ditemukan"
style={{ width: '100%' }}
fz={{ base: 'sm', md: 'md' }}
lh={{ base: 1.5, md: 1.55 }}
c="black"
/>
</Box>
</SimpleGrid>
@@ -114,15 +124,29 @@ function Page() {
style={{ objectFit: 'cover' }}
loading="lazy"
/>
<Text py="sm" fw="bold" fz={{ base: 'md', md: 'lg' }}>
<Text
py="sm"
fw="bold"
fz={{ base: 'md', md: 'lg' }}
lh={{ base: 1.3, md: 1.25 }}
c="black"
>
{v.nama}
</Text>
<Text fz={{ base: 'sm', md: 'md' }}>
<Text
fz={{ base: 'sm', md: 'md' }}
lh={{ base: 1.5, md: 1.55 }}
c="black"
>
Rp {v.harga.toLocaleString('id-ID')}
</Text>
<Flex py="sm" gap="md">
<Flex py="sm" gap="md" align="center">
<IconStarFilled size={20} color="#EBCB09" />
<Text fz={{ base: 'xs', md: 'sm' }} ml={2}>
<Text
fz={{ base: 'xs', md: 'sm' }}
lh={{ base: 1.4, md: 1.45 }}
c="black"
>
{v.rating}
</Text>
</Flex>
@@ -130,7 +154,11 @@ function Page() {
<Box>
<Flex gap="md" align="center">
<IconMapPinFilled size={20} color="red" />
<Text fz={{ base: 'xs', md: 'sm' }} ml={2}>
<Text
fz={{ base: 'xs', md: 'sm' }}
lh={{ base: 1.4, md: 1.45 }}
c="black"
>
{v.alamatUsaha}
</Text>
</Flex>

View File

@@ -69,48 +69,47 @@ function Page() {
}
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Stack pos="relative" bg={colors.Bg} py={{ base: 'xl', md: 'xl' }} gap={'22'}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }}>
<Grid align='center'>
<Grid align="center">
<GridCol span={{ base: 12, md: 9 }}>
<Title
order={1}
c={colors["blue-button"]}
fw={"bold"}
fz={{ base: '28px', md: '32px' }}
lh={{ base: '1.2', md: '1.25' }}
fw="bold"
lh={{ base: 1.2, md: 1.2 }}
>
Program Kemiskinan
</Title>
</GridCol>
<GridCol span={{ base: 12, md: 3 }}>
<TextInput
radius={"lg"}
placeholder='Cari Program'
radius="lg"
placeholder="Cari Program"
value={search}
onChange={(e) => setSearch(e.target.value)}
leftSection={<IconSearch size={20} />}
w={{ base: "50%", md: "100%" }}
w="100%"
/>
</GridCol>
</Grid>
<Text
fz={{ base: '14px', md: '16px' }}
lh={{ base: '1.5', md: '1.6' }}
fz={{ base: 'sm', md: 'md' }}
lh={{ base: 1.5, md: 1.6 }}
c="black"
ta={{ base: 'left', md: 'left' }}
pt={20}
ta="left"
pt={{ base: 'sm', md: 20 }}
>
Berbagai program bantuan untuk mengurangi kemiskinan dan meningkatkan kesejahteraan masyarakat
</Text>
</Box>
<Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'} justify='center'>
<Stack gap={'lg'} justify="center">
<SimpleGrid
pb={10}
pb={{ base: 'md', md: 10 }}
cols={{
base: 1,
md: 2
@@ -118,20 +117,19 @@ function Page() {
>
{state.findMany.data.map(v => {
return (
<Paper p={'xl'} key={v.id}>
<Paper p={{ base: 'lg', md: 'xl' }} key={v.id}>
<Title
order={3}
fw={'bold'}
fw="bold"
c={colors['blue-button']}
fz={{ base: '18px', md: '20px' }}
lh={{ base: '1.3', md: '1.35' }}
lh={{ base: 1.2, md: 1.2 }}
>
{v.nama}
</Title>
<Text
fz={{ base: '14px', md: '16px' }}
lh={{ base: '1.5', md: '1.6' }}
c={'black'}
fz={{ base: 'sm', md: 'md' }}
lh={{ base: 1.5, md: 1.6 }}
c="black"
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
/>
@@ -139,7 +137,7 @@ function Page() {
)
})}
</SimpleGrid>
<Center my={10}>
<Center my={{ base: 'md', md: 10 }}>
<Pagination
value={page}
onChange={(newPage) => {
@@ -147,16 +145,15 @@ function Page() {
window.scrollTo({ top: 0, behavior: 'smooth' })
}}
total={totalPages}
my={"md"}
my="md"
/>
</Center>
<Paper p={'xl'}>
<Paper p={{ base: 'lg', md: 'xl' }}>
<Title
order={3}
fw={'bold'}
fw="bold"
c={colors['blue-button']}
fz={{ base: '18px', md: '20px' }}
lh={{ base: '1.3', md: '1.35' }}
lh={{ base: 1.2, md: 1.2 }}
mb="md"
>
Statistik Kemiskinan Masyarakat
@@ -166,7 +163,7 @@ function Page() {
<Box w="100%" style={{ overflowX: 'auto' }}>
<Center>
<RechartsLineChart
width={Math.min(800, window.innerWidth - 100)}
width={Math.min(800, typeof window !== 'undefined' ? window.innerWidth - 100 : 800)}
height={400}
data={statistikData}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
@@ -175,10 +172,12 @@ function Page() {
<XAxis
dataKey="tahun"
label={{ value: 'Tahun', position: 'insideBottomRight', offset: -5 }}
tick={{ fontSize: 12 }}
/>
<YAxis
label={{ value: 'Jumlah', angle: -90, position: 'insideLeft' }}
domain={[0, 'auto']}
tick={{ fontSize: 12 }}
/>
<Tooltip
formatter={(value) => [`${value} orang`, 'Jumlah']}
@@ -199,9 +198,9 @@ function Page() {
) : (
<Box p="md" ta="center" bg="gray.0" style={{ borderRadius: '8px' }}>
<Text
fz={{ base: '12px', md: '14px' }}
fz={{ base: 'xs', md: 'sm' }}
c="dimmed"
lh={{ base: '1.4', md: '1.5' }}
lh={{ base: 1.4, md: 1.4 }}
>
{state.findMany.loading
? 'Memuat data statistik...'

View File

@@ -1,6 +1,6 @@
'use client'
import colors from '@/con/colors';
import { Stack, Box, Text, Paper, Skeleton } from '@mantine/core';
import { Stack, Box, Text, Paper, Skeleton, Center, Title } from '@mantine/core';
import React from 'react';
import BackButton from '../../desa/layanan/_com/BackButto';
import { BarChart } from '@mantine/charts';
@@ -28,16 +28,15 @@ function Page() {
)
}
// Add this check before the return statement
if (data.length === 0) {
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap={22}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
<Text fz={{ base: 'h1', md: '2.5rem' }} c={colors['blue-button']} fw="bold">
<Title order={1} c={colors['blue-button']} fw="bold">
Sektor Unggulan Desa Darmasaba
</Text>
<Text c="dimmed" mt="md">
</Title>
<Text c="dimmed" mt="md" fz={{ base: 'sm', md: 'md' }} lh={1.5}>
Data sektor unggulan belum tersedia
</Text>
</Box>
@@ -53,53 +52,72 @@ function Page() {
Ton: item.value,
}));
const chartWidth = Math.max(600, chartData.length * 150); // contoh: 150px per bar
const chartWidth = Math.max(600, chartData.length * 150);
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }} >
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
<Box px={{ base: 'md', md: 100 }}>
<Title ta="center" order={1} c={colors['blue-button']} fw="bold">
Sektor Unggulan Desa Darmasaba
</Title>
<Text
ta="center"
fz={{ base: 'sm', md: 'md' }}
lh={{ base: 1.5, md: 1.6 }}
mt="sm"
>
Desa Darmasaba dikenal sebagai desa dengan potensi unggulan di sektor pertanian dan peternakan
</Text>
<Text ta={'center'} fz={'h4'}> Desa Darmasaba dikenal sebagai desa dengan potensi unggulan di sektor pertanian dan peternakan</Text>
</Box>
<Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'} justify='center'>
<Box px={{ base: 'md', md: 100 }}>
<Stack gap="lg" justify="center">
{data.map((v, k) => {
return (
<Paper p={'xl'} key={k}>
<Text fw={'bold'} fz={'h4'}>{v.name}</Text>
<Text fz={'h4'} ta={'justify'} style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: v.description || '' }} />
<Paper p="xl" key={k}>
<Title order={3} fw="bold">
{v.name}
</Title>
<Text
ta="justify"
fz={{ base: 'sm', md: 'md' }}
lh={{ base: 1.5, md: 1.6 }}
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
dangerouslySetInnerHTML={{ __html: v.description || '' }}
/>
</Paper>
)
);
})}
<Box style={{ width: '100%', overflowX: 'auto' }}>
<Paper p="xl">
<Text pb={10} fw="bold" fz="h4">Statistik Sektor Unggulan Darmasaba</Text>
<Title order={3} fw="bold" pb="md">
Statistik Sektor Unggulan Darmasaba
</Title>
<Box style={{ width: '100%', overflowX: 'auto', maxWidth: `${chartWidth}px` }}>
<BarChart
p={10}
h={300}
data={chartData}
dataKey="sektor"
series={[
{ name: 'Ton', color: colors['blue-button'] },
]}
tickLine="y"
tooltipAnimationDuration={200}
withTooltip
withXAxis
withYAxis
xAxisLabel="Sektor"
yAxisLabel="Ton"
style={{
fontFamily: 'inherit',
fontSize: '12px', // ukuran font lebih kecil di mobile
}}
/>
<Center>
<BarChart
p={10}
h={300}
data={chartData}
dataKey="sektor"
series={[
{ name: 'Ton', color: colors['blue-button'] },
]}
tickLine="y"
tooltipAnimationDuration={200}
withTooltip
withXAxis
withYAxis
xAxisLabel="Sektor"
yAxisLabel="Ton"
style={{
fontFamily: 'inherit',
fontSize: '12px',
}}
/>
</Center>
</Box>
</Paper>
</Box>
@@ -109,4 +127,4 @@ function Page() {
);
}
export default Page;
export default Page;

View File

@@ -0,0 +1,174 @@
'use client';
import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
import colors from '@/con/colors';
import {
Box,
Divider,
Group,
Image,
Paper,
Skeleton,
Stack,
Text,
Title,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
function DetailPegawaiBumdes() {
const statePegawai = useProxy(stateStrukturBumDes.pegawai);
const params = useParams();
const router = useRouter();
useShallowEffect(() => {
stateStrukturBumDes.posisiOrganisasi.findMany.load();
statePegawai.findUnique.load(params?.id as string);
}, []);
if (!statePegawai.findUnique.data) {
return (
<Stack py="lg">
<Skeleton height={500} radius="md" />
</Stack>
);
}
const data = statePegawai.findUnique.data;
return (
<Box px={{ base: 'md', md: 100 }} py="xl">
{/* Back button */}
<Group mb="lg" px={{ base: 'md', md: 100 }}>
<Box
onClick={() => router.back()}
style={{
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: 8,
}}
>
<IconArrowBack size={22} color={colors['blue-button']} />
<Text fz={{ base: 'sm', md: 'md' }} lh="1.4" fw={500} c={colors['blue-button']}>
Kembali
</Text>
</Box>
</Group>
<Paper
w={{ base: '100%', md: '70%' }}
mx="auto"
p="xl"
radius="lg"
shadow="sm"
bg="white"
style={{ border: '1px solid #eaeaea' }}
>
<Stack align="center" gap="md">
{/* Foto Profil */}
<Image
src={data.image?.link || '/placeholder-profile.png'}
alt={data.namaLengkap || 'Foto Profil'}
w={160}
h={160}
radius={100}
fit="cover"
style={{ border: `2px solid ${colors['blue-button']}` }}
loading="lazy"
/>
{/* Nama & Jabatan */}
<Stack align="center" gap={2}>
<Title
order={2}
c={colors['blue-button']}
fw={700}
fz={{ base: 'xl', md: '28px' }}
lh="1.2"
ta="center"
>
{data.namaLengkap || '-'} {data.gelarAkademik || ''}
</Title>
<Text
fz={{ base: 'sm', md: 'md' }}
lh="1.4"
c="dimmed"
ta="center"
>
{data.posisi?.nama || 'Posisi tidak tersedia'}
</Text>
</Stack>
</Stack>
<Divider my="lg" />
{/* Informasi Detail */}
<Stack gap="md">
<InfoRow label="Email" value={data.email} />
<InfoRow label="Telepon" value={data.telepon} />
<InfoRow label="Alamat" value={data.alamat} multiline />
<InfoRow
label="Tanggal Masuk"
value={
data.tanggalMasuk
? new Date(data.tanggalMasuk).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric',
})
: '-'
}
/>
<InfoRow
label="Status"
value={data.isActive ? 'Aktif' : 'Tidak Aktif'}
valueColor={data.isActive ? 'green' : 'red'}
/>
</Stack>
</Paper>
</Box>
);
}
/* Komponen Baris Informasi */
function InfoRow({
label,
value,
valueColor,
multiline = false,
}: {
label: string;
value?: string | null;
valueColor?: string;
multiline?: boolean;
}) {
return (
<Box>
<Text
fz={{ base: 'sm', md: 'md' }}
fw={600}
lh="1.3"
c="dark"
>
{label}
</Text>
<Text
fz={{ base: 'sm', md: 'md' }}
lh="1.5"
c={valueColor || 'dimmed'}
style={{
whiteSpace: multiline ? 'normal' : 'nowrap',
wordBreak: 'break-word',
}}
>
{value || '-'}
</Text>
</Box>
);
}
export default DetailPegawaiBumdes;

View File

@@ -1,7 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi'
import colors from '@/con/colors'
import {
@@ -32,12 +31,13 @@ import {
IconZoomOut,
} from '@tabler/icons-react'
import { debounce } from 'lodash'
import { useTransitionRouter } from 'next-view-transitions'
import { OrganizationChart } from 'primereact/organizationchart'
import { useEffect, useRef, useState } from 'react'
import { useProxy } from 'valtio/utils'
import BackButton from '../../desa/layanan/_com/BackButto'
import '../../ppid/struktur-ppid/struktur.css'
import { useMediaQuery } from '@mantine/hooks'
import { useTransitionRouter } from 'next-view-transitions'
export default function Page() {
return (
@@ -49,14 +49,16 @@ export default function Page() {
paddingBottom: 48,
}}
>
<Box px={{ base: 'md', md: 100 }} py="xl">
<Box px={{ base: 'md', md: 100 }} py={"xl"}>
<BackButton />
<Stack align="center" gap="xl" mt="xl">
<Title
order={1}
ta="center"
c={colors['blue-button']}
fz={{ base: 28, md: 36 }}
fz={{ base: 28, md: 36, lg: 44 }}
lh={{ base: 1.05, md: 1.03 }}
>
Struktur Organisasi & SK Pengurus BumDes
</Title>
@@ -75,14 +77,18 @@ export default function Page() {
}
function StrukturOrganisasiBumDes() {
const router = useTransitionRouter()
const stateOrganisasi: any = useProxy(stateStrukturBumDes.pegawai)
const router = useTransitionRouter()
const chartContainerRef = useRef<HTMLDivElement>(null)
const [scale, setScale] = useState(1)
const [isFullscreen, setFullscreen] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
// debounce pencarian
const debouncedSearch = useRef(
debounce((value: string) => setSearchQuery(value), 1000)
debounce((value: string) => {
setSearchQuery(value)
}, 1000)
).current
useEffect(() => {
@@ -90,8 +96,7 @@ function StrukturOrganisasiBumDes() {
}, [])
const isLoading =
!stateOrganisasi.findMany.data &&
stateOrganisasi.findMany.loading !== false
!stateOrganisasi.findMany.data && stateOrganisasi.findMany.loading !== false
if (isLoading) {
return (
@@ -149,7 +154,7 @@ function StrukturOrganisasiBumDes() {
)
}
// 📊 susun struktur organisasi
// 🧩 buat struktur organisasi
const posisiMap = new Map<string, any>()
const aktifPegawai = data.filter((p: any) => p.isActive)
@@ -183,7 +188,6 @@ function StrukturOrganisasiBumDes() {
name: pegawai?.namaLengkap || 'Belum Ditugaskan',
title: node.nama || 'Tanpa Jabatan',
image: pegawai?.image?.link || '/img/default.png',
description: node.deskripsi || '',
},
children: node.children?.map(toOrgChartFormat) || [],
}
@@ -208,7 +212,7 @@ function StrukturOrganisasiBumDes() {
chartData = filterNodes(chartData)
}
// 🔍 fullscreen dan zoom control
// 🎬 fullscreen & zoom control
const toggleFullscreen = () => {
if (!document.fullscreenElement) {
chartContainerRef.current?.requestFullscreen()
@@ -225,7 +229,7 @@ function StrukturOrganisasiBumDes() {
return (
<Stack align="center" mt="xl">
{/* 🧭 Kontrol atas */}
{/* 🔍 Controls */}
<Paper
shadow="xs"
w={{
@@ -244,6 +248,7 @@ function StrukturOrganisasiBumDes() {
overflowX: 'auto' // ⬅️ untuk mencegah overflow
}}
>
<Stack gap="sm">
<Group justify='center'>
<TextInput
@@ -360,165 +365,163 @@ function StrukturOrganisasiBumDes() {
</Stack>
</Paper>
{/* 🧩 Chart Container */}
<Center style={{ width: '100%' }}>
<Box
ref={chartContainerRef}
{/* 🧩 Chart Container */}
<Center style={{ width: '100%' }}>
<Box
ref={chartContainerRef}
style={{
overflowX: 'auto',
overflowY: 'auto',
width: '100%',
maxWidth: '100%',
padding: '32px 16px',
transition: 'transform 0.2s ease',
}}
>
<Box style={{
transform: `scale(${scale})`,
transformOrigin: 'center top',
display: 'inline-block', // 👈 agar tidak memenuhi lebar parent
minWidth: 'min-content', // 👈 penting agar chart tidak dipaksa muat di width 100%
}}>
<OrganizationChart
value={chartData}
nodeTemplate={(node) => <NodeCard node={node} router={router} />}
className="p-organizationchart p-organizationchart-horizontal"
/>
</Box>
</Box>
</Center>
</Stack>
)
}
function NodeCard({ node, router }: any) {
const imageSrc = node?.data?.image || '/img/default.png'
const name = node?.data?.name || 'Tanpa Nama'
const title = node?.data?.title || 'Tanpa Jabatan'
const hasId = Boolean(node?.data?.id)
const isMobile = useMediaQuery("(max-width: 768px)");
return (
<Transition mounted transition="pop" duration={300}>
{(styles) => (
<Card
shadow="md"
radius="xl"
withBorder
style={{
...styles,
width: '100%',
maxWidth: isMobile ? 200 : 240, // lebih kecil di mobile
minHeight: isMobile ? 240 : 280,
padding: isMobile ? 16 : 20,
background: 'linear-gradient(135deg, rgba(28,110,164,0.15) 0%, rgba(255,255,255,0.95) 100%)',
borderColor: 'rgba(28, 110, 164, 0.3)',
borderWidth: 2,
transition: 'all 0.3s ease',
cursor: hasId ? 'pointer' : 'default',
}}
onMouseEnter={(e) => {
if (hasId) {
e.currentTarget.style.transform = 'translateY(-4px)'
e.currentTarget.style.boxShadow = '0 8px 24px rgba(28, 110, 164, 0.25)'
}
}}
onMouseLeave={(e) => {
if (hasId) {
e.currentTarget.style.transform = 'translateY(0)'
e.currentTarget.style.boxShadow = ''
}
}}
>
<Stack align="center" gap={12}>
{/* Photo */}
<Box
style={{
width: 96,
height: 96,
borderRadius: '50%',
overflow: 'hidden',
border: '3px solid rgba(28, 110, 164, 0.4)',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
background: 'white',
}}
>
<Image
src={imageSrc}
alt={name}
width={96}
height={96}
fit="cover"
loading="lazy"
style={{
overflowX: 'auto',
overflowY: 'auto',
width: '100%',
maxWidth: '100%',
padding: '32px 16px',
transition: 'transform 0.2s ease',
objectFit: 'cover',
}}
/>
</Box>
{/* Name */}
<Text
fw={700}
ta="center"
c={colors['blue-button']}
lineClamp={2}
fz={{ base: 13, md: 15 }}
lh={1.2}
style={{
minHeight: 40,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
wordBreak: 'break-word',
}}
>
{name}
</Text>
{/* Title/Position */}
<Text
c="dimmed"
ta="center"
fw={500}
lineClamp={2}
fz={{ base: 12, md: 13 }}
lh={1.3}
style={{
minHeight: 32,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
wordBreak: 'break-word',
}}
>
{title}
</Text>
{/* Detail Button */}
{hasId && (
<Button
variant="gradient"
gradient={{ from: 'blue', to: 'cyan' }}
size="xs"
fullWidth
mt={8}
radius="md"
onClick={() =>
router.push(`/darmasaba/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/${node.data.id}`)
}
style={{
height: 32,
fontWeight: 600,
}}
>
<Box style={{
transform: `scale(${scale})`,
transformOrigin: 'center top',
display: 'inline-block', // 👈 agar tidak memenuhi lebar parent
minWidth: 'min-content', // 👈 penting agar chart tidak dipaksa muat di width 100%
}}>
<OrganizationChart
value={chartData}
nodeTemplate={(node) => <NodeCard node={node} router={router} />}
className="p-organizationchart p-organizationchart-horizontal"
/>
</Box>
</Box>
</Center>
</Stack>
)
}
function NodeCard({ node, router }: any) {
const imageSrc = node?.data?.image || '/img/default.png'
const name = node?.data?.name || 'Tanpa Nama'
const title = node?.data?.title || 'Tanpa Jabatan'
const hasId = Boolean(node?.data?.id)
const isMobile = useMediaQuery("(max-width: 768px)");
return (
<Transition mounted transition="pop" duration={300}>
{(styles) => (
<Card
shadow="md"
radius="xl"
withBorder
style={{
...styles,
width: '100%',
maxWidth: isMobile ? 200 : 240, // lebih kecil di mobile
minHeight: isMobile ? 240 : 280,
padding: isMobile ? 16 : 20,
background: 'linear-gradient(135deg, rgba(28,110,164,0.15) 0%, rgba(255,255,255,0.95) 100%)',
borderColor: 'rgba(28, 110, 164, 0.3)',
borderWidth: 2,
transition: 'all 0.3s ease',
cursor: hasId ? 'pointer' : 'default',
}}
onMouseEnter={(e) => {
if (hasId) {
e.currentTarget.style.transform = 'translateY(-4px)'
e.currentTarget.style.boxShadow = '0 8px 24px rgba(28, 110, 164, 0.25)'
}
}}
onMouseLeave={(e) => {
if (hasId) {
e.currentTarget.style.transform = 'translateY(0)'
e.currentTarget.style.boxShadow = ''
}
}}
>
<Stack align="center" gap={12}>
{/* Photo */}
<Box
style={{
width: 96,
height: 96,
borderRadius: '50%',
overflow: 'hidden',
border: '3px solid rgba(28, 110, 164, 0.4)',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
background: 'white',
}}
>
<Image
src={imageSrc}
alt={name}
width={96}
height={96}
fit="cover"
loading="lazy"
style={{
objectFit: 'cover',
}}
/>
</Box>
{/* Name */}
<Text
fw={700}
ta="center"
c={colors['blue-button']}
lineClamp={2}
fz={{ base: 13, md: 15 }}
lh={1.2}
style={{
minHeight: 40,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
wordBreak: 'break-word',
}}
>
{name}
</Text>
{/* Title/Position */}
<Text
c="dimmed"
ta="center"
fw={500}
lineClamp={2}
fz={{ base: 12, md: 13 }}
lh={1.3}
style={{
minHeight: 32,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
wordBreak: 'break-word',
}}
>
{title}
</Text>
{/* Detail Button */}
{hasId && (
<Button
variant="gradient"
gradient={{ from: 'blue', to: 'cyan' }}
size="xs"
fullWidth
mt={8}
radius="md"
onClick={() =>
router.push(`/darmasaba/ppid/struktur-ppid/${node.data.id}`)
}
style={{
height: 32,
fontWeight: 600,
}}
>
<Text fz={{ base: 12, md: 13 }} lh={1} ta="center">Lihat Detail</Text>
</Button>
)}
</Stack>
</Card>
<Text c={"white"} fz={{ base: 12, md: 13 }} lh={1} ta="center">Lihat Detail</Text>
</Button>
)}
</Transition>
)
}
</Stack>
</Card>
)}
</Transition>
)
}

View File

@@ -54,7 +54,7 @@ function Page() {
value={search}
onChange={(e) => setSearch(e.target.value)}
leftSection={<IconSearch size={20} />}
w={{ base: "50%", md: "100%" }}
w={"100%"}
/>
</GridCol>
</Grid>

View File

@@ -1,6 +1,6 @@
'use client'
import { Box, Center, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'
import { Box, Center, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core'
import { useShallowEffect } from '@mantine/hooks'
import { useParams } from 'next/navigation'
import { useProxy } from 'valtio/utils'
@@ -31,59 +31,62 @@ function DetailKeamananLingkunganUser() {
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="lg">
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
{/* Wrapper Detail */}
<Paper
withBorder
w={{ base: '100%', md: '80%' }}
mx="auto"
bg={colors['white-1']}
p="xl"
radius="lg"
shadow="md"
>
<Stack gap="lg">
{/* Judul */}
<Text
ta="center"
fz={{ base: 'xl', md: '2xl' }}
fw={700}
c={colors['blue-button']}
>
{data?.name || 'Tanpa Judul'}
</Text>
{/* Wrapper Detail */}
<Paper
withBorder
w={{ base: '100%', md: '80%' }}
mx="auto"
bg={colors['white-1']}
p="xl"
radius="lg"
shadow="md"
>
<Stack gap="lg">
{/* Judul */}
<Title
order={1}
ta="center"
c={colors['blue-button']}
style={{ lineHeight: 1.15 }}
>
{data?.name || 'Tanpa Judul'}
</Title>
{/* Gambar */}
<Center>
<Image
w={{ base: 250, sm: 400, md: 550 }}
src={data?.image?.link}
alt={data?.name || 'gambar keamanan lingkungan'}
radius="md"
loading="lazy"
fit="cover"
/>
</Center>
{/* Gambar */}
<Center>
<Image
w={{ base: 250, sm: 400, md: 550 }}
src={data?.image?.link}
alt={data?.name || 'gambar keamanan lingkungan'}
radius="md"
loading="lazy"
fit="cover"
/>
</Center>
{/* Deskripsi */}
<Box>
<Text fz="lg" fw="bold" mb={5}>
Deskripsi
</Text>
<Text
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{ __html: data?.deskripsi || '-' }}
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
/>
</Box>
</Stack>
</Paper>
</Box>
{/* Deskripsi */}
<Box>
<Title order={3} mb={5} style={{ lineHeight: 1.2 }}>
Deskripsi
</Title>
<Box pl={20}>
<Text
fz={{ base: 'sm', md: 'md' }}
lh={{ base: 1.5, md: 1.55 }}
c={data?.deskripsi ? 'text' : 'dimmed'}
dangerouslySetInnerHTML={{ __html: data?.deskripsi || '-' }}
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
/>
</Box>
</Box>
</Stack>
</Paper>
</Box>
</Stack>
)
}
export default DetailKeamananLingkunganUser
export default DetailKeamananLingkunganUser

View File

@@ -1,7 +1,7 @@
'use client'
import keamananLingkunganState from '@/app/admin/(dashboard)/_state/keamanan/keamanan-lingkungan';
import colors from '@/con/colors';
import { Box, Button, Center, Grid, GridCol, Image, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
import { Box, Button, Center, Grid, GridCol, Image, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from '@mantine/core';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconSearch } from '@tabler/icons-react';
import { useState } from 'react';
@@ -14,7 +14,7 @@ function Page() {
const state = useProxy(keamananLingkunganState)
const router = useRouter()
const [search, setSearch] = useState('')
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const [debouncedSearch] = useDebouncedValue(search, 1000);
const {
data,
page,
@@ -43,9 +43,9 @@ function Page() {
<Box>
<Grid align='center' px={{ base: 'md', md: 100 }}>
<GridCol span={{ base: 12, md: 9 }}>
<Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
<Title order={1} c={colors["blue-button"]} lh={1.15}>
Keamanan Lingkungan (Pecalang / Patwal)
</Text>
</Title>
</GridCol>
<GridCol span={{ base: 12, md: 3 }}>
<TextInput
@@ -54,18 +54,29 @@ function Page() {
value={search}
onChange={(e) => setSearch(e.target.value)}
leftSection={<IconSearch size={20} />}
w={{ base: "50%", md: "100%" }}
w={"100%"}
/>
</GridCol>
</Grid>
<Text px={{ base: 'md', md: 100 }} pt={20} ta={"justify"} fz="md" mt={4} >
<Text
px={{ base: 'md', md: 100 }}
pt={20}
ta={"justify"}
fz={{ base: 'sm', md: 'md' }}
lh={1.55}
mt={4}
c={'black'}
>
Pecalang dan Patwal (Patroli Pengawal) bertugas memastikan desa tetap aman, tertib, dan kondusif bagi seluruh warga.
</Text>
</Box>
<Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'}>
<SimpleGrid
cols={{ base: 1, sm: 2, md: 3 }} spacing="xl" mt="lg">
cols={{ base: 1, sm: 2, md: 3 }}
spacing="xl"
mt="lg"
>
{data.map((v, k) => (
<Paper
key={k}
@@ -107,17 +118,18 @@ function Page() {
onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')}
/>
</Box>
<Text ta="center" fw={700} fz="lg" c={colors['blue-button']}>
<Title order={2} ta="center" c={colors['blue-button']} lh={1.2}>
{v.name}
</Text>
</Title>
<Text
fz="sm"
fz={{ base: 'xs', md: 'sm' }}
ta="justify"
lineClamp={3}
lh={1.6}
lh={1.55}
style={{
minHeight: '4.8em',
}}
c={'black'}
>
<span
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
@@ -159,4 +171,4 @@ function Page() {
);
}
export default Page;
export default Page;

View File

@@ -1,7 +1,7 @@
'use client'
import kontakDarurat from '@/app/admin/(dashboard)/_state/keamanan/kontak-darurat-keamanan';
import colors from '@/con/colors';
import { Avatar, Box, Center, Flex, Group, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
import { Avatar, Box, Center, Flex, Group, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from '@mantine/core';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconPhoneCall, IconSearch } from '@tabler/icons-react';
import { useState } from 'react';
@@ -42,10 +42,10 @@ function Page() {
</Box>
<Group px={{ base: 'md', md: 100 }} justify={'space-between'} align='center'>
<Box>
<Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
<Title order={1} c={colors["blue-button"]} style={{ lineHeight: 1.15 }}>
Kontak Darurat
</Text>
<Text fz="md" >
</Title>
<Text fz={{ base: 'sm', md: 'md' }} lh={1.5} c={colors['blue-button-2']} style={{ color: colors['blue-button-2'] }}>
Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung.
</Text>
</Box>
@@ -66,17 +66,21 @@ function Page() {
<IconPhoneCall size={30} color={colors["blue-button"]} />
</Avatar>
<Box>
<Text ta={'center'} c={colors['blue-button']} py={10} fz={{ base: "md", md: "h4" }} fw={"bold"} >
<Text ta={'center'} c={colors['blue-button']} py={10} fz={{ base: "sm", md: "md" }} fw={"bold"} lh={1.3} >
Nomor Darurat Utama
</Text>
<Text ta={'center'} fw={"bold"} fz={'h2'} c={colors["blue-button"]}>112</Text>
<Title order={2} ta={'center'} c={colors["blue-button"]} style={{ lineHeight: 1.15 }}>
112
</Title>
</Box>
</Flex>
</Paper>
</Stack>
</Box>
<Center>
<Text fz={"h1"} c={colors["blue-button"]} fw={"bold"}>Tidak ada kontak darurat yang ditemukan</Text>
<Title order={2} c={colors["blue-button"]} style={{ lineHeight: 1.15 }}>
Tidak ada kontak darurat yang ditemukan
</Title>
</Center>
</Stack>
);
@@ -89,10 +93,10 @@ function Page() {
</Box>
<Group px={{ base: 'md', md: 100 }} justify={'space-between'} align='center'>
<Box>
<Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
<Title order={1} c={colors["blue-button"]} style={{ lineHeight: 1.15 }}>
Kontak Darurat
</Text>
<Text fz={{ base: "h4", md: "h3" }} >
</Title>
<Text fz={{ base: 'sm', md: 'md' }} lh={1.5} c={colors['blue-button-2']}>
Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung.
</Text>
</Box>
@@ -113,10 +117,12 @@ function Page() {
<IconPhoneCall size={30} color={colors["blue-button"]} />
</Avatar>
<Box>
<Text ta={'center'} c={colors['blue-button']} py={10} fz={{ base: "md", md: "h4" }} fw={"bold"} >
<Text ta={'center'} c={colors['blue-button']} py={10} fz={{ base: "sm", md: "md" }} fw={"bold"} lh={1.3} >
Nomor Darurat Utama
</Text>
<Text ta={'center'} fw={"bold"} fz={'h2'} c={colors["blue-button"]}>112</Text>
<Title order={2} ta={'center'} c={colors["blue-button"]} style={{ lineHeight: 1.15 }}>
112
</Title>
</Box>
</Flex>
</Paper>
@@ -124,19 +130,13 @@ function Page() {
</Box>
<Box px={{ base: "md", md: 100 }}>
<SimpleGrid cols={{ base: 1, md: 2 }} spacing="xl">
{/* Layanan Darurat */}
{data.map((item) => (
<a
key={item.id}
href={`tel:${item.kontakItems[0]?.kontakItem?.nomorTelepon || '112'}`}
style={{ textDecoration: 'none' }}
>
<Paper
p="lg"
radius="md"
bg={colors['white-trans-1']}
>
<Paper p="lg" radius="md" bg={colors['white-trans-1']}>
<Group pb="md" align="center">
<Avatar radius="xl" size="lg" bg={colors['BG-trans']}>
{item.icon && (
@@ -147,12 +147,11 @@ function Page() {
/>
)}
</Avatar>
<Text fw="bold" fz={{ base: "lg", md: "xl" }} c={colors["blue-button"]}>
<Title order={3} c={colors["blue-button"]} style={{ lineHeight: 1.2 }}>
{item.nama}
</Text>
</Title>
</Group>
{/* Kontak Items */}
{item.kontakItems?.map((kontak) => (
<Paper
key={kontak.id}
@@ -171,11 +170,11 @@ function Page() {
color={colors['blue-button']}
/>
)}
<Text fw="bold" fz={{ base: "sm", md: "md" }} c={colors["blue-button"]}>
<Text fw="bold" fz={{ base: "xs", md: "sm" }} c={colors["blue-button"]} lh={1.45}>
{kontak.kontakItem?.nama}
</Text>
</Group>
<Text fw="bold" fz={{ base: "sm", md: "md" }} c={colors["blue-button"]}>
<Text fw="bold" fz={{ base: "xs", md: "sm" }} c={colors["blue-button"]} lh={1.45}>
{kontak.kontakItem?.nomorTelepon}
</Text>
</Group>
@@ -204,4 +203,4 @@ function Page() {
);
}
export default Page;
export default Page;

View File

@@ -2,7 +2,7 @@
'use client'
import pencegahanKriminalitasState from '@/app/admin/(dashboard)/_state/keamanan/pencegahan-kriminalitas';
import colors from '@/con/colors';
import { Box, Button, Center, Paper, SimpleGrid, Skeleton, Stack, Text } from '@mantine/core';
import { Box, Button, Center, Paper, SimpleGrid, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowRight } from '@tabler/icons-react';
import { useTransitionRouter } from 'next-view-transitions';
@@ -26,7 +26,7 @@ function Page() {
if (!findFirst.data && !findFirst.loading) {
kriminalitasState.findFirst.load();
}
}, [findFirst.data, findFirst.loading]);
}, []);
useShallowEffect(() => {
const LIMIT = 3;
@@ -45,10 +45,10 @@ function Page() {
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap={22}>
<Box px={{ base: 'md', md: 100 }}>
<Text fz={{ base: 'h1', md: '2.5rem' }} c={colors['blue-button']} fw="bold">
<Title order={1} c={colors['blue-button']} fw="bold" lh={1.2}>
Pencegahan Kriminalitas
</Text>
<Text fz='md'>
</Title>
<Text fz={{ base: 'sm', md: 'md' }} lh={1.5}>
Keamanan Komunitas & Pencegahan Kriminal
</Text>
</Box>
@@ -58,11 +58,11 @@ function Page() {
spacing="xl"
>
<Paper p="xl" radius="xl" shadow="lg" >
<Text fz={{ base: 'h3', md: 'h2' }} c={colors['blue-button']} fw="bold">
<Title order={2} c={colors['blue-button']} fw="bold" lh={1.2}>
Program Keamanan Berjalan
</Text>
</Title>
<Stack pt={30} gap="lg">
<Text c="dimmed">
<Text c="dimmed" fz={{ base: 'sm', md: 'md' }} lh={1.5}>
Tidak ada data pencegahan kriminalitas yang cocok
</Text>
</Stack>
@@ -75,10 +75,10 @@ function Page() {
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap={22}>
<Box px={{ base: 'md', md: 100 }}>
<Text fz={{ base: 'h1', md: '2.5rem' }} c={colors['blue-button']} fw="bold">
<Title order={1} c={colors['blue-button']} fw="bold" lh={1.2}>
Pencegahan Kriminalitas
</Text>
<Text fz='md'>
</Title>
<Text fz={{ base: 'sm', md: 'md' }} lh={1.5}>
Keamanan Komunitas & Pencegahan Kriminal
</Text>
</Box>
@@ -88,13 +88,13 @@ function Page() {
spacing="xl"
>
<Paper p="xl" radius="xl" shadow="lg" >
<Text fz={{ base: 'h3', md: 'h2' }} c={colors['blue-button']} fw="bold">
<Title order={2} c={colors['blue-button']} fw="bold" lh={1.2}>
Program Keamanan Berjalan
</Text>
</Title>
<Stack pt={30} gap="lg">
<Box
style={{
minHeight: 300, // sesuaikan: tinggi area yg muat 3 item
minHeight: 300,
}}
>
{data.length > 0 ? (
@@ -120,14 +120,16 @@ function Page() {
}
>
<Stack gap="xs">
<Text fz="h3" c={colors['white-1']}>
<Title order={3} c={colors['white-1']} lh={1.2}>
{item.judul}
</Text>
</Title>
</Stack>
</Paper>
))
) : (
<Text c="dimmed">Tidak ada data pencegahan kriminalitas yang cocok</Text>
<Text c="dimmed" fz={{ base: 'sm', md: 'md' }} lh={1.5}>
Tidak ada data pencegahan kriminalitas yang cocok
</Text>
)}
</Box>
<Button
@@ -169,12 +171,18 @@ function Page() {
style={{ borderRadius: 8 }}
/>
) : (
<Text fz="sm" c="dimmed">Tidak ada video</Text>
<Text fz={{ base: 'xs', md: 'sm' }} c="dimmed" lh={1.4}>
Tidak ada video
</Text>
)}
<Text py={10} fz={{ base: 'h3', md: 'h2' }} fw="bold" c={colors['blue-button']}>
<Title order={2} py={10} fw="bold" c={colors['blue-button']} lh={1.2}>
{findFirst.data?.judul}
</Text>
<Text fz="h4" dangerouslySetInnerHTML={{ __html: findFirst.data?.deskripsiSingkat }} />
</Title>
<Text
fz={{ base: 'sm', md: 'md' }}
lh={1.5}
dangerouslySetInnerHTML={{ __html: findFirst.data?.deskripsiSingkat }}
/>
</Paper>
) : null}
</Box>
@@ -195,4 +203,4 @@ function Page() {
}
}
export default Page;
export default Page;

View File

@@ -2,7 +2,7 @@
/* eslint-disable react-hooks/exhaustive-deps */
import polsekTerdekatState from '@/app/admin/(dashboard)/_state/keamanan/polsek-terdekat';
import colors from '@/con/colors';
import { Badge, Box, Button, Center, Flex, Paper, SimpleGrid, Skeleton, Stack, Text } from '@mantine/core';
import { Badge, Box, Button, Center, Flex, Paper, SimpleGrid, Skeleton, Stack, Text, Title } from '@mantine/core';
import { IconArrowDown, IconClock, IconNavigation, IconPhone, IconPin } from '@tabler/icons-react';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
@@ -35,15 +35,15 @@ function Page() {
<BackButton />
</Box>
<Box pb={10} px={{ base: 20, md: 100 }}>
<Text fz={{ base: 'h1', md: '2.5rem' }} c={colors['blue-button']} fw="bold">
<Title order={1} c={colors['blue-button']}>
Kantor Polisi Terdekat
</Text>
<Text pb={15} fz="md">
</Title>
<Text pb={15} fz={{ base: 'sm', md: 'md' }} lh={1.5}>
Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung
</Text>
</Box>
<Center py="xl">
<Text fz="lg" fw="bold" c="red">
<Text fz={{ base: 'md', md: 'lg' }} fw="bold" c="red" lh={1.4}>
Data Polsek tidak ada
</Text>
</Center>
@@ -58,10 +58,10 @@ function Page() {
</Box>
<Box pb={10} px={{ base: 20, md: 100 }}>
<Text fz={{ base: 'h1', md: '2.5rem' }} c={colors['blue-button']} fw="bold">
<Title order={1} c={colors['blue-button']}>
Kantor Polisi Terdekat
</Text>
<Text pb={15} fz="h4">
</Title>
<Text pb={15} fz={{ base: 'sm', md: 'md' }} lh={1.5}>
Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung
</Text>
</Box>
@@ -79,10 +79,10 @@ function Page() {
<>
{/* === KIRI === */}
<Box>
<Text c={colors['blue-button']} fw="bold" fz="h2">
<Title order={2} c={colors['blue-button']} lh={1.2}>
{data.nama}
</Text>
<Text c={colors['blue-button']} fz="sm">
</Title>
<Text c={colors['blue-button']} fz={{ base: 'xs', md: 'sm' }} lh={1.4}>
{data.jarakKeDesa}
</Text>
@@ -98,11 +98,11 @@ function Page() {
<IconPin size={22} />
</Box>
<Text
fz="lg"
fz={{ base: 'sm', md: 'md' }}
style={{
flex: 1,
wordBreak: 'break-word',
lineHeight: 1.4,
lineHeight: 1.5,
}}
>
{data.alamat}
@@ -119,7 +119,7 @@ function Page() {
<Box w={25} mt={3}>
<IconPhone size={22} />
</Box>
<Text fz="lg">
<Text fz={{ base: 'sm', md: 'md' }} lh={1.5}>
{data.nomorTelepon}
</Text>
</Flex>
@@ -133,26 +133,26 @@ function Page() {
style={{ wordBreak: 'break-word' }}
>
<Box w={25} mt={3}>
<IconClock size={22} />
<IconClock size={22} />
</Box>
<Text fz="lg">
<Text fz={{ base: 'sm', md: 'md' }} lh={1.5}>
{data.jamOperasional}
</Text>
</Flex>
{/* Layanan */}
<Box>
<Text c={colors['blue-button']} fw="bold" fz="h2">
Layanan Yang Tersedia :
</Text>
<Box pt={15}>
<Title order={2} c={colors['blue-button']} lh={1.2}>
Layanan Yang Tersedia:
</Title>
<SimpleGrid py={10} cols={{ base: 1, md: 2 }}>
<Text fz="lg">
<Text fz={{ base: 'sm', md: 'md' }} lh={1.5}>
{data.layananPolsek.nama}
</Text>
</SimpleGrid>
</Box>
<Box>
<Box pt={15}>
<Button
bg={colors['blue-button']}
onClick={() =>
@@ -205,4 +205,4 @@ function Page() {
);
}
export default Page;
export default Page;

View File

@@ -1,7 +1,7 @@
'use client'
import polsekTerdekatState from '@/app/admin/(dashboard)/_state/keamanan/polsek-terdekat';
import colors from '@/con/colors';
import { Box, Button, Center, Grid, GridCol, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
import { Box, Button, Center, Grid, GridCol, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconNavigation, IconSearch } from '@tabler/icons-react';
import React, { useState } from 'react';
@@ -13,8 +13,8 @@ import { useDebouncedValue } from '@mantine/hooks';
function Page() {
const state = useProxy(polsekTerdekatState);
const [search, setSearch] = useState('');
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const router = useRouter()
const [debouncedSearch] = useDebouncedValue(search, 1000);
const router = useRouter();
const {
data,
@@ -25,71 +25,98 @@ function Page() {
} = state.findMany;
useShallowEffect(() => {
load(page, 3, debouncedSearch)
}, [page, debouncedSearch])
load(page, 3, debouncedSearch);
}, [page, debouncedSearch]);
if (loading || !data) {
return (
<Box py={10}>
<Skeleton h={500} />
</Box>
)
);
}
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Grid align='center' px={{ base: 'md', md: 100 }}>
<Grid align="center" px={{ base: 'md', md: 100 }}>
<GridCol span={{ base: 12, md: 9 }}>
<Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
<Title
order={1}
c={colors['blue-button']}
lh={1.2}
>
Semua Polsek Terdekat
</Text>
</Title>
</GridCol>
<GridCol span={{ base: 12, md: 3 }}>
<TextInput
radius={"lg"}
placeholder='Cari Polsek'
radius="lg"
placeholder="Cari Polsek"
value={search}
onChange={(e) => setSearch(e.target.value)}
leftSection={<IconSearch size={20} />}
w={{ base: "50%", md: "100%" }}
w={{ base: '50%', md: '100%' }}
/>
</GridCol>
</Grid>
<Box px={{ base: "md", md: 100 }}>
<SimpleGrid
cols={{
base: 1,
md: 3,
}}
>
{data.map((v, k) => {
return (
<Paper p={"xl"} bg={colors['white-trans-1']} key={k}>
<Stack gap={"xs"}>
<Text fw={"bold"} fz={"h3"}>{v.nama}</Text>
<Text>Alamat: {v.alamat}</Text>
<Text>Jarak: {v.jarakKeDesa}</Text>
<Text>Telepon: {v.nomorTelepon}</Text>
<Text>Jam Operasional: {v.jamOperasional}</Text>
<Box pt={20}>
<iframe style={{ border: 2, width: "100%" }} src={v.embedMapUrl} width="550" height="300" ></iframe>
</Box>
<Box pt={20}>
<Button onClick={() => router.push(v.linkPetunjukArah)} fullWidth bg={colors["blue-button"]} radius={10} leftSection={<IconNavigation size={20} />}>Petunjuk Arah</Button>
</Box>
</Stack>
</Paper>
)
})}
<Box px={{ base: 'md', md: 100 }}>
<SimpleGrid cols={{ base: 1, md: 3 }}>
{data.map((v, k) => (
<Paper p="xl" bg={colors['white-trans-1']} key={k}>
<Stack gap="xs">
<Title
order={3}
fw="bold"
lh={1.2}
>
{v.nama}
</Title>
<Text fz={{ base: 'sm', md: 'md' }} lh={1.5}>
Alamat: {v.alamat}
</Text>
<Text fz={{ base: 'sm', md: 'md' }} lh={1.5}>
Jarak: {v.jarakKeDesa}
</Text>
<Text fz={{ base: 'sm', md: 'md' }} lh={1.5}>
Telepon: {v.nomorTelepon}
</Text>
<Text fz={{ base: 'sm', md: 'md' }} lh={1.5}>
Jam Operasional: {v.jamOperasional}
</Text>
<Box pt={20}>
<iframe
style={{ border: 2, width: '100%' }}
src={v.embedMapUrl}
width="550"
height="300"
></iframe>
</Box>
<Box pt={20}>
<Button
onClick={() => router.push(v.linkPetunjukArah)}
fullWidth
bg={colors['blue-button']}
radius={10}
leftSection={<IconNavigation size={20} />}
>
Petunjuk Arah
</Button>
</Box>
</Stack>
</Paper>
))}
</SimpleGrid>
</Box>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)} // ini penting!
onChange={(newPage) => load(newPage)}
total={totalPages}
mt="md"
mb="md"
@@ -99,4 +126,4 @@ function Page() {
);
}
export default Page;
export default Page;

View File

@@ -1,7 +1,7 @@
// Create a new component: components/EdukasiCard.tsx
// components/EdukasiCard.tsx
'use client';
import { Box, Paper, Stack, Text } from '@mantine/core';
import { Box, Paper, Stack, Text, Title } from '@mantine/core';
import { ReactNode } from 'react';
interface EdukasiCardProps {
@@ -18,7 +18,7 @@ export function EdukasiCard({ icon, title, description, color = '#1e88e5' }: Edu
radius="md"
shadow="sm"
withBorder
style={{
style={{
height: '100%',
transition: 'transform 0.2s, box-shadow 0.2s',
'&:hover': {
@@ -31,32 +31,35 @@ export function EdukasiCard({ icon, title, description, color = '#1e88e5' }: Edu
<Box>
<Stack align="center" gap="xs" mb="md">
<Box style={{ color }}>{icon}</Box>
<Text
fz={{ base: 'h5', md: 'h4' }}
fw={700}
c={color}
ta="center"
lineClamp={2}
style={{
wordBreak: 'break-word',
minHeight: '3.5rem',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
dangerouslySetInnerHTML={{ __html: title }}
/>
<Title
order={3}
fz={{ base: 'sm', md: 'md' }}
c={color}
ta="center"
lineClamp={2}
style={{
wordBreak: 'break-word',
minHeight: '3.5rem',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
lineHeight: 1.2
}}
dangerouslySetInnerHTML={{ __html: title }}
/>
</Stack>
<Text
size="sm"
pl={20}
style={{
wordBreak: 'break-word',
lineHeight: 1.6,
color: 'var(--mantine-color-gray-7)'
}}
dangerouslySetInnerHTML={{ __html: description }}
/>
<Box pl={20}>
<Text
fz={{ base: 'sm', md: 'md' }}
lh={1.5}
c="gray.7"
ta="justify"
style={{
wordBreak: 'break-word'
}}
dangerouslySetInnerHTML={{ __html: description }}
/>
</Box>
</Box>
</Stack>
</Paper>

View File

@@ -1,6 +1,6 @@
'use client';
import { Box, Container, SimpleGrid, Skeleton, Stack, Text } from '@mantine/core';
import { Box, Container, SimpleGrid, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconLeaf, IconPlant2, IconRecycle } from '@tabler/icons-react';
import { useProxy } from 'valtio/utils';
@@ -51,19 +51,21 @@ export default function EdukasiLingkunganPage() {
</Box>
<Container size="lg" ta="center">
<Text
component="h1"
fz={{ base: 'h2', md: '2.5rem' }}
<Title
order={1}
c={colors['blue-button']}
fw={700}
mb="md"
lh={1.15}
>
Edukasi Lingkungan
</Text>
</Title>
<Text
fz={{ base: 'md', md: 'lg' }}
fz={{ base: 'sm', md: 'md' }}
lh={1.5}
maw={800}
mx="auto"
c="dark.9"
>
Program edukasi ini membimbing masyarakat untuk peduli dan bertanggung jawab terhadap alam,
meningkatkan kesehatan, kenyamanan, dan keberlanjutan hidup bersama.

View File

@@ -41,23 +41,23 @@ function DetailKegiatanDesaUser() {
shadow="sm"
maw={900}
mx="auto"
>
>
<Stack gap="md">
{data.image?.link && (
<Image
src={data.image.link}
alt={data.judul || 'Kegiatan Desa'}
radius="md"
fit="cover"
h={300}
mb="xl"
style={{ objectPosition: 'center', width: '100%' }}
/>
)}
{/* Judul */}
<Title order={2} c={colors['blue-button']}>
<Title order={1} ta={"center"} c={colors['blue-button']}>
{data.judul || 'Kegiatan Desa'}
</Title>
{data.image?.link && (
<Image
src={data.image.link}
alt={data.judul || 'Kegiatan Desa'}
radius="md"
fit="cover"
h={300}
mb="xl"
style={{ objectPosition: 'center', width: '100%' }}
/>
)}
{/* Meta Info */}
<Group gap="xl" c="dimmed">

View File

@@ -1,6 +1,10 @@
// app/desa/berita/BeritaLayoutClient.tsx
'use client'
import colors from '@/con/colors';
import { Box } from '@mantine/core';
import dynamic from 'next/dynamic';
import { usePathname } from 'next/navigation';
import BackButton from '../../desa/layanan/_com/BackButto';
const LayoutTabsGotongRoyong = dynamic(
() => import('./_lib/layoutTabs'),
@@ -8,5 +12,21 @@ const LayoutTabsGotongRoyong = dynamic(
);
export default function GotongRoyongLayoutClient({ children }: { children: React.ReactNode }) {
const pathname = usePathname()
const segments = pathname.split('/').filter(Boolean)
const isDetailPage = segments.length === 5;
if (isDetailPage) {
// Tampilkan tanpa tab menu
return (
<Box bg={colors.Bg}>
<Box pt={33} px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
{children}
</Box>
);
}
return <LayoutTabsGotongRoyong>{children}</LayoutTabsGotongRoyong>;
}

View File

@@ -11,7 +11,6 @@ import {
Center,
Container,
Divider,
Flex,
Grid,
GridCol,
Group,
@@ -23,7 +22,7 @@ import {
Stack,
Text,
Title,
Transition,
Transition
} from '@mantine/core';
import { IconArrowRight, IconCalendar } from '@tabler/icons-react';
import { motion } from 'framer-motion';
@@ -46,7 +45,7 @@ export default function Page() {
// Load featured data once on component mount
useEffect(() => {
let mounted = true;
const loadFeatured = async () => {
try {
if (!featured.data && !loadingFeatured) {
@@ -68,7 +67,7 @@ export default function Page() {
useEffect(() => {
let mounted = true;
const loadData = async () => {
try {
const limit = 3;
@@ -92,7 +91,7 @@ export default function Page() {
if (search) url.set('search', search);
if (newPage > 1) url.set('page', newPage.toString());
else url.delete('page');
// Use push instead of replace to keep browser history
router.push(`?${url.toString()}`, { scroll: false });
};
@@ -139,7 +138,6 @@ export default function Page() {
height={400}
fit="cover"
radius="md"
style={{ borderBottomRightRadius: 0, borderTopRightRadius: 0 }}
loading="lazy"
/>
</GridCol>
@@ -222,6 +220,7 @@ export default function Page() {
alt={item.judul}
fit="cover"
loading="lazy"
radius={"md"}
/>
</Card.Section>
@@ -241,14 +240,17 @@ export default function Page() {
dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat }}
/>
<Flex align="center" justify="apart" mt="md" gap="xs">
<Text size="xs" c="dimmed">
{new Date(item.createdAt).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'short',
year: 'numeric',
})}
</Text>
<Group justify="apart" mt="auto">
<Group gap="xs">
<IconCalendar size={18} />
<Text size="xs" c="dimmed">
{new Date(item.createdAt).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'short',
year: 'numeric',
})}
</Text>
</Group>
<Button
p="xs"
@@ -262,7 +264,7 @@ export default function Page() {
>
Baca Selengkapnya
</Button>
</Flex>
</Group>
</Card>
))}
</SimpleGrid>

View File

@@ -1,11 +1,12 @@
'use client'
import stateKonservasiAdatBali from '@/app/admin/(dashboard)/_state/lingkungan/konservasi-adat-bali';
import colors from '@/con/colors';
import { Box, Center, Paper, SimpleGrid, Skeleton, Stack, Text } from '@mantine/core';
import { Box, Center, Paper, SimpleGrid, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto';
function Page() {
const filosofi = useProxy(stateKonservasiAdatBali.stateFilosofiTriHita.findById)
const nilai = useProxy(stateKonservasiAdatBali.stateNilaiKonservasiAdat.findById)
@@ -30,11 +31,24 @@ function Page() {
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }} pb={30}>
<Text ta="center" fz={{ base: '2xl', md: '3rem' }} c={colors['blue-button']} fw="bold">
<Box px={{ base: 'md', md: 100 }} >
<Title
order={1}
ta="center"
c={colors['blue-button']}
fw="bold"
style={{ lineHeight: 1.15 }}
>
Konservasi Adat Bali
</Text>
<Text px={20} ta="center" fz="lg" c="black">
</Title>
<Text
px={20}
ta="center"
fz={{ base: 'sm', md: 'lg' }}
c="black"
style={{ lineHeight: 1.55 }}
>
Pelestarian lingkungan di Bali yang berpijak pada kearifan lokal, menjaga harmoni antara alam, budaya, dan manusia.
</Text>
</Box>
@@ -54,53 +68,31 @@ function Page() {
>
<Stack gap="md" px={20} style={{ height: '100%' }}>
<Center>
<Text fz="xl" fw="bold" c="black">
<Title
order={3}
c="black"
fw="bold"
style={{ lineHeight: 1.15 }}
>
{filosofi.data?.judul}
</Text>
</Center>
<div
style={{
wordBreak: "break-word",
whiteSpace: "normal",
flexGrow: 1
}}
dangerouslySetInnerHTML={{ __html: filosofi.data?.deskripsi || '' }}
/>
</Stack>
</Paper>
</Box>
{/* Nilai */}
<Box style={{ display: 'flex', height: '100%' }}>
<Paper
p="lg"
style={{
borderRadius: 16,
width: '100%',
display: 'flex',
flexDirection: 'column',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)'
}}
>
<Stack gap="md" px={20} style={{ height: '100%' }}>
<Center>
<Text fz="xl" fw="bold" c="black">
{nilai.data?.judul}
</Text>
</Title>
</Center>
<div
style={{
wordBreak: "break-word",
whiteSpace: "normal",
flexGrow: 1,
minHeight: 0
fontSize: '14px',
lineHeight: 1.55,
color: 'black'
}}
dangerouslySetInnerHTML={{ __html: nilai.data?.deskripsi || '' }}
dangerouslySetInnerHTML={{ __html: filosofi.data?.deskripsi || '' }}
/>
</Stack>
</Paper>
</Box>
{/* Bentuk */}
<Box>
</Box>
{/* Nilai */}
<Box style={{ display: 'flex', height: '100%' }}>
<Paper
p="lg"
style={{
@@ -113,26 +105,72 @@ function Page() {
>
<Stack gap="md" px={20} style={{ height: '100%' }}>
<Center>
<Text fz="xl" fw="bold" c="black">
{bentuk.data?.judul}
</Text>
<Title
order={3}
c="black"
fw="bold"
style={{ lineHeight: 1.15 }}
>
{nilai.data?.judul}
</Title>
</Center>
<div
style={{
wordBreak: "break-word",
whiteSpace: "normal",
flexGrow: 1,
minHeight: 0
minHeight: 0,
fontSize: '14px',
lineHeight: 1.55,
color: 'black'
}}
dangerouslySetInnerHTML={{ __html: nilai.data?.deskripsi || '' }}
/>
</Stack>
</Paper>
</Box>
{/* Bentuk */}
<Box>
<Paper
p="lg"
style={{
borderRadius: 16,
width: '100%',
display: 'flex',
flexDirection: 'column',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)'
}}
>
<Stack gap="md" px={20} style={{ height: '100%' }}>
<Center>
<Title
order={3}
c="black"
fw="bold"
style={{ lineHeight: 1.15 }}
>
{bentuk.data?.judul}
</Title>
</Center>
<div
style={{
wordBreak: "break-word",
whiteSpace: "normal",
flexGrow: 1,
minHeight: 0,
fontSize: '14px',
lineHeight: 1.55,
color: 'black'
}}
dangerouslySetInnerHTML={{ __html: bentuk.data?.deskripsi || '' }}
/>
</Stack>
</Paper>
</Box>
</Box>
</SimpleGrid>
</Box>
</Stack>
);
}
export default Page;
export default Page;

View File

@@ -5,6 +5,7 @@ import {
Button,
Container,
Divider,
Flex,
Group,
Modal,
Paper,
@@ -23,11 +24,11 @@ import { useProxy } from 'valtio/utils';
import beasiswaDesaState from '@/app/admin/(dashboard)/_state/pendidikan/beasiswa-desa';
import colors from '@/con/colors';
export default function BeasiswaPage() {
const router = useRouter();
const beasiswaDesa = useProxy(beasiswaDesaState.beasiswaPendaftar)
const beasiswaDesa = useProxy(beasiswaDesaState.beasiswaPendaftar);
const [opened, { open, close }] = useDisclosure(false);
const resetForm = () => {
beasiswaDesa.create.form = {
namaLengkap: "",
@@ -61,6 +62,7 @@ export default function BeasiswaPage() {
leftSection={<IconArrowLeft size={18} />}
onClick={() => router.back()}
mb="lg"
style={{ fontSize: '1rem', fontWeight: 500 }}
>
Kembali
</Button>
@@ -69,11 +71,18 @@ export default function BeasiswaPage() {
{/* Hero Section */}
<Container size="lg" py="xl">
<Stack gap="md" maw={600}>
<Group>
<Flex gap={"md"} justify={"flex-start"} align={"center"}>
<IconSchool size={30} color={colors["blue-button"]} />
<Title order={2} c={colors["blue-button"]}>Program Beasiswa Pendidikan Desa Darmasaba</Title>
</Group>
<Text>
<Title
order={1}
fz={{ base: '1.125rem', md: '1.375rem' }}
lh={1.15}
c={colors["blue-button"]}
>
Program Beasiswa Pendidikan Desa Darmasaba
</Title>
</Flex>
<Text fz={{ base: '0.875rem', md: '1rem' }} lh={1.55}>
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>
@@ -84,20 +93,22 @@ export default function BeasiswaPage() {
<Container size="lg" py="xl">
<Group mb="sm">
<IconInfoCircle size={24} color={colors["blue-button"]} />
<Title order={3} c={colors["blue-button"]}>Tentang Program</Title>
<Title order={3} fz={{ base: '1.125rem', md: '1.375rem' }} lh={1.15} c={colors["blue-button"]}>
Tentang Program
</Title>
</Group>
<Text>
<Text fz={{ base: '0.875rem', md: '1rem' }} lh={1.55}>
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 */}
{/* Periode Beasiswa */}
<Paper mt="md" p="md" radius="lg" shadow="xs" bg="#f8fbff" withBorder>
<Text fw={500}>
<Text fz={{ base: '1rem', md: '1.125rem' }} fw={600} lh={1.4}>
Periode Beasiswa Tahun 2025
</Text>
<Text fz="sm" c="dimmed">
<Text fz={{ base: '0.8125rem', md: '0.875rem' }} c="dimmed" lh={1.5}>
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>
@@ -108,27 +119,35 @@ export default function BeasiswaPage() {
<Container size="lg" py="xl">
<Group mb="sm">
<IconChecklist size={24} color={colors["blue-button"]} />
<Title order={3} c={colors["blue-button"]}>Syarat Pendaftaran</Title>
<Title order={3} fz={{ base: '1.125rem', md: '1.375rem' }} lh={1.15} c={colors["blue-button"]}>
Syarat Pendaftaran
</Title>
</Group>
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="lg">
<Paper shadow="sm" p="md" radius="lg" withBorder>
<Text fw={500}>Domisili Desa Darmasaba</Text>
<Text c="dimmed" fz="sm">
<Text fz={{ base: '1rem', md: '1.125rem' }} fw={600} lh={1.4}>
Domisili Desa Darmasaba
</Text>
<Text c="dimmed" fz={{ base: '0.8125rem', md: '0.875rem' }} lh={1.5}>
Peserta harus merupakan warga desa yang berdomisili minimal 2 tahun.
</Text>
</Paper>
<Paper shadow="sm" p="md" radius="lg" withBorder>
<Text fw={500}>Nilai Akademik</Text>
<Text c="dimmed" fz="sm">
<Text fz={{ base: '1rem', md: '1.125rem' }} fw={600} lh={1.4}>
Nilai Akademik
</Text>
<Text c="dimmed" fz={{ base: '0.8125rem', md: '0.875rem' }} lh={1.5}>
Rata-rata nilai raport minimal 80 atau setara.
</Text>
</Paper>
<Paper shadow="sm" p="md" radius="lg" withBorder>
<Text fw={500}>Surat Rekomendasi</Text>
<Text c="dimmed" fz="sm">
<Text fz={{ base: '1rem', md: '1.125rem' }} fw={600} lh={1.4}>
Surat Rekomendasi
</Text>
<Text c="dimmed" fz={{ base: '0.8125rem', md: '0.875rem' }} lh={1.5}>
Diperlukan surat rekomendasi dari sekolah atau guru wali kelas.
</Text>
</Paper>
@@ -139,75 +158,102 @@ export default function BeasiswaPage() {
<Container size="lg" py="xl">
<Group mb="sm">
<IconTimeline size={24} color={colors["blue-button"]} />
<Title order={3} c={colors["blue-button"]}>Proses Seleksi</Title>
<Title order={3} fz={{ base: '1.125rem', md: '1.375rem' }} lh={1.15} c={colors["blue-button"]}>
Proses Seleksi
</Title>
</Group>
<Timeline active={4} bulletSize={24} lineWidth={2}>
<Timeline.Item title="Pendaftaran Online">
<Text c="dimmed" size="sm">
<Timeline.Item
title={
<Text fz={{ base: '1rem', md: '1.125rem' }} fw={600} lh={1.4}>
Pendaftaran Online
</Text>
}
>
<Text c="dimmed" fz={{ base: '0.8125rem', md: '0.875rem' }} lh={1.5}>
Calon peserta mengisi formulir pendaftaran dan mengunggah dokumen pendukung.
</Text>
<Text size="sm" fw={500} mt={4}>
<Text fz={{ base: '0.8125rem', md: '0.875rem' }} fw={600} mt={4} lh={1.4}>
Estimasi waktu: 1 Februari 31 Mei 2025
</Text>
</Timeline.Item>
<Timeline.Item title="Seleksi Administrasi">
<Text c="dimmed" size="sm">
<Timeline.Item
title={
<Text fz={{ base: '1rem', md: '1.125rem' }} fw={600} lh={1.4}>
Seleksi Administrasi
</Text>
}
>
<Text c="dimmed" fz={{ base: '0.8125rem', md: '0.875rem' }} lh={1.5}>
Panitia memverifikasi kelengkapan dan validitas berkas.
</Text>
<Text size="sm" fw={500} mt={4}>
<Text fz={{ base: '0.8125rem', md: '0.875rem' }} fw={600} mt={4} lh={1.4}>
Estimasi waktu: 57 hari kerja setelah penutupan pendaftaran
</Text>
</Timeline.Item>
<Timeline.Item title="Wawancara dan Penilaian">
<Text c="dimmed" size="sm">
<Timeline.Item
title={
<Text fz={{ base: '1rem', md: '1.125rem' }} fw={600} lh={1.4}>
Wawancara dan Penilaian
</Text>
}
>
<Text c="dimmed" fz={{ base: '0.8125rem', md: '0.875rem' }} lh={1.5}>
Peserta yang lolos administrasi akan diundang untuk wawancara langsung dengan tim seleksi.
</Text>
<Text size="sm" fw={500} mt={4}>
<Text fz={{ base: '0.8125rem', md: '0.875rem' }} fw={600} mt={4} lh={1.4}>
Estimasi waktu: 710 hari kerja setelah pengumuman seleksi administrasi
</Text>
</Timeline.Item>
<Timeline.Item title="Pengumuman Penerima">
<Text c="dimmed" size="sm">
<Timeline.Item
title={
<Text fz={{ base: '1rem', md: '1.125rem' }} fw={600} lh={1.4}>
Pengumuman Penerima
</Text>
}
>
<Text c="dimmed" fz={{ base: '0.8125rem', md: '0.875rem' }} lh={1.5}>
Daftar penerima beasiswa diumumkan melalui website resmi Desa Darmasaba.
</Text>
<Text size="sm" fw={500} mt={4}>
<Text fz={{ base: '0.8125rem', md: '0.875rem' }} fw={600} mt={4} lh={1.4}>
Estimasi waktu: 5 hari kerja setelah tahap wawancara selesai
</Text>
</Timeline.Item>
</Timeline>
<Text c="dimmed" size="sm" mt="lg" ta="center">
<Text c="dimmed" fz={{ base: '0.8125rem', md: '0.875rem' }} lh={1.5} mt="lg" ta="center">
Total estimasi keseluruhan proses: sekitar 34 minggu setelah penutupan pendaftaran
</Text>
</Container>
{/* Testimoni */}
<Container size="lg" py="xl">
<Group mb="sm">
<IconQuote size={24} color={colors["blue-button"]} />
<Title order={3} c={colors["blue-button"]}>Cerita Sukses Penerima Beasiswa</Title>
<Title order={3} fz={{ base: '1.125rem', md: '1.375rem' }} lh={1.15} c={colors["blue-button"]}>
Cerita Sukses Penerima Beasiswa
</Title>
</Group>
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="lg">
<Paper shadow="md" p="lg" radius="lg">
<Text fs={'italic'}>
<Text fs="italic" fz={{ base: '0.9375rem', md: '1rem' }} lh={1.5}>
Program ini sangat membantu saya melanjutkan kuliah di Universitas Udayana. Terima kasih Desa Darmasaba!
</Text>
<Text mt="sm" fw={600}>
<Text mt="sm" fw={600} fz={{ base: '0.9375rem', md: '1rem' }} lh={1.4}>
Ni Kadek Ayu S., Penerima Beasiswa 2024
</Text>
</Paper>
<Paper shadow="md" p="lg" radius="lg">
<Text fs={'italic'}>
<Text fs="italic" fz={{ base: '0.9375rem', md: '1rem' }} lh={1.5}>
Selain bantuan dana, kami juga mendapatkan pelatihan komputer dan bahasa Inggris.
</Text>
<Text mt="sm" fw={600}>
<Text mt="sm" fw={600} fz={{ base: '0.9375rem', md: '1rem' }} lh={1.4}>
I Made Gede A., Penerima Beasiswa 2023
</Text>
</Paper>
@@ -218,16 +264,25 @@ export default function BeasiswaPage() {
<Container size="lg" py="xl" ta="center">
<Group justify="center" mb="sm">
<IconUserPlus size={28} color={colors["blue-button"]} />
<Title order={3} c={colors["blue-button"]}>Siap Bergabung dengan Program Ini?</Title>
<Title order={3} fz={{ base: '1.375rem', md: '1.5rem' }} lh={1.15} c={colors["blue-button"]}>
Siap Bergabung dengan Program Ini?
</Title>
</Group>
<Text c="dimmed" mb="md">
<Text c="dimmed" fz={{ base: '0.875rem', md: '1rem' }} lh={1.5} mb="md">
Segera daftar dan wujudkan mimpimu bersama Desa Darmasaba.
</Text>
<Button onClick={open} size="lg" radius="xl" bg={colors["blue-button"]}>
<Button
onClick={open}
size="lg"
radius="xl"
bg={colors["blue-button"]}
style={{ fontSize: '1.125rem', fontWeight: 600, lineHeight: 1.4 }}
>
Daftar Sekarang
</Button>
</Container>
{/* Modal Formulir */}
<Modal
opened={opened}
onClose={close}
@@ -235,7 +290,7 @@ export default function BeasiswaPage() {
size="lg"
transitionProps={{ transition: 'fade', duration: 200 }}
title={
<Text fz="xl" fw={800} c={colors['blue-button']}>
<Text fz={{ base: '1.375rem', md: '1.5rem' }} fw={800} c={colors['blue-button']} lh={1.2}>
Formulir Beasiswa
</Text>
}
@@ -245,64 +300,105 @@ export default function BeasiswaPage() {
<TextInput
label="Nama Lengkap"
placeholder="Masukkan nama lengkap"
onChange={(val) => { beasiswaDesa.create.form.namaLengkap = val.target.value }} />
labelProps={{ style: { fontSize: '0.9375rem', fontWeight: 600, lineHeight: 1.4 } }}
onChange={(val) => { beasiswaDesa.create.form.namaLengkap = val.target.value }}
/>
<TextInput
type="number"
label="NIS"
placeholder="Masukkan NIS"
onChange={(val) => { beasiswaDesa.create.form.nis = val.target.value }} />
labelProps={{ style: { fontSize: '0.9375rem', fontWeight: 600, lineHeight: 1.4 } }}
onChange={(val) => { beasiswaDesa.create.form.nis = val.target.value }}
/>
<TextInput
label="Kelas"
placeholder="Masukkan kelas"
onChange={(val) => { beasiswaDesa.create.form.kelas = val.target.value }} />
labelProps={{ style: { fontSize: '0.9375rem', fontWeight: 600, lineHeight: 1.4 } }}
onChange={(val) => { beasiswaDesa.create.form.kelas = val.target.value }}
/>
<Select
label="Jenis Kelamin"
placeholder="Pilih jenis kelamin"
data={[{ value: "LAKI_LAKI", label: "Laki-laki" }, { value: "PEREMPUAN", label: "Perempuan" }]}
onChange={(val) => { if (val) beasiswaDesa.create.form.jenisKelamin = val }} />
data={[
{ value: "LAKI_LAKI", label: "Laki-laki" },
{ value: "PEREMPUAN", label: "Perempuan" }
]}
labelProps={{ style: { fontSize: '0.9375rem', fontWeight: 600, lineHeight: 1.4 } }}
onChange={(val) => { if (val) beasiswaDesa.create.form.jenisKelamin = val }}
/>
<TextInput
label="Alamat Domisili"
placeholder="Masukkan alamat domisili"
onChange={(val) => { beasiswaDesa.create.form.alamatDomisili = val.target.value }} />
labelProps={{ style: { fontSize: '0.9375rem', fontWeight: 600, lineHeight: 1.4 } }}
onChange={(val) => { beasiswaDesa.create.form.alamatDomisili = val.target.value }}
/>
<TextInput
label="Tempat Lahir"
placeholder="Masukkan tempat lahir"
onChange={(val) => { beasiswaDesa.create.form.tempatLahir = val.target.value }} />
labelProps={{ style: { fontSize: '0.9375rem', fontWeight: 600, lineHeight: 1.4 } }}
onChange={(val) => { beasiswaDesa.create.form.tempatLahir = val.target.value }}
/>
<TextInput
type="date"
label="Tanggal Lahir"
placeholder="Pilih tanggal lahir"
onChange={(val) => { beasiswaDesa.create.form.tanggalLahir = val.target.value }} />
labelProps={{ style: { fontSize: '0.9375rem', fontWeight: 600, lineHeight: 1.4 } }}
onChange={(val) => { beasiswaDesa.create.form.tanggalLahir = val.target.value }}
/>
<Box pt={15} pb={10}>
<Divider />
</Box>
<TextInput
label="Nama Orang Tua / Wali"
placeholder="Masukkan nama orang tua / wali"
onChange={(val) => { beasiswaDesa.create.form.namaOrtu = val.target.value }} />
labelProps={{ style: { fontSize: '0.9375rem', fontWeight: 600, lineHeight: 1.4 } }}
onChange={(val) => { beasiswaDesa.create.form.namaOrtu = val.target.value }}
/>
<TextInput
label="NIK Orang Tua / Wali"
placeholder="Masukkan NIK orang tua / wali"
onChange={(val) => { beasiswaDesa.create.form.nik = val.target.value }} />
labelProps={{ style: { fontSize: '0.9375rem', fontWeight: 600, lineHeight: 1.4 } }}
onChange={(val) => { beasiswaDesa.create.form.nik = val.target.value }}
/>
<TextInput
label="Pekerjaan Orang Tua / Wali"
placeholder="Masukkan pekerjaan orang tua / wali"
onChange={(val) => { beasiswaDesa.create.form.pekerjaanOrtu = val.target.value }} />
labelProps={{ style: { fontSize: '0.9375rem', fontWeight: 600, lineHeight: 1.4 } }}
onChange={(val) => { beasiswaDesa.create.form.pekerjaanOrtu = val.target.value }}
/>
<TextInput
label="Penghasilan Orang Tua / Wali"
placeholder="Masukkan penghasilan orang tua / wali"
onChange={(val) => { beasiswaDesa.create.form.penghasilan = val.target.value }} />
labelProps={{ style: { fontSize: '0.9375rem', fontWeight: 600, lineHeight: 1.4 } }}
onChange={(val) => { beasiswaDesa.create.form.penghasilan = val.target.value }}
/>
<TextInput
label="No HP"
placeholder="Masukkan no hp"
onChange={(val) => { beasiswaDesa.create.form.noHp = val.target.value }} />
labelProps={{ style: { fontSize: '0.9375rem', fontWeight: 600, lineHeight: 1.4 } }}
onChange={(val) => { beasiswaDesa.create.form.noHp = val.target.value }}
/>
<Group justify="flex-end" mt="md">
<Button variant="default" radius="xl" onClick={close}>Batal</Button>
<Button radius="xl" bg={colors['blue-button']} onClick={handleSubmit}>Kirim</Button>
<Button
variant="default"
radius="xl"
onClick={close}
style={{ fontSize: '0.9375rem', fontWeight: 600, lineHeight: 1.4 }}
>
Batal
</Button>
<Button
radius="xl"
bg={colors['blue-button']}
onClick={handleSubmit}
style={{ fontSize: '0.9375rem', fontWeight: 600, lineHeight: 1.4 }}
>
Kirim
</Button>
</Group>
</Stack>
</Paper>
</Modal>
</Box>
);
}
}

View File

@@ -38,11 +38,17 @@ function Page() {
</Box>
<Box px={{ base: 'md', md: 120 }} pb={80}>
<Box mb="lg">
<Title ta="center" order={1} fw="bold" c={colors['blue-button']} fz={{ base: 28, md: 38 }}>
<Title ta="center" order={1} fw="bold" c={colors['blue-button']} fz={{ base: 'xl', md: '2em' }} lh={1.15}>
Program Bimbingan Belajar Desa
</Title>
<Divider size="sm" my="md" mx="auto" w="60%" color={colors['blue-button']} />
<Text ta="center" fz="lg" c="black" px={{ base: 'sm', md: 120 }}>
<Text
ta="center"
fz={{ base: 'sm', md: 'md' }}
c="black"
lh={{ base: 1.6, md: 1.5 }}
px={{ base: 'sm', md: 120 }}
>
Program unggulan untuk mendukung siswa Desa Darmasaba memahami pelajaran sekolah, meningkatkan prestasi akademik, dan menumbuhkan semangat belajar sejak dini.
</Text>
</Box>
@@ -55,11 +61,16 @@ function Page() {
<IconBook2 size={36} stroke={1.5} color={colors['blue-button']} />
</Box>
</Tooltip>
<Title order={3} fw={700} c={colors['blue-button']} mb="xs">
<Title order={3} fw={700} c={colors['blue-button']} lh={1.2} fz={{ base: 'sm', md: 'md' }}>
{stateTujuanProgram.findById.data?.judul}
</Title>
</Group>
<Text fz="md" style={{wordBreak: "break-word", whiteSpace: "normal"}} lh={1.6} dangerouslySetInnerHTML={{ __html: stateTujuanProgram.findById.data?.deskripsi }} />
<Text
fz={{ base: 'xs', md: 'md' }}
lh={{ base: 1.6, md: 1.5 }}
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
dangerouslySetInnerHTML={{ __html: stateTujuanProgram.findById.data?.deskripsi }}
/>
</Stack>
</Paper>
<Paper p="xl" radius="lg" shadow="md" withBorder bg={colors['white-trans-1']}>
@@ -70,11 +81,16 @@ function Page() {
<IconMapPin size={36} stroke={1.5} color={colors['blue-button']} />
</Box>
</Tooltip>
<Title order={3} fw={700} c={colors['blue-button']} mb="xs">
<Title order={3} fw={700} c={colors['blue-button']} lh={1.2} fz={{ base: 'sm', md: 'md' }}>
{stateLokasiDanJadwal.findById.data?.judul}
</Title>
</Group>
<Text fz="md" lh={1.6} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: stateLokasiDanJadwal.findById.data?.deskripsi }} />
<Text
fz={{ base: 'xs', md: 'md' }}
lh={{ base: 1.6, md: 1.5 }}
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
dangerouslySetInnerHTML={{ __html: stateLokasiDanJadwal.findById.data?.deskripsi }}
/>
</Stack>
</Paper>
<Paper p="xl" radius="lg" shadow="md" withBorder bg={colors['white-trans-1']}>
@@ -85,11 +101,16 @@ function Page() {
<IconCalendarTime size={36} stroke={1.5} color={colors['blue-button']} />
</Box>
</Tooltip>
<Title order={3} fw={700} c={colors['blue-button']} mb="xs">
<Title order={3} fw={700} c={colors['blue-button']} lh={1.2} fz={{ base: 'sm', md: 'md' }}>
{stateFasilitas.findById.data?.judul}
</Title>
</Group>
<Text fz="md" lh={1.6} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: stateFasilitas.findById.data?.deskripsi }} />
<Text
fz={{ base: 'xs', md: 'md' }}
lh={{ base: 1.6, md: 1.5 }}
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
dangerouslySetInnerHTML={{ __html: stateFasilitas.findById.data?.deskripsi }}
/>
</Stack>
</Paper>
</SimpleGrid>
@@ -98,4 +119,4 @@ function Page() {
);
}
export default Page;
export default Page;

View File

@@ -1,8 +1,24 @@
'use client'
import colors from '@/con/colors';
import { Box } from '@mantine/core';
import { usePathname } from 'next/navigation';
import React, { Suspense } from 'react';
import LayoutTabs from './_lib/layoutTabs';
function Layout({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
const segments = pathname.split('/').filter(Boolean);
const isDetailPage = segments.length === 5; // [darmasaba, desa, berita, kategori, id]
if (isDetailPage) {
// Tampilkan tanpa tab menu
return (
<Box bg={colors.Bg}>
{children}
</Box>
);
}
return (
<Suspense fallback={<div>Loading...</div>}>
<LayoutTabs>

View File

@@ -517,7 +517,7 @@ function NodeCard({ node, router }: any) {
fontWeight: 600,
}}
>
<Text fz={{ base: 12, md: 13 }} lh={1} ta="center">Lihat Detail</Text>
<Text c={"white"} fz={{ base: 12, md: 13 }} lh={1} ta="center">Lihat Detail</Text>
</Button>
)}
</Stack>

View File

@@ -10,10 +10,19 @@ import {
Stack,
Text,
Title,
Progress,
Group,
} from '@mantine/core';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { authStore } from '@/store/authStore'; // ✅ integrasi authStore
import { authStore } from '@/store/authStore';
// ⚙️ Configuration
const CONFIG = {
POLL_INTERVAL: 3000, // 3 detik
MAX_RETRIES: 2, // 2x retry
TIMEOUT_DURATION: 5 * 60 * 1000, // 5 menit (300 detik)
};
async function fetchUser() {
const res = await fetch('/api/auth/me', {
@@ -26,21 +35,48 @@ async function fetchUser() {
return res.json();
}
function formatTime(seconds: number): string {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
export default function WaitingRoom() {
const router = useRouter();
const [user, setUser] = useState<any>(null);
const [error, setError] = useState<string | null>(null);
const [isRedirecting, setIsRedirecting] = useState(false);
const [retryCount, setRetryCount] = useState(0);
const MAX_RETRIES = 2;
// ⏱️ Countdown timer
const [timeLeft, setTimeLeft] = useState(CONFIG.TIMEOUT_DURATION / 1000); // dalam detik
const [hasTimedOut, setHasTimedOut] = useState(false);
// ⏱️ Countdown effect
useEffect(() => {
if (isRedirecting || hasTimedOut) return;
const countdownInterval = setInterval(() => {
setTimeLeft((prev) => {
if (prev <= 1) {
setHasTimedOut(true);
setError('Waktu tunggu habis. Silakan hubungi administrator atau coba login ulang nanti.');
return 0;
}
return prev - 1;
});
}, 1000);
return () => clearInterval(countdownInterval);
}, [isRedirecting, hasTimedOut]);
// 🔄 Polling effect
useEffect(() => {
let isMounted = true;
let interval: ReturnType<typeof setInterval>;
const poll = async () => {
if (isRedirecting || !isMounted) return;
if (isRedirecting || !isMounted || hasTimedOut) return;
try {
const data = await fetchUser();
@@ -59,12 +95,11 @@ export default function WaitingRoom() {
});
}
// In the poll function
// ✅ Check if approved
if (currentUser?.isActive === true) {
setIsRedirecting(true);
clearInterval(interval);
// Update authStore with the current user data
authStore.setUser({
id: currentUser.id,
name: currentUser.name || 'User',
@@ -78,7 +113,7 @@ export default function WaitingRoom() {
localStorage.removeItem('auth_nomor');
localStorage.removeItem('auth_username');
// Force a session refresh
// Force session refresh
try {
const res = await fetch('/api/auth/refresh-session', {
method: 'POST',
@@ -99,26 +134,26 @@ export default function WaitingRoom() {
redirectPath = '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
break;
}
window.location.href = redirectPath; // Use window.location to force full page reload
window.location.href = redirectPath;
}
} catch (error) {
console.error('Error refreshing session:', error);
router.refresh(); // Fallback to client-side refresh
router.refresh();
}
}
} catch (err: any) {
if (!isMounted) return;
if (err.message.includes('401')) {
if (retryCount < MAX_RETRIES) {
if (retryCount < CONFIG.MAX_RETRIES) {
setRetryCount((prev) => prev + 1);
setTimeout(() => {
if (isMounted) interval = setInterval(poll, 3000);
if (isMounted) interval = setInterval(poll, CONFIG.POLL_INTERVAL);
}, 800);
} else {
setError('Sesi tidak valid. Silakan login ulang.');
clearInterval(interval);
authStore.setUser(null); // ✅ clear sesi
authStore.setUser(null);
}
} else {
console.error('Error polling:', err);
@@ -126,26 +161,53 @@ export default function WaitingRoom() {
}
};
interval = setInterval(poll, 3000);
interval = setInterval(poll, CONFIG.POLL_INTERVAL);
return () => {
isMounted = false;
if (interval) clearInterval(interval);
};
}, [router, isRedirecting, retryCount]);
}, [router, isRedirecting, retryCount, hasTimedOut]);
// ✅ UI Error
if (error) {
// 🚨 Handle logout
const handleLogout = async () => {
try {
await fetch('/api/auth/logout', {
method: 'POST',
credentials: 'include'
});
} catch (err) {
console.error('Logout error:', err);
} finally {
authStore.setUser(null);
localStorage.clear();
router.push('/login');
}
};
// ❌ UI Error / Timeout
if (error || hasTimedOut) {
return (
<Center h="100vh">
<Paper p="xl" radius="md" bg={colors['white-trans-1']} w={400}>
<Center h="100vh" bg={colors.Bg}>
<Paper p="xl" radius="md" bg={colors['white-trans-1']} w={{ base: '90%', sm: 400 }}>
<Stack align="center" gap="md">
<Title order={3} c="red">
Sesi Tidak Valid
<Title order={3} c="red" ta="center">
{hasTimedOut ? '⏱️ Waktu Habis' : '❌ Sesi Tidak Valid'}
</Title>
<Text>{error}</Text>
<Button onClick={() => router.push('/login')}>
Login Ulang
</Button>
<Text ta="center" size="sm">
{error || 'Waktu tunggu persetujuan telah habis.'}
</Text>
<Text ta="center" size="xs" c="dimmed">
Silakan hubungi Superadmin atau coba login ulang nanti.
</Text>
<Group gap="sm" w="100%">
<Button
fullWidth
variant="outline"
onClick={handleLogout}
>
Kembali ke Login
</Button>
</Group>
</Stack>
</Paper>
</Center>
@@ -171,24 +233,56 @@ export default function WaitingRoom() {
);
}
// UI Default (MENUNGGU) — INI YANG KAMU HILANGKAN!
// UI Default (MENUNGGU)
const progressValue = ((CONFIG.TIMEOUT_DURATION / 1000 - timeLeft) / (CONFIG.TIMEOUT_DURATION / 1000)) * 100;
return (
<Center h="100vh" bg={colors.Bg}>
<Paper p="xl" radius="md" bg={colors['white-trans-1']} w={{ base: '90%', sm: 400 }}>
<Stack align="center" gap="lg">
<Title order={2} c={colors['blue-button']} ta="center">
Menunggu Persetujuan
Menunggu Persetujuan
</Title>
<Text ta="center" c="dimmed">
Akun Anda sedang dalam proses verifikasi oleh Superadmin.
</Text>
<Text ta="center" size="sm" c="dimmed">
<Text ta="center" size="sm" fw={500}>
Nomor: {user?.nomor || '...'}
</Text>
{/* ⏱️ Countdown Timer */}
<Stack w="100%" gap="xs">
<Group justify="space-between" w="100%">
<Text size="sm" c="dimmed">Sisa waktu:</Text>
<Text size="sm" fw={600} c={timeLeft < 60 ? 'red' : colors['blue-button']}>
{formatTime(timeLeft)}
</Text>
</Group>
<Progress
value={progressValue}
color={timeLeft < 60 ? 'red' : colors['blue-button']}
size="sm"
animated
/>
</Stack>
<Loader size="sm" color={colors['blue-button']} />
<Text ta="center" size="xs" c="dimmed">
Jangan tutup halaman ini. Anda akan dialihkan otomatis setelah disetujui.
</Text>
{/* 🚪 Tombol Keluar */}
<Button
variant="subtle"
size="xs"
onClick={handleLogout}
c="dimmed"
>
Keluar dari Halaman Ini
</Button>
</Stack>
</Paper>
</Center>