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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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