Fix QC Kak Ayu 15 Des

Fix QC Kak Inno 15 Des
Fix UI User Font Size, Font Weight, Line Height
Fix UI Admin Font Size, Font Weight, Line Height & UI Mobile
This commit is contained in:
2025-12-16 16:37:17 +08:00
parent 342e9bbc65
commit c8484357cb
34 changed files with 1458 additions and 661 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -69,7 +69,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
keepMounted={false} keepMounted={false}
> >
{/* ✅ Scroll horizontal wrapper */} {/* ✅ Scroll horizontal wrapper */}
<Box visibleFrom='md'> <Box visibleFrom='md' pb={10}>
<ScrollArea type="auto" offsetScrollbars> <ScrollArea type="auto" offsetScrollbars>
<TabsList <TabsList
p="sm" p="sm"
@@ -102,7 +102,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
</ScrollArea> </ScrollArea>
</Box> </Box>
<Box hiddenFrom='md'> <Box hiddenFrom='md' pb={10}>
<ScrollArea <ScrollArea
type="auto" type="auto"
offsetScrollbars={false} offsetScrollbars={false}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -75,7 +75,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
keepMounted={false} keepMounted={false}
> >
{/* ✅ Scroll horizontal wrapper */} {/* ✅ Scroll horizontal wrapper */}
<Box visibleFrom='md'> <Box visibleFrom='md' pb={10}>
<ScrollArea type="auto" offsetScrollbars> <ScrollArea type="auto" offsetScrollbars>
<TabsList <TabsList
p="sm" p="sm"
@@ -108,7 +108,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
</ScrollArea> </ScrollArea>
</Box> </Box>
<Box hiddenFrom='md'> <Box hiddenFrom='md' pb={10}>
<ScrollArea <ScrollArea
type="auto" type="auto"
offsetScrollbars={false} offsetScrollbars={false}

View File

@@ -33,7 +33,7 @@ import { useEffect, useState } from "react";
import { getNavbar } from "./(dashboard)/user&role/_com/dynamicNavbar"; import { getNavbar } from "./(dashboard)/user&role/_com/dynamicNavbar";
export default function Layout({ children }: { children: React.ReactNode }) { 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 [loading, setLoading] = useState(true);
const [isLoggingOut, setIsLoggingOut] = useState(false); const [isLoggingOut, setIsLoggingOut] = useState(false);
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true); const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
@@ -45,21 +45,19 @@ export default function Layout({ children }: { children: React.ReactNode }) {
const fetchUser = async () => { const fetchUser = async () => {
try { try {
const res = await fetch('/api/auth/me', { const res = await fetch('/api/auth/me', {
credentials: 'include' // ✅ ADD credentials credentials: 'include'
}); });
const data = await res.json(); const data = await res.json();
if (data.user) { if (data.user) {
// ✅ Check if user is NOT active → redirect to waiting room
if (!data.user.isActive) { if (!data.user.isActive) {
authStore.setUser(null); authStore.setUser(null);
router.replace('/waiting-room'); router.replace('/waiting-room');
return; return;
} }
// ✅ Fetch menuIds
const menuRes = await fetch(`/api/admin/user-menu-access?userId=${data.user.id}`, { const menuRes = await fetch(`/api/admin/user-menu-access?userId=${data.user.id}`, {
credentials: 'include' // ✅ ADD credentials credentials: 'include'
}); });
const menuData = await menuRes.json(); const menuData = await menuRes.json();
@@ -67,7 +65,6 @@ export default function Layout({ children }: { children: React.ReactNode }) {
? [...menuData.menuIds] ? [...menuData.menuIds]
: null; : null;
// ✅ Set user dengan menuIds yang fresh
authStore.setUser({ authStore.setUser({
id: data.user.id, id: data.user.id,
name: data.user.name, name: data.user.name,
@@ -76,7 +73,6 @@ export default function Layout({ children }: { children: React.ReactNode }) {
isActive: data.user.isActive isActive: data.user.isActive
}); });
// ✅ IMPROVED: Redirect ONLY if di root /admin
const currentPath = window.location.pathname; const currentPath = window.location.pathname;
if (currentPath === '/admin') { if (currentPath === '/admin') {
@@ -84,7 +80,6 @@ export default function Layout({ children }: { children: React.ReactNode }) {
console.log('🔄 Redirecting from /admin to:', expectedPath); console.log('🔄 Redirecting from /admin to:', expectedPath);
router.replace(expectedPath); router.replace(expectedPath);
} }
// ✅ Jangan redirect jika user sudah di path yang valid
} else { } else {
authStore.setUser(null); authStore.setUser(null);
@@ -100,17 +95,17 @@ export default function Layout({ children }: { children: React.ReactNode }) {
}; };
fetchUser(); fetchUser();
}, [router]); // ✅ Only depend on router }, [router]);
const getRedirectPath = (roleId: number): string => { const getRedirectPath = (roleId: number): string => {
switch (roleId) { switch (roleId) {
case 0: // DEVELOPER case 0:
case 1: // SUPERADMIN case 1:
case 2: // ADMIN_DESA case 2:
return '/admin/landing-page/profil/program-inovasi'; return '/admin/landing-page/profil/program-inovasi';
case 3: // ADMIN_KESEHATAN case 3:
return '/admin/kesehatan/posyandu'; return '/admin/kesehatan/posyandu';
case 4: // ADMIN_PENDIDIKAN case 4:
return '/admin/pendidikan/info-sekolah/jenjang-pendidikan'; return '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
default: default:
return '/admin'; return '/admin';
@@ -139,7 +134,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
const response = await fetch('/api/auth/logout', { const response = await fetch('/api/auth/logout', {
method: 'POST', method: 'POST',
credentials: 'include' // ✅ ADD credentials credentials: 'include'
}); });
const result = await response.json(); const result = await response.json();
@@ -163,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 ( return (
<AppShell <AppShell
suppressHydrationWarning suppressHydrationWarning
@@ -177,7 +178,6 @@ export default function Layout({ children }: { children: React.ReactNode }) {
}} }}
padding="md" padding="md"
> >
{/* ... rest of your JSX (Header, Navbar, Main) sama seperti sebelumnya ... */}
<AppShellHeader <AppShellHeader
style={{ style={{
background: "linear-gradient(90deg, #ffffff, #f9fbff)", background: "linear-gradient(90deg, #ffffff, #f9fbff)",
@@ -230,16 +230,48 @@ export default function Layout({ children }: { children: React.ReactNode }) {
</AppShellHeader> </AppShellHeader>
<AppShellNavbar component={ScrollArea} style={{ background: "#ffffff", borderRight: `1px solid ${colors["blue-button"]}20` }} p={{ base: 'xs', sm: 'sm' }}> <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"> <AppShell.Section p="sm">
{currentNav.map((v, k) => { {currentNav.map((v, k) => {
const isParentActive = segments.includes(_.lowerCase(v.name)); const isParentActive = segments.includes(_.lowerCase(v.name));
return ( 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) => { {v.children.map((child, key) => {
const isChildActive = segments.includes(_.lowerCase(child.name)); const isChildActive = segments.includes(_.lowerCase(child.name));
return ( 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> </NavLink>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
'use client' 'use client'
import lowonganKerjaState from '@/app/admin/(dashboard)/_state/ekonomi/lowongan-kerja'; import lowonganKerjaState from '@/app/admin/(dashboard)/_state/ekonomi/lowongan-kerja';
import colors from '@/con/colors'; 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 { useDebouncedValue } from '@mantine/hooks';
import { IconBriefcase, IconClock, IconMapPin, IconSearch } from '@tabler/icons-react'; import { IconBriefcase, IconClock, IconMapPin, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@@ -53,17 +53,19 @@ function Page() {
<BackButton /> <BackButton />
</Box> </Box>
<Box px={{ base: 'md', md: 100 }} pb={80}> <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 Lowongan Kerja Lokal
</Text> </Title>
<Group justify='center'> <Group justify='center'>
<TextInput <TextInput
radius={'xl'} radius={'xl'}
w={{ base: 500, md: 700 }} w={{ base: '100%', md: 700 }}
placeholder='Cari Pekerjaan' placeholder='Cari Pekerjaan'
leftSection={<IconSearch size={20} />} leftSection={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
fz={{ base: 'sm', md: 'md' }}
lh={1.5}
/> />
</Group> </Group>
</Box> </Box>
@@ -80,30 +82,42 @@ function Page() {
<Paper key={k} p={'xl'}> <Paper key={k} p={'xl'}>
<Stack gap={'md'}> <Stack gap={'md'}>
<Box> <Box>
<Flex gap={'xl'} align={'center'}> <Flex gap={{ base: 'md', md: 'xl' }} align={'center'}>
<IconBriefcase color={colors['blue-button']} size={50} /> <IconBriefcase color={colors['blue-button']} size={40} />
<Box> <Box>
<Text fw={'bold'} fz={'h4'} c={colors['blue-button']}>{v.posisi}</Text> <Text fw={'bold'} fz={{ base: 'lg', md: 'h4' }} c={colors['blue-button']} lh={1.3}>
<Text fz={'h4'}>{v.namaPerusahaan}</Text> {v.posisi}
</Text>
<Text fz={{ base: 'md', md: 'h4' }} lh={1.5}>
{v.namaPerusahaan}
</Text>
</Box> </Box>
</Flex> </Flex>
</Box> </Box>
<Box> <Box>
<Flex gap={'xl'} align={'center'}> <Flex gap={{ base: 'md', md: 'xl' }} align={'center'}>
<IconMapPin color={colors['blue-button']} size={50} /> <IconMapPin color={colors['blue-button']} size={40} />
<Text fz={'h4'}>{v.lokasi}</Text> <Text fz={{ base: 'md', md: 'h4' }} lh={1.5}>
{v.lokasi}
</Text>
</Flex> </Flex>
</Box> </Box>
<Box> <Box>
<Flex gap={'xl'} align={'center'}> <Flex gap={{ base: 'md', md: 'xl' }} align={'center'}>
<IconClock color={colors['blue-button']} size={50} /> <IconClock color={colors['blue-button']} size={40} />
<Box> <Box>
<Text fw={'bold'} fz={'h4'} c={colors['blue-button']}>Full Time</Text> <Text fw={'bold'} fz={{ base: 'md', md: 'h4' }} c={colors['blue-button']} lh={1.3}>
<Text fz={'h4'}>{formatCurrency(v.gaji)}</Text> Full Time
</Text>
<Text fz={{ base: 'sm', md: 'h4' }} lh={1.5}>
{formatCurrency(v.gaji)}
</Text>
</Box> </Box>
</Flex> </Flex>
</Box> </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> </Stack>
</Paper> </Paper>
) )

View File

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

View File

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

View File

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

View File

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

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 react-hooks/exhaustive-deps */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
'use client' 'use client'
import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi' import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi'
import colors from '@/con/colors' import colors from '@/con/colors'
import { import {
@@ -32,12 +31,13 @@ import {
IconZoomOut, IconZoomOut,
} from '@tabler/icons-react' } from '@tabler/icons-react'
import { debounce } from 'lodash' import { debounce } from 'lodash'
import { useTransitionRouter } from 'next-view-transitions'
import { OrganizationChart } from 'primereact/organizationchart' import { OrganizationChart } from 'primereact/organizationchart'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { useProxy } from 'valtio/utils' import { useProxy } from 'valtio/utils'
import BackButton from '../../desa/layanan/_com/BackButto' import BackButton from '../../desa/layanan/_com/BackButto'
import '../../ppid/struktur-ppid/struktur.css'
import { useMediaQuery } from '@mantine/hooks' import { useMediaQuery } from '@mantine/hooks'
import { useTransitionRouter } from 'next-view-transitions'
export default function Page() { export default function Page() {
return ( return (
@@ -49,14 +49,16 @@ export default function Page() {
paddingBottom: 48, paddingBottom: 48,
}} }}
> >
<Box px={{ base: 'md', md: 100 }} py="xl"> <Box px={{ base: 'md', md: 100 }} py={"xl"}>
<BackButton /> <BackButton />
<Stack align="center" gap="xl" mt="xl"> <Stack align="center" gap="xl" mt="xl">
<Title <Title
order={1} order={1}
ta="center" ta="center"
c={colors['blue-button']} 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 Struktur Organisasi & SK Pengurus BumDes
</Title> </Title>
@@ -75,14 +77,18 @@ export default function Page() {
} }
function StrukturOrganisasiBumDes() { function StrukturOrganisasiBumDes() {
const router = useTransitionRouter()
const stateOrganisasi: any = useProxy(stateStrukturBumDes.pegawai) const stateOrganisasi: any = useProxy(stateStrukturBumDes.pegawai)
const router = useTransitionRouter()
const chartContainerRef = useRef<HTMLDivElement>(null) const chartContainerRef = useRef<HTMLDivElement>(null)
const [scale, setScale] = useState(1) const [scale, setScale] = useState(1)
const [isFullscreen, setFullscreen] = useState(false) const [isFullscreen, setFullscreen] = useState(false)
const [searchQuery, setSearchQuery] = useState('') const [searchQuery, setSearchQuery] = useState('')
// debounce pencarian
const debouncedSearch = useRef( const debouncedSearch = useRef(
debounce((value: string) => setSearchQuery(value), 1000) debounce((value: string) => {
setSearchQuery(value)
}, 1000)
).current ).current
useEffect(() => { useEffect(() => {
@@ -90,8 +96,7 @@ function StrukturOrganisasiBumDes() {
}, []) }, [])
const isLoading = const isLoading =
!stateOrganisasi.findMany.data && !stateOrganisasi.findMany.data && stateOrganisasi.findMany.loading !== false
stateOrganisasi.findMany.loading !== false
if (isLoading) { if (isLoading) {
return ( return (
@@ -149,7 +154,7 @@ function StrukturOrganisasiBumDes() {
) )
} }
// 📊 susun struktur organisasi // 🧩 buat struktur organisasi
const posisiMap = new Map<string, any>() const posisiMap = new Map<string, any>()
const aktifPegawai = data.filter((p: any) => p.isActive) const aktifPegawai = data.filter((p: any) => p.isActive)
@@ -183,7 +188,6 @@ function StrukturOrganisasiBumDes() {
name: pegawai?.namaLengkap || 'Belum Ditugaskan', name: pegawai?.namaLengkap || 'Belum Ditugaskan',
title: node.nama || 'Tanpa Jabatan', title: node.nama || 'Tanpa Jabatan',
image: pegawai?.image?.link || '/img/default.png', image: pegawai?.image?.link || '/img/default.png',
description: node.deskripsi || '',
}, },
children: node.children?.map(toOrgChartFormat) || [], children: node.children?.map(toOrgChartFormat) || [],
} }
@@ -208,7 +212,7 @@ function StrukturOrganisasiBumDes() {
chartData = filterNodes(chartData) chartData = filterNodes(chartData)
} }
// 🔍 fullscreen dan zoom control // 🎬 fullscreen & zoom control
const toggleFullscreen = () => { const toggleFullscreen = () => {
if (!document.fullscreenElement) { if (!document.fullscreenElement) {
chartContainerRef.current?.requestFullscreen() chartContainerRef.current?.requestFullscreen()
@@ -225,7 +229,7 @@ function StrukturOrganisasiBumDes() {
return ( return (
<Stack align="center" mt="xl"> <Stack align="center" mt="xl">
{/* 🧭 Kontrol atas */} {/* 🔍 Controls */}
<Paper <Paper
shadow="xs" shadow="xs"
w={{ w={{
@@ -244,6 +248,7 @@ function StrukturOrganisasiBumDes() {
overflowX: 'auto' // ⬅️ untuk mencegah overflow overflowX: 'auto' // ⬅️ untuk mencegah overflow
}} }}
> >
<Stack gap="sm"> <Stack gap="sm">
<Group justify='center'> <Group justify='center'>
<TextInput <TextInput
@@ -360,165 +365,163 @@ function StrukturOrganisasiBumDes() {
</Stack> </Stack>
</Paper> </Paper>
{/* 🧩 Chart Container */} {/* 🧩 Chart Container */}
<Center style={{ width: '100%' }}> <Center style={{ width: '100%' }}>
<Box <Box
ref={chartContainerRef} 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={{ style={{
overflowX: 'auto', objectFit: 'cover',
overflowY: 'auto', }}
width: '100%', />
maxWidth: '100%', </Box>
padding: '32px 16px',
transition: 'transform 0.2s ease', {/* 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={{ <Text c={"white"} fz={{ base: 12, md: 13 }} lh={1} ta="center">Lihat Detail</Text>
</Button>
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>
)} )}
</Transition> </Stack>
) </Card>
} )}
</Transition>
)
}

View File

@@ -1,8 +1,24 @@
'use client' 'use client'
import colors from '@/con/colors';
import { Box } from '@mantine/core';
import { usePathname } from 'next/navigation';
import React, { Suspense } from 'react'; import React, { Suspense } from 'react';
import LayoutTabs from './_lib/layoutTabs'; import LayoutTabs from './_lib/layoutTabs';
function Layout({ children }: { children: React.ReactNode }) { 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 ( return (
<Suspense fallback={<div>Loading...</div>}> <Suspense fallback={<div>Loading...</div>}>
<LayoutTabs> <LayoutTabs>

View File

@@ -517,7 +517,7 @@ function NodeCard({ node, router }: any) {
fontWeight: 600, 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> </Button>
)} )}
</Stack> </Stack>

View File

@@ -10,10 +10,19 @@ import {
Stack, Stack,
Text, Text,
Title, Title,
Progress,
Group,
} from '@mantine/core'; } from '@mantine/core';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; 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() { async function fetchUser() {
const res = await fetch('/api/auth/me', { const res = await fetch('/api/auth/me', {
@@ -26,21 +35,48 @@ async function fetchUser() {
return res.json(); 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() { export default function WaitingRoom() {
const router = useRouter(); const router = useRouter();
const [user, setUser] = useState<any>(null); const [user, setUser] = useState<any>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [isRedirecting, setIsRedirecting] = useState(false); const [isRedirecting, setIsRedirecting] = useState(false);
const [retryCount, setRetryCount] = useState(0); 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(() => { useEffect(() => {
let isMounted = true; let isMounted = true;
let interval: ReturnType<typeof setInterval>; let interval: ReturnType<typeof setInterval>;
const poll = async () => { const poll = async () => {
if (isRedirecting || !isMounted) return; if (isRedirecting || !isMounted || hasTimedOut) return;
try { try {
const data = await fetchUser(); const data = await fetchUser();
@@ -59,12 +95,11 @@ export default function WaitingRoom() {
}); });
} }
// In the poll function // ✅ Check if approved
if (currentUser?.isActive === true) { if (currentUser?.isActive === true) {
setIsRedirecting(true); setIsRedirecting(true);
clearInterval(interval); clearInterval(interval);
// Update authStore with the current user data
authStore.setUser({ authStore.setUser({
id: currentUser.id, id: currentUser.id,
name: currentUser.name || 'User', name: currentUser.name || 'User',
@@ -78,7 +113,7 @@ export default function WaitingRoom() {
localStorage.removeItem('auth_nomor'); localStorage.removeItem('auth_nomor');
localStorage.removeItem('auth_username'); localStorage.removeItem('auth_username');
// Force a session refresh // Force session refresh
try { try {
const res = await fetch('/api/auth/refresh-session', { const res = await fetch('/api/auth/refresh-session', {
method: 'POST', method: 'POST',
@@ -99,26 +134,26 @@ export default function WaitingRoom() {
redirectPath = '/admin/pendidikan/info-sekolah/jenjang-pendidikan'; redirectPath = '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
break; break;
} }
window.location.href = redirectPath; // Use window.location to force full page reload window.location.href = redirectPath;
} }
} catch (error) { } catch (error) {
console.error('Error refreshing session:', error); console.error('Error refreshing session:', error);
router.refresh(); // Fallback to client-side refresh router.refresh();
} }
} }
} catch (err: any) { } catch (err: any) {
if (!isMounted) return; if (!isMounted) return;
if (err.message.includes('401')) { if (err.message.includes('401')) {
if (retryCount < MAX_RETRIES) { if (retryCount < CONFIG.MAX_RETRIES) {
setRetryCount((prev) => prev + 1); setRetryCount((prev) => prev + 1);
setTimeout(() => { setTimeout(() => {
if (isMounted) interval = setInterval(poll, 3000); if (isMounted) interval = setInterval(poll, CONFIG.POLL_INTERVAL);
}, 800); }, 800);
} else { } else {
setError('Sesi tidak valid. Silakan login ulang.'); setError('Sesi tidak valid. Silakan login ulang.');
clearInterval(interval); clearInterval(interval);
authStore.setUser(null); // ✅ clear sesi authStore.setUser(null);
} }
} else { } else {
console.error('Error polling:', err); console.error('Error polling:', err);
@@ -126,26 +161,53 @@ export default function WaitingRoom() {
} }
}; };
interval = setInterval(poll, 3000); interval = setInterval(poll, CONFIG.POLL_INTERVAL);
return () => { return () => {
isMounted = false; isMounted = false;
if (interval) clearInterval(interval); if (interval) clearInterval(interval);
}; };
}, [router, isRedirecting, retryCount]); }, [router, isRedirecting, retryCount, hasTimedOut]);
// ✅ UI Error // 🚨 Handle logout
if (error) { 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 ( return (
<Center h="100vh"> <Center h="100vh" bg={colors.Bg}>
<Paper p="xl" radius="md" bg={colors['white-trans-1']} w={400}> <Paper p="xl" radius="md" bg={colors['white-trans-1']} w={{ base: '90%', sm: 400 }}>
<Stack align="center" gap="md"> <Stack align="center" gap="md">
<Title order={3} c="red"> <Title order={3} c="red" ta="center">
Sesi Tidak Valid {hasTimedOut ? '⏱️ Waktu Habis' : '❌ Sesi Tidak Valid'}
</Title> </Title>
<Text>{error}</Text> <Text ta="center" size="sm">
<Button onClick={() => router.push('/login')}> {error || 'Waktu tunggu persetujuan telah habis.'}
Login Ulang </Text>
</Button> <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> </Stack>
</Paper> </Paper>
</Center> </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 ( return (
<Center h="100vh" bg={colors.Bg}> <Center h="100vh" bg={colors.Bg}>
<Paper p="xl" radius="md" bg={colors['white-trans-1']} w={{ base: '90%', sm: 400 }}> <Paper p="xl" radius="md" bg={colors['white-trans-1']} w={{ base: '90%', sm: 400 }}>
<Stack align="center" gap="lg"> <Stack align="center" gap="lg">
<Title order={2} c={colors['blue-button']} ta="center"> <Title order={2} c={colors['blue-button']} ta="center">
Menunggu Persetujuan Menunggu Persetujuan
</Title> </Title>
<Text ta="center" c="dimmed"> <Text ta="center" c="dimmed">
Akun Anda sedang dalam proses verifikasi oleh Superadmin. Akun Anda sedang dalam proses verifikasi oleh Superadmin.
</Text> </Text>
<Text ta="center" size="sm" c="dimmed">
<Text ta="center" size="sm" fw={500}>
Nomor: {user?.nomor || '...'} Nomor: {user?.nomor || '...'}
</Text> </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']} /> <Loader size="sm" color={colors['blue-button']} />
<Text ta="center" size="xs" c="dimmed"> <Text ta="center" size="xs" c="dimmed">
Jangan tutup halaman ini. Anda akan dialihkan otomatis setelah disetujui. Jangan tutup halaman ini. Anda akan dialihkan otomatis setelah disetujui.
</Text> </Text>
{/* 🚪 Tombol Keluar */}
<Button
variant="subtle"
size="xs"
onClick={handleLogout}
c="dimmed"
>
Keluar dari Halaman Ini
</Button>
</Stack> </Stack>
</Paper> </Paper>
</Center> </Center>