Compare commits

...

5 Commits

Author SHA1 Message Date
dc8793e3ae Fix QC Kak Inno 16 Des
Fix QC Kak Ayu 16 Des
FIx UI Admin Mobile Menu PPID
Fix Search Admin Menu Landing Page & Menu PPID
2025-12-17 17:37:58 +08:00
c8484357cb 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
2025-12-16 16:37:17 +08:00
342e9bbc65 Fix QC Kak Ayu Tgl 12
Fix QC Kak Ino Tgl 12
Fix UI Mobile Menu Keamanan
Fix UI Mobile Admin Menu Landing Page
2025-12-16 10:19:15 +08:00
f6f77d9e35 Fix QC Kak Inno Tgl 11 Des
Fix QC Kak Ayu Tgl 11 Des
Fix font style {font size, color, line height} menu kesehatan
2025-12-12 17:06:33 +08:00
a00481152c Fix Konsisten teks di tampilan mobile dan desktop
Fix QC Kak Inno tgl 10 Des
Fix QC Kak Ayu tgl 10 Des
2025-12-11 17:58:03 +08:00
160 changed files with 7381 additions and 4080 deletions

View File

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

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -136,12 +137,43 @@ const statepermohonanInformasiPublik = proxy({
};
}>[]
| null,
async load() {
const res = await ApiFetch.api.ppid.permohonaninformasipublik[
"find-many"
].get();
if (res.status === 200) {
statepermohonanInformasiPublik.findMany.data = res.data?.data ?? [];
page: 1,
totalPages: 1,
total: 0,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
// Change to arrow function
statepermohonanInformasiPublik.findMany.loading = true; // Use the full path to access the property
statepermohonanInformasiPublik.findMany.page = page;
statepermohonanInformasiPublik.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ppid.permohonaninformasipublik[
"find-many"
].get({
query,
});
if (res.status === 200 && res.data?.success) {
statepermohonanInformasiPublik.findMany.data = res.data.data || [];
statepermohonanInformasiPublik.findMany.total = res.data.total || 0;
statepermohonanInformasiPublik.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error("Failed to load permohonan keberatan informasi:", res.data?.message);
statepermohonanInformasiPublik.findMany.data = [];
statepermohonanInformasiPublik.findMany.total = 0;
statepermohonanInformasiPublik.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading permohonan keberatan informasi:", error);
statepermohonanInformasiPublik.findMany.data = [];
statepermohonanInformasiPublik.findMany.total = 0;
statepermohonanInformasiPublik.findMany.totalPages = 1;
} finally {
statepermohonanInformasiPublik.findMany.loading = false;
}
},
},

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -57,17 +58,48 @@ const permohonanKeberatanInformasi = proxy({
},
},
findMany: {
data: null as
data: null as
| null
| Prisma.FormulirPermohonanKeberatanGetPayload<{
omit: { isActive: true };
}>[]
| null,
async load() {
const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik[
"find-many"
].get();
if (res.status === 200) {
permohonanKeberatanInformasi.findMany.data = res.data?.data ?? [];
}>[],
page: 1,
totalPages: 1,
total: 0,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
// Change to arrow function
permohonanKeberatanInformasi.findMany.loading = true; // Use the full path to access the property
permohonanKeberatanInformasi.findMany.page = page;
permohonanKeberatanInformasi.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik[
"find-many"
].get({
query,
});
if (res.status === 200 && res.data?.success) {
permohonanKeberatanInformasi.findMany.data = res.data.data || [];
permohonanKeberatanInformasi.findMany.total = res.data.total || 0;
permohonanKeberatanInformasi.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error("Failed to load permohonan keberatan informasi:", res.data?.message);
permohonanKeberatanInformasi.findMany.data = [];
permohonanKeberatanInformasi.findMany.total = 0;
permohonanKeberatanInformasi.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading permohonan keberatan informasi:", error);
permohonanKeberatanInformasi.findMany.data = [];
permohonanKeberatanInformasi.findMany.total = 0;
permohonanKeberatanInformasi.findMany.totalPages = 1;
} finally {
permohonanKeberatanInformasi.findMany.loading = false;
}
},
},

View File

@@ -4,7 +4,7 @@ import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors';
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
@@ -108,7 +108,7 @@ function DetailPerbekelDariMasa() {
radius="md"
size="md"
>
<IconX size={20} />
<IconTrash size={20} />
</Button>
<Button

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
'use client'
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
@@ -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"
@@ -106,7 +106,7 @@ function DetailSDGSDesa() {
size="md"
disabled={sdgsState.delete.loading}
>
<IconX size={20} />
<IconTrash size={20} />
</Button>
<Button

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

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

View File

@@ -204,7 +204,7 @@ function EditAPBDes() {
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box px={{ base: 0, md: 'lg' }} py="xs">
<Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
@@ -215,7 +215,7 @@ function EditAPBDes() {
</Group>
<Paper
w={{ base: '100%', md: '100%' }}
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
@@ -368,6 +368,13 @@ function EditAPBDes() {
{ value: '2', label: 'Level 2 (Sub-kelompok)' },
{ value: '3', label: 'Level 3 (Detail)' },
]}
styles={{
option: {
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
},
}}
value={String(newItem.level)}
onChange={(val) => setNewItem({ ...newItem, level: Number(val) || 1 })}
/>
@@ -378,6 +385,13 @@ function EditAPBDes() {
{ value: 'belanja', label: 'Belanja' },
{ value: 'pembiayaan', label: 'Pembiayaan' },
]}
styles={{
option: {
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
},
}}
value={newItem.tipe}
onChange={(val) => setNewItem({ ...newItem, tipe: (val as any) || 'pendapatan' })}
/>

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} />
@@ -353,6 +353,13 @@ function CreateAPBDes() {
{ value: '2', label: 'Level 2 (Sub-kelompok)' },
{ value: '3', label: 'Level 3 (Detail)' },
]}
styles={{
option: {
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
},
}}
value={String(newItem.level)}
onChange={(val) => setNewItem({ ...newItem, level: Number(val) || 1 })}
/>
@@ -363,6 +370,13 @@ function CreateAPBDes() {
{ value: 'belanja', label: 'Belanja' },
{ value: 'pembiayaan', label: 'Pembiayaan' },
]}
styles={{
option: {
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
},
}}
value={newItem.level === 1 ? null : newItem.tipe}
onChange={(val) => setNewItem({ ...newItem, tipe: val as any })}
disabled={newItem.level === 1}

View File

@@ -18,7 +18,7 @@ import {
Text,
Title,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconFile, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
@@ -45,82 +45,97 @@ function APBDes() {
function ListAPBDes({ search }: { search: string }) {
const listState = useProxy(apbdes);
const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { data, page, totalPages, loading, load } = listState.findMany;
useShallowEffect(() => {
load(page, 10, search);
}, [page, search]);
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
const filteredData = data || [];
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 +143,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>
<Box>
<Text fz="sm"fw={600} lh={1.4}>
Tahun
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.tahun || '-'}
</Text>
</Box>
<Box>
<Text fz="sm"fw={600} 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>
)}
</Box>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={14} />}
onClick={() => router.push(`/admin/landing-page/apbdes/${item.id}`)}
mt="sm"
fz="xs"
lh={1.4}
>
Detail
</Button>
</Stack>
</Paper>
))
) : (
<Paper withBorder bg={colors['white-1']} p="md" radius="md">
<Center py="lg">
<Text c="dimmed" fz="xs" lh={1.4}>
Tidak ada data APBDes yang cocok
</Text>
</Center>
</Paper>
)}
</Stack>
</Paper>
</Box>
<Center mt="xl">
<Pagination
value={page}
onChange={(newPage) => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
@@ -32,6 +32,7 @@ function ListKategoriPrestasi({ search }: { search: string }) {
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter()
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const handleHapus = () => {
if (selectedId) {
@@ -50,14 +51,14 @@ function ListKategoriPrestasi({ search }: { search: string }) {
} = stateKategori.findMany
useShallowEffect(() => {
load(page, 10, search)
}, [page, search])
load(page, 10, debouncedSearch)
}, [page, debouncedSearch])
const filteredData = data || []
if (loading || !data) {
return (
<Stack py={10}>
<Stack py="md">
<Skeleton h={500} />
</Stack>
)
@@ -65,28 +66,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 +101,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 { useDebouncedValue, 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,8 @@ function ListPrestasiDesa() {
function ListPrestasi({ search }: { search: string }) {
const listState = useProxy(prestasiState.prestasiDesa)
const router = useRouter();
const isMobile = useMediaQuery('(max-width: 768px)');
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const {
data,
@@ -38,93 +40,149 @@ function ListPrestasi({ search }: { search: string }) {
} = listState.findMany
useShallowEffect(() => {
load(page, 10, search);
}, [page, search]);
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
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" style={{ overflowX: 'auto' }}>
<Table highlightOnHover striped verticalSpacing="sm" miw={800}>
<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>Nama Prestasi</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Kategori</TableTh>
<TableTh 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 style={{ maxWidth: 250 }}>
<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 style={{ maxWidth: 250 }}>
<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>
<Text truncate="end" fz="md" lh={1.5}>
{item.kategori?.name || 'Tidak ada kategori'}
</Text>
</TableTd>
<TableTd style={{ width: '25%', textAlign: 'center' }}>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() => router.push(`/admin/landing-page/prestasi-desa/list-prestasi-desa/${item.id}`)}
>
Detail
</Button>
<TableTd ta="center">
<Center>
<Button
size="sm"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() => router.push(`/admin/landing-page/prestasi-desa/list-prestasi-desa/${item.id}`)}
>
Detail
</Button>
</Center>
</TableTd>
</TableTr>
))
) : (
<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={"xs"}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Nama Prestasi</Text>
<Text fz="sm" fw={500} lh={1.4} lineClamp={2}>
{item.name}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Deskripsi</Text>
<Text fz="sm" fw={500} lh={1.5} lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Kategori</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.kategori?.name || 'Tidak ada kategori'}
</Text>
</Box>
<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 +190,4 @@ function ListPrestasi({ search }: { search: string }) {
)
}
export default ListPrestasiDesa;
export default ListPrestasiDesa;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -79,7 +79,7 @@ function EditDaftarInformasiPublik() {
};
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

@@ -52,7 +52,7 @@ function DetailDaftarInformasiPublik() {
<Paper
withBorder
w={{ base: '100%', md: '60%' }}
w={{ base: '100%', md: '70%' }}
bg={colors['white-1']}
p="lg"
radius="md"
@@ -83,39 +83,39 @@ function DetailDaftarInformasiPublik() {
<Box>
<Text fz="lg" fw="bold" mb={4}>Deskripsi</Text>
<Box
fz="md"
c="dimmed"
<Text
px={"xs"}
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
className="prose max-w-none"
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/>
</Box>
<Group gap="sm" mt="md">
<Button
variant="light"
color="green"
leftSection={<IconEdit size={18} />}
onClick={() => router.push(`/admin/ppid/daftar-informasi-publik/${data.id}/edit`)}
disabled={!data}
>
Edit
</Button>
<Button
variant="light"
color="green"
leftSection={<IconEdit size={18} />}
onClick={() => router.push(`/admin/ppid/daftar-informasi-publik/${data.id}/edit`)}
disabled={!data}
>
Edit
</Button>
<Button
variant="light"
color="red"
leftSection={<IconTrash size={18} />}
onClick={() => {
setSelectedId(data.id);
setModalHapus(true);
}}
disabled={stateDaftarInformasi.delete.loading || !data}
loading={stateDaftarInformasi.delete.loading}
>
Hapus
</Button>
<Button
variant="light"
color="red"
leftSection={<IconTrash size={18} />}
onClick={() => {
setSelectedId(data.id);
setModalHapus(true);
}}
disabled={stateDaftarInformasi.delete.loading || !data}
loading={stateDaftarInformasi.delete.loading}
>
Hapus
</Button>
</Group>
</Stack>
</Paper>

View File

@@ -41,7 +41,7 @@ export default function CreateDaftarInformasi() {
};
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,24 @@
'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, useViewportSize } from '@mantine/hooks';
import {
Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
} from '@mantine/core';
import { useDebouncedValue, useShallowEffect, useViewportSize } from '@mantine/hooks';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
@@ -10,7 +27,7 @@ import HeaderSearch from '../../_com/header';
import daftarInformasiPublik from '../../_state/ppid/daftar_informasi_publik/daftarInformasiPublik';
function DaftarInformasiPublik() {
const [search, setSearch] = useState("");
const [search, setSearch] = useState('');
return (
<Box>
<HeaderSearch
@@ -26,102 +43,158 @@ function DaftarInformasiPublik() {
}
function ListDaftarInformasi({ search }: { search: string }) {
const listData = useProxy(daftarInformasiPublik)
const router = useRouter()
const listData = useProxy(daftarInformasiPublik);
const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { data, page, totalPages, loading, load } = listData.findMany
const { width } = useViewportSize()
const isMobile = width < 768
const { data, page, totalPages, loading, load } = listData.findMany;
const { width } = useViewportSize();
const isMobile = width < 768;
useShallowEffect(() => {
load(page, 10, search)
}, [page, search])
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
const filteredData = data || []
const filteredData = data || [];
if (loading || !data) {
return (
<Stack py={10}>
<Stack py="md">
<Skeleton height={600} radius="md" />
</Stack>
)
);
}
return (
<Box py="md">
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>List Daftar Informasi Publik</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ppid/daftar-informasi-publik/create')}
>
{isMobile ? 'Tambah' : 'Tambah Baru'}
</Button>
<Box py={{ base: 'md', md: 'lg' }}>
<Paper withBorder bg={colors['white-1']} p={{ base: 'sm', md: 'lg' }} shadow="md" radius="md">
<Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
<Title order={2} lh={1.2}>
List Daftar Informasi Publik
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ppid/daftar-informasi-publik/create')}
>
{isMobile ? 'Tambah' : 'Tambah Baru'}
</Button>
</Group>
{filteredData.length === 0 ? (
<Stack align="center" py="xl">
<IconDeviceImacCog size={40} stroke={1.5} color={colors['blue-button']} />
<Text fw={500} c="dimmed">Belum ada informasi publik yang tersedia</Text>
<Text fz={{ base: 'sm', md: 'md' }} fw={500} c="dimmed" lh={1.5}>
Belum ada informasi publik yang tersedia
</Text>
</Stack>
) : (
<Box style={{ overflowX: 'auto' }}>
<Table
highlightOnHover
striped
stickyHeader
style={{ minWidth: '700px' }}
>
<TableThead>
<TableTr>
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh>
<TableTh style={{ width: '25%' }}>Jenis Informasi</TableTh>
<TableTh style={{ width: '40%' }}>Deskripsi</TableTh>
<TableTh style={{ width: '20%', textAlign: 'center' }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd style={{ textAlign: 'center' }}>
<Text fz="sm">{(page - 1) * 10 + index + 1}</Text>
</TableTd>
<TableTd>
<Box w={200}>
<Text fw={500} lineClamp={1}>{item.jenisInformasi}</Text>
</Box>
</TableTd>
<TableTd>
<Box w={200}>
<Text lineClamp={1} fz="sm" c="dimmed">
{item.deskripsi?.replace(/<[^>]*>?/gm, '').substring(0, 80) + '...'}
</Text>
</Box>
</TableTd>
<TableTd style={{ textAlign: 'center' }}>
<>
{/* Desktop Table */}
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table
highlightOnHover
striped
stickyHeader
style={{ minWidth: '700px' }}
>
<TableThead>
<TableTr>
<TableTh w="25%">
<Text fw={600} lh={1.4}>
Jenis Informasi
</Text>
</TableTh>
<TableTh w="40%">
<Text fw={600} lh={1.4}>
Deskripsi
</Text>
</TableTh>
<TableTh ta="center" w="20%">
<Text fw={600} lh={1.4}>
Aksi
</Text>
</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text fz="sm" fw={600} lh={1.5} lineClamp={1}>
{item.jenisInformasi}
</Text>
</TableTd>
<TableTd>
<Text fz="sm" lh={1.5} c="dimmed" lineClamp={1}>
{item.deskripsi?.replace(/<[^>]*>?/gm, '').substring(0, 80)}...
</Text>
</TableTd>
<TableTd ta="center">
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() => router.push(`/admin/ppid/daftar-informasi-publik/${item.id}`)}
onClick={() =>
router.push(`/admin/ppid/daftar-informasi-publik/${item.id}`)
}
>
Detail
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
{/* Mobile Card List */}
<Stack hiddenFrom="md" gap="sm">
{filteredData.map((item) => (
<Paper key={item.id} withBorder p="sm" radius="md">
<Stack gap={4}>
<Box>
<Text fw={600} lh={1.4}>
Jenis Informasi
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.jenisInformasi}
</Text>
</Box>
<Box>
<Text fw={600} lh={1.4}>
Deskripsi
</Text>
<Text fz="sm" fw={500} lh={1.5} c="dimmed">
{item.deskripsi?.replace(/<[^>]*>?/gm, '').substring(0, 80)}...
</Text>
</Box>
<Box>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() =>
router.push(`/admin/ppid/daftar-informasi-publik/${item.id}`)
}
>
Detail
</Button>
</Box>
</Stack>
</Paper>
))}
</Stack>
</>
)}
</Paper>
<Center mt="lg">
<Center mt={{ base: 'lg', md: 'xl' }}>
<Pagination
value={page}
onChange={(newPage) => {
@@ -129,14 +202,12 @@ function ListDaftarInformasi({ search }: { search: string }) {
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
</Box>
)
);
}
export default DaftarInformasiPublik;
export default DaftarInformasiPublik;

View File

@@ -8,11 +8,11 @@ import { useProxy } from 'valtio/utils';
import stateDasarHukumPPID from '../../_state/ppid/dasar_hukum/dasarHukum';
function Page() {
const router = useRouter()
const listDasarHukum = useProxy(stateDasarHukumPPID)
const router = useRouter();
const listDasarHukum = useProxy(stateDasarHukumPPID);
useShallowEffect(() => {
listDasarHukum.findById.load('1')
}, [])
listDasarHukum.findById.load('1');
}, []);
if (listDasarHukum.findById.loading) {
return (
@@ -40,15 +40,16 @@ function Page() {
<Title order={3} c={colors['blue-button']}>Preview Dasar Hukum PPID</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button
c="green"
variant="light"
leftSection={<IconEdit size={18} stroke={2} />}
radius="md"
onClick={() => router.push('/admin/ppid/dasar-hukum/edit')}
>
Edit
</Button>
<Button
w={{ base: '100%', md: "110%" }}
c="green"
variant="light"
leftSection={<IconEdit size={18} stroke={2} />}
radius="md"
onClick={() => router.push('/admin/ppid/dasar-hukum/edit')}
>
Edit
</Button>
</GridCol>
</Grid>
@@ -57,33 +58,39 @@ function Page() {
<Grid>
<GridCol span={12}>
<Center>
<Image loading='lazy' src="/darmasaba-icon.png" w={{ base: 100, md: 150 }} alt="Logo PPID" />
<Image loading="lazy" src="/darmasaba-icon.png" w={{ base: 100, md: 150 }} alt="Logo PPID" />
</Center>
</GridCol>
<GridCol span={12}>
<Text
ta="center"
fz={{ base: '1.5rem', md: '2rem' }}
fw="bold"
<Title
order={3}
ta="center"
lh={{ base: 1.15, md: 1.1 }}
fw="bold"
c={colors['blue-button']}
dangerouslySetInnerHTML={{ __html: listDasarHukum.findById.data.judul }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
dangerouslySetInnerHTML={{ __html: listDasarHukum.findById.data.judul }}
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
/>
</GridCol>
</Grid>
<Divider my="xl" color={colors['blue-button']} />
<Box
className="prose max-w-none"
<Text
dangerouslySetInnerHTML={{ __html: listDasarHukum.findById.data.content }}
style={{ wordBreak: "break-word", whiteSpace: "normal", fontSize: '1.1rem', lineHeight: 1.7, textAlign: 'justify' }}
style={{
wordBreak: 'break-word',
whiteSpace: 'normal',
fontSize: '1rem',
lineHeight: 1.55,
textAlign: 'justify',
}}
/>
</Box>
</Paper>
</Stack>
</Paper>
)
);
}
export default Page;
export default Page;

View File

@@ -1,7 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client';
import colors from '@/con/colors';
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { Box, ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title} from '@mantine/core';
import { IconChartBar, IconUsers } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
@@ -56,42 +56,77 @@ function LayoutTabsIKM({ children }: { children: React.ReactNode }) {
radius="lg"
keepMounted={false}
>
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}}
>
{tabs.map((e, i) => (
<Tooltip
key={i}
label={e.description}
withArrow
position="bottom"
transitionProps={{ transition: 'pop', duration: 200 }}
>
{/* ✅ Scroll horizontal wrapper */}
<Box visibleFrom='md' pb={10}>
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}}
>
{tabs.map((tab, i) => (
<TabsTab
value={e.value}
leftSection={e.icon}
key={i}
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 500,
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
}}
>
{e.label}
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
</ScrollArea>
))}
</TabsList>
</ScrollArea>
</Box>
<Box hiddenFrom='md' pb={10}>
<ScrollArea
type="auto"
offsetScrollbars={false}
w="100%"
>
<TabsList
p="xs" // lebih kecil
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
width: "max-content", // ⬅️ kunci
maxWidth: "100%", // ⬅️ penting
}}
>
{tabs.map((tab, i) => (
<TabsTab
key={i}
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
paddingInline: "0.75rem", // ⬅️ lebih ramping
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
}}
>
{tab.label}
</TabsTab>
))}
</TabsList>
</ScrollArea>
</Box>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value} mt="md">
{/* Konten dummy, bisa diganti tergantung routing */}

View File

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

View File

@@ -38,7 +38,7 @@ export default function DetailResponden() {
)
}
return (
<Box py={10}>
<Box px={{ base: 0, md: 'xs' }} py="xs">
<Button
variant="subtle"
onClick={() => router.back()}

View File

@@ -51,7 +51,7 @@ function RespondenCreate() {
}
}
return (
<Box>
<Box px={{ base: 0, md: 'lg' }} py="xs">
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack size={20} />
@@ -96,24 +96,24 @@ function RespondenCreate() {
}
disabled={indeksKepuasanState.jenisKelaminResponden.findMany.loading}
/>
<Select
<Select
key={"rating_responden"}
label={"Rating"}
placeholder={indeksKepuasanState.pilihanRatingResponden.findMany.loading ? 'Memuat...' : 'Pilih rating'}
value={stategrafikBerdasarkanResponden.create.form.ratingId || ""}
onChange={(val) => {
stategrafikBerdasarkanResponden.create.form.ratingId = val ?? "";
}}
data={
(indeksKepuasanState.pilihanRatingResponden.findMany.data || [])
.filter(Boolean) // Hapus null, undefined, dll
.map((item) => ({
value: item.id,
label: item.name || 'Tanpa Nama',
}))
}
disabled={indeksKepuasanState.pilihanRatingResponden.findMany.loading}
/>
label={"Rating"}
placeholder={indeksKepuasanState.pilihanRatingResponden.findMany.loading ? 'Memuat...' : 'Pilih rating'}
value={stategrafikBerdasarkanResponden.create.form.ratingId || ""}
onChange={(val) => {
stategrafikBerdasarkanResponden.create.form.ratingId = val ?? "";
}}
data={
(indeksKepuasanState.pilihanRatingResponden.findMany.data || [])
.filter(Boolean) // Hapus null, undefined, dll
.map((item) => ({
value: item.id,
label: item.name || 'Tanpa Nama',
}))
}
disabled={indeksKepuasanState.pilihanRatingResponden.findMany.loading}
/>
<Select
key={"kelompokUmur"}
label={"Kelompok Umur"}

View File

@@ -1,5 +1,21 @@
'use client';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import {
Box,
Button,
Center,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
@@ -9,11 +25,11 @@ import HeaderSearch from '../../../_com/header';
import indeksKepuasanState from '../../../_state/landing-page/indeks-kepuasan';
function Responden() {
const [search, setSearch] = useState("");
const [search, setSearch] = useState('');
return (
<Box>
<HeaderSearch
title="Data Responden"
title="Responden"
placeholder="Cari nama responden..."
searchIcon={<IconSearch size={18} />}
value={search}
@@ -33,17 +49,13 @@ function ListResponden({ search }: ListRespondenProps) {
const router = useRouter();
const { data, page, totalPages, loading, load } = state.findMany;
useShallowEffect(() => {
load(page, 10)
load(page, 10);
}, [page]);
const filteredData = (data || []).filter(item => {
const filteredData = (data || []).filter((item) => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword)
);
return item.name.toLowerCase().includes(keyword);
});
if (loading || !data) {
@@ -56,21 +68,25 @@ function ListResponden({ search }: ListRespondenProps) {
if (data.length === 0) {
return (
<Paper withBorder bg="white" p="lg" radius="md" shadow="sm">
<Paper withBorder bg="white" p={{ base: 'md', sm: 'lg' }} radius="md" shadow="sm">
<Stack gap="md">
<Title order={3}>Data Responden</Title>
<Table striped withRowBorders>
<TableThead>
<TableTr>
<TableTh style={{ textAlign: 'center' }}>No</TableTh>
<TableTh>Nama</TableTh>
<TableTh>Tanggal</TableTh>
<TableTh>Jenis Kelamin</TableTh>
<TableTh style={{ textAlign: 'center' }}>Aksi</TableTh>
</TableTr>
</TableThead>
</Table>
<Text c="dimmed" ta="center" py="md">
<Title order={2} lh={1.2}>
Data Responden
</Title>
<Box visibleFrom="md">
<Table striped withRowBorders>
<TableThead>
<TableTr>
<TableTh ta="center">No</TableTh>
<TableTh>Nama</TableTh>
<TableTh>Tanggal</TableTh>
<TableTh>Jenis Kelamin</TableTh>
<TableTh ta="center">Aksi</TableTh>
</TableTr>
</TableThead>
</Table>
</Box>
<Text c="dimmed" ta="center" py="md" fz={{ base: 'sm', md: 'md' }} lh={1.4}>
Belum ada data responden yang tersedia
</Text>
</Stack>
@@ -79,54 +95,133 @@ function ListResponden({ search }: ListRespondenProps) {
}
return (
<Paper withBorder bg="white" p="lg" radius="md" shadow="sm">
<Paper withBorder bg="white" p={{ base: 'md', sm: 'lg' }} radius="md" shadow="sm">
<Stack gap="md">
<Title order={3}>Data Responden</Title>
<Table striped withRowBorders>
<TableThead>
<TableTr>
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh>
<TableTh style={{ width: '25%' }}>Nama</TableTh>
<TableTh style={{ width: '25%' }}>Tanggal</TableTh>
<TableTh style={{ width: '20%' }}>Jenis Kelamin</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length === 0 ? (
<Title order={2} lh={1.2}>
Data Responden
</Title>
{/* Desktop Table */}
<Box visibleFrom="md">
<Table striped withRowBorders>
<TableThead>
<TableTr>
<TableTd colSpan={5}>
<Text c="dimmed" ta="center" py="md">
Tidak ada data yang cocok dengan pencarian
</Text>
</TableTd>
<TableTh w="5%" ta="center">
No
</TableTh>
<TableTh w="25%">Nama</TableTh>
<TableTh w="25%">Tanggal</TableTh>
<TableTh w="20%">Jenis Kelamin</TableTh>
<TableTh w="15%" ta="center">
Aksi
</TableTh>
</TableTr>
) : (
filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd style={{ width: '5%', textAlign: 'center' }}>{index + 1}</TableTd>
<TableTd style={{ width: '25%' }}>{item.name}</TableTd>
<TableTd style={{ width: '25%' }}>
{item.tanggal ? new Date(item.tanggal).toLocaleDateString('id-ID') : '-'}
</TableTd>
<TableTd style={{ width: '20%' }}>{item.jenisKelamin.name}</TableTd>
<TableTd style={{ width: '15%', textAlign: 'center' }}>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() => router.push(`/admin/ppid/ikm-desa-darmasaba/responden/${item.id}`)}
>
Detail
</Button>
</TableThead>
<TableTbody>
{filteredData.length === 0 ? (
<TableTr>
<TableTd colSpan={5}>
<Text c="dimmed" ta="center" py="md" fz="sm" lh={1.4}>
Tidak ada data yang cocok dengan pencarian
</Text>
</TableTd>
</TableTr>
))
)}
</TableTbody>
</Table>
) : (
filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd ta="center">{index + 1}</TableTd>
<TableTd>{item.name}</TableTd>
<TableTd>
{item.tanggal ? new Date(item.tanggal).toLocaleDateString('id-ID') : '-'}
</TableTd>
<TableTd>{item.jenisKelamin.name}</TableTd>
<TableTd ta="center">
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() =>
router.push(`/admin/ppid/ikm-desa-darmasaba/responden/${item.id}`)
}
>
Detail
</Button>
</TableTd>
</TableTr>
))
)}
</TableTbody>
</Table>
</Box>
{/* Mobile Card View */}
<Box hiddenFrom="md">
{filteredData.length === 0 ? (
<Text c="dimmed" ta="center" py="md" fz="sm" lh={1.4}>
Tidak ada data yang cocok dengan pencarian
</Text>
) : (
<Stack gap="sm">
{filteredData.map((item, index) => (
<Paper key={item.id} withBorder p="sm" radius="md">
<Stack gap={4}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
No
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{index + 1}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Nama
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.name}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Tanggal
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.tanggal
? new Date(item.tanggal).toLocaleDateString('id-ID')
: '-'}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Jenis Kelamin
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.jenisKelamin.name}
</Text>
</Box>
<Box ta="center">
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() =>
router.push(`/admin/ppid/ikm-desa-darmasaba/responden/${item.id}`)
}
>
Detail
</Button>
</Box>
</Stack>
</Paper>
))}
</Stack>
)}
</Box>
{filteredData.length > 0 && (
<Center>
<Pagination
@@ -138,7 +233,6 @@ function ListResponden({ search }: ListRespondenProps) {
}}
size="md"
radius="md"
mt="md"
/>
</Center>
)}
@@ -147,6 +241,4 @@ function ListResponden({ search }: ListRespondenProps) {
);
}
export default Responden;
export default Responden;

View File

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

View File

@@ -1,105 +1,277 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors'
import { Box, Button, Group, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'
import { useShallowEffect } from '@mantine/hooks'
import { IconDeviceImacCog, IconId, IconInfoCircle, IconPhone, IconUser } from '@tabler/icons-react'
import {
Box,
Button,
Center,
Grid,
GridCol,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
TextInput,
Title,
} from '@mantine/core'
import {
IconDeviceImacCog,
IconId,
IconInfoCircle,
IconPhone,
IconSearch,
IconUser,
} from '@tabler/icons-react'
import { useRouter } from 'next/navigation'
import { useEffect, useState } from 'react'
import { useProxy } from 'valtio/utils'
import statepermohonanInformasiPublikForm from '../../_state/ppid/permohonan_informasi_publik/permohonanInformasiPublik'
import { useDebouncedValue } from '@mantine/hooks'
function Page() {
const permohonanInformasiPublikState = useProxy(statepermohonanInformasiPublikForm)
const router = useRouter()
const { data, page, totalPages, loading, load } = permohonanInformasiPublikState.statepermohonanInformasiPublik.findMany
const [search, setSearch] = useState('')
const [debouncedSearch] = useDebouncedValue(search, 1000);
useShallowEffect(() => {
permohonanInformasiPublikState.statepermohonanInformasiPublik.findMany.load()
}, [])
useEffect(() => {
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
if (!permohonanInformasiPublikState.statepermohonanInformasiPublik.findMany.data) {
if (loading) {
return (
<Stack pos="relative" bg={colors.Bg} p="lg" align="center">
<Skeleton radius="md" h={40} w="60%" />
<Stack pos="relative" p="lg" align="center">
<Skeleton radius="md" h={200} w="100%" />
</Stack>
)
}
if (!data || data.length === 0) {
return (
<Box py={{ base: 'md', md: 'lg' }}>
<Paper bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} radius="xl" shadow="sm" withBorder>
<Stack gap={'sm'}>
<Grid mb={10}>
<GridCol span={{ base: 12, md: 9 }}>
<Title order={2} lh={1.2} c="dark">
Daftar Permohonan Informasi Publik
</Title>
</GridCol>
<GridCol span={{ base: 12, md: 3 }}>
<Paper radius="lg" bg={colors['white-1']}>
<TextInput
radius="lg"
placeholder={"Cari nama..."}
leftSection={<IconSearch size={16} />}
w="100%"
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
</Paper>
</GridCol>
</Grid>
<Stack align="center" py="xl" ta="center">
<IconInfoCircle size={40} stroke={1.5} color={colors['blue-button']} />
<Text fz={{ base: 'sm', md: 'md' }} fw={500} c="dimmed" lh={1.5}>
{search
? 'Tidak ditemukan data yang sesuai dengan pencarian'
: 'Belum ada permohonan yang tercatat'
}
</Text>
</Stack>
</Stack>
</Paper>
</Box>
)
}
const data = permohonanInformasiPublikState.statepermohonanInformasiPublik.findMany.data
return (
<Box py="md">
<Paper bg={colors['white-1']} p="lg" radius="xl" shadow="sm" withBorder>
<Stack gap="md">
<Group justify="space-between">
<Title order={2} c="dark">Daftar Permohonan Informasi Publik</Title>
<IconInfoCircle size={20} stroke={1.5} />
</Group>
<Box py={{ base: 'sm', md: 'md' }}>
<Paper bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} radius="xl" shadow="sm" withBorder>
<Stack gap={'sm'}>
<Grid mb={10}>
<GridCol span={{ base: 12, md: 9 }}>
<Title order={2} lh={1.2} c="dark">
Daftar Permohonan Informasi Publik
</Title>
</GridCol>
<GridCol span={{ base: 12, md: 3 }}>
<Paper radius="lg" bg={colors['white-1']}>
<TextInput
radius="lg"
placeholder={"Cari nama..."}
leftSection={<IconSearch size={16} />}
w="100%"
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
</Paper>
</GridCol>
</Grid>
{data.length === 0 ? (
<Stack align="center" py="xl">
<Stack align="center" py={{ base: 'xl', md: 'xl' }}>
<IconInfoCircle size={40} stroke={1.5} color={colors['blue-button']} />
<Text fw={500} c="dimmed">Belum ada permohonan informasi yang tercatat</Text>
<Text fz={{ base: 'sm', md: 'md' }} fw={500} c="dimmed" lh={1.5}>
Belum ada permohonan informasi yang tercatat
</Text>
</Stack>
) : (
<Box style={{ overflowX: 'auto' }}>
<Table
highlightOnHover
withRowBorders
withColumnBorders
withTableBorder
striped
stickyHeader
>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh><Group gap={5}><IconUser size={16} /> Nama</Group></TableTh>
<TableTh><Group gap={5}><IconId size={16} /> NIK</Group></TableTh>
<TableTh><Group gap={5}><IconPhone size={16} /> Telepon</Group></TableTh>
<TableTh><Group gap={5}><IconInfoCircle size={16} /> Detail</Group></TableTh>
</TableTr>
</TableThead>
<TableTbody>
{data.map((item, index) => (
<TableTr key={item.id}>
<TableTd>{index + 1}</TableTd>
<TableTd>
<Box w={200}>
<Text lineClamp={1} fw={500}>{item.name}</Text>
</Box>
</TableTd>
<TableTd>
<Box w={200}>
{item.nik}
</Box>
</TableTd>
<TableTd>
<Box w={200}>
{item.notelp}
</Box>
</TableTd>
<TableTd>
<>
{/* Desktop Table */}
<Box visibleFrom="md">
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh fz="sm" fw={600} ta="center" w={60}>
No
</TableTh>
<TableTh fz="sm" fw={600}>
<Group gap={5}>
<IconUser size={16} />
Nama
</Group>
</TableTh>
<TableTh fz="sm" fw={600}>
<Group gap={5}>
<IconId size={16} />
NIK
</Group>
</TableTh>
<TableTh fz="sm" fw={600}>
<Group gap={5}>
<IconPhone size={16} />
Telepon
</Group>
</TableTh>
<TableTh fz="sm" fw={600} w={140}>
<Group gap={5}>
<IconInfoCircle size={16} />
Detail
</Group>
</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{data.map((item, index) => (
<TableTr key={item.id}>
<TableTd ta="center" fz="sm" lh={1.5}>
{index + 1}
</TableTd>
<TableTd>
<Text fz="sm" fw={500} lh={1.5} lineClamp={1}>
{item.name}
</Text>
</TableTd>
<TableTd>
<Text fz="sm" lh={1.5}>{item.nik}</Text>
</TableTd>
<TableTd>
<Text fz="sm" lh={1.5}>{item.notelp}</Text>
</TableTd>
<TableTd>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() =>
router.push(`/admin/ppid/permohonan-informasi-publik/${item.id}`)
}
>
Detail
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
{/* Mobile Cards */}
<Stack hiddenFrom="md" gap="xs">
{data.map((item, index) => (
<Paper key={item.id} p="sm" radius="md" withBorder bg="white">
<Stack gap={4}>
<Box>
<Text fz="xs" fw={600} lh={1.4} c="dark">
No
</Text>
<Text fz="sm" fw={500} lh={1.5} c="dark">
{index + 1}
</Text>
</Box>
<Box>
<Text fz="xs" fw={600} lh={1.4} c="dark">
Nama
</Text>
<Text fz="sm" fw={500} lh={1.5} c="dark" lineClamp={1}>
{item.name}
</Text>
</Box>
<Box>
<Text fz="xs" fw={600} lh={1.4} c="dark">
NIK
</Text>
<Text fz="sm" lh={1.5} c="dark">{item.nik}</Text>
</Box>
<Box>
<Text fz="xs" fw={600} lh={1.4} c="dark">
Telepon
</Text>
<Text fz="sm" lh={1.5} c="dark">{item.notelp}</Text>
</Box>
<Box>
<Text fz="xs" fw={600} lh={1.4} c="dark">
Detail
</Text>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() => router.push(`/admin/ppid/permohonan-informasi-publik/${item.id}`)}
onClick={() =>
router.push(`/admin/ppid/permohonan-informasi-publik/${item.id}`)
}
mt={2}
>
Detail
Lihat Detail
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</Box>
</Stack>
</Paper>
))}
</Stack>
</>
)}
</Stack>
<Center mt="xl">
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={totalPages}
withEdges
withControls
radius="md"
/>
</Center>
</Paper>
</Box>
)
}
export default Page
export default Page

View File

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

View File

@@ -1,97 +1,285 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors'
import { Box, Button, Group, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'
import { useShallowEffect } from '@mantine/hooks'
import { IconDeviceImacCog, IconInfoCircle, IconMail, IconPhone, IconUser } from '@tabler/icons-react'
import {
Box,
Button,
Center,
Grid,
GridCol,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
TextInput,
Title,
} from '@mantine/core'
import {
IconDeviceImacCog,
IconInfoCircle,
IconMail,
IconPhone,
IconSearch,
IconUser,
} from '@tabler/icons-react'
import { useRouter } from 'next/navigation'
import { useProxy } from 'valtio/utils'
import statePermohonanKeberatan from '../../_state/ppid/permohonan_keberatan_informasi_publik/permohonanKeberatanInformasi'
import { useEffect, useState } from 'react'
import { useDebouncedValue } from '@mantine/hooks'
function Page() {
const listState = useProxy(statePermohonanKeberatan)
const router = useRouter()
useShallowEffect(() => {
listState.findMany.load()
}, [])
const listState = useProxy(statePermohonanKeberatan)
const [search, setSearch] = useState("");
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { data, page, totalPages, loading, load } = listState.findMany
if (!listState.findMany.data) {
useEffect(() => {
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
if (loading) {
return (
<Stack pos="relative" bg={colors.Bg} p="lg" align="center">
<Skeleton radius="md" h={40} w="60%" />
<Stack pos="relative" p="lg" align="center">
<Skeleton radius="md" h={200} w="100%" />
</Stack>
)
}
const data = listState.findMany.data
if (!data || data.length === 0) {
return (
<Box py={{ base: 'md', md: 'lg' }}>
<Paper bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} radius="xl" shadow="sm" withBorder>
<Stack gap={'sm'}>
<Grid mb={10}>
<GridCol span={{ base: 12, md: 9 }}>
<Title order={2} lh={1.2} c="dark">
Daftar Permohonan Keberatan Informasi Publik
</Title>
</GridCol>
<GridCol span={{ base: 12, md: 3 }}>
<Paper radius="lg" bg={colors['white-1']}>
<TextInput
radius="lg"
placeholder={"Cari nama..."}
leftSection={<IconSearch size={16} />}
w="100%"
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
</Paper>
</GridCol>
</Grid>
<Stack align="center" py="xl" ta="center">
<IconInfoCircle size={40} stroke={1.5} color={colors['blue-button']} />
<Text fz={{ base: 'sm', md: 'md' }} fw={500} c="dimmed" lh={1.5}>
{search
? 'Tidak ditemukan data yang sesuai dengan pencarian'
: 'Belum ada permohonan keberatan yang tercatat'
}
</Text>
</Stack>
</Stack>
</Paper>
</Box>
)
}
return (
<Box py="md">
<Paper bg={colors['white-1']} p="lg" radius="xl" shadow="sm" withBorder>
<Stack gap="md">
<Group justify="space-between">
<Title order={2} c="dark">Daftar Permohonan Keberatan Informasi Publik</Title>
<IconInfoCircle size={20} stroke={1.5} />
</Group>
<Box py={{ base: 'md', md: 'lg' }}>
<Paper bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} radius="xl" shadow="sm" withBorder>
<Stack gap={'sm'}>
<Grid mb={10}>
<GridCol span={{ base: 12, md: 9 }}>
<Title order={2} lh={1.2} c="dark">
Daftar Permohonan Keberatan Informasi Publik
</Title>
</GridCol>
<GridCol span={{ base: 12, md: 3 }}>
<Paper radius="lg" bg={colors['white-1']}>
<TextInput
radius="lg"
placeholder={"Cari nama..."}
leftSection={<IconSearch size={16} />}
w="100%"
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
</Paper>
</GridCol>
</Grid>
{data.length === 0 ? (
<Stack align="center" py="xl">
<Stack align="center" py="xl" ta="center">
<IconInfoCircle size={40} stroke={1.5} color={colors['blue-button']} />
<Text fw={500} c="dimmed">Belum ada permohonan keberatan yang tercatat</Text>
<Text fz={{ base: 'sm', md: 'md' }} fw={500} c="dimmed" lh={1.5}>
Belum ada permohonan keberatan yang tercatat
</Text>
</Stack>
) : (
<Box style={{ overflowX: 'auto' }}>
<Table
highlightOnHover
withRowBorders
withColumnBorders
withTableBorder
striped
stickyHeader
>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh><Group gap={5}><IconUser size={16} /> Nama</Group></TableTh>
<TableTh><Group gap={5}><IconMail size={16} /> Email</Group></TableTh>
<TableTh><Group gap={5}><IconPhone size={16} /> Telepon</Group></TableTh>
<TableTh><Group gap={5}><IconInfoCircle size={16} /> Detail</Group></TableTh>
</TableTr>
</TableThead>
<TableTbody>
{data.map((item, index) => (
<TableTr key={item.id}>
<TableTd>{index + 1}</TableTd>
<TableTd>
<Text lineClamp={1} fw={500}>{item.name}</Text>
</TableTd>
<TableTd>
<Text size="sm">{item.email || '-'}</Text>
</TableTd>
<TableTd>
<Text>{item.notelp || '-'}</Text>
</TableTd>
<TableTd>
<>
{/* Desktop Table */}
<Box visibleFrom="md">
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh fz="sm" fw={600} lh={1.4} ta="center">
No
</TableTh>
<TableTh fz="sm" fw={600} lh={1.4}>
<Group gap={5}>
<IconUser size={16} />
Nama
</Group>
</TableTh>
<TableTh fz="sm" fw={600} lh={1.4}>
<Group gap={5}>
<IconMail size={16} />
Email
</Group>
</TableTh>
<TableTh fz="sm" fw={600} lh={1.4}>
<Group gap={5}>
<IconPhone size={16} />
Telepon
</Group>
</TableTh>
<TableTh fz="sm" fw={600} lh={1.4} ta="center">
<Group gap={5}>
<IconInfoCircle size={16} />
Detail
</Group>
</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{data.map((item, index) => (
<TableTr key={item.id}>
<TableTd ta="center" fz="sm" lh={1.5}>
{index + 1}
</TableTd>
<TableTd>
<Text fz="sm" fw={500} lh={1.5} lineClamp={1}>
{item.name}
</Text>
</TableTd>
<TableTd>
<Text fz="sm" lh={1.5}>
{item.email || '-'}
</Text>
</TableTd>
<TableTd>
<Text fz="sm" lh={1.5}>
{item.notelp || '-'}
</Text>
</TableTd>
<TableTd>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() =>
router.push(
`/admin/ppid/permohonan-keberatan-informasi-publik/${item.id}`
)
}
>
Detail
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
{/* Mobile Cards */}
<Stack hiddenFrom="md" gap="xs">
{data.map((item, index) => (
<Paper key={item.id} p="sm" radius="md" withBorder bg="white">
<Stack gap={4}>
<Box>
<Text fz="xs" fw={600} lh={1.4} c="dimmed">
No
</Text>
<Text fz="sm" fw={600} lh={1.5}>
{index + 1}
</Text>
</Box>
<Box>
<Text fz="xs" fw={600} lh={1.4} c="dimmed">
Nama
</Text>
<Text fz="sm" fw={600} lh={1.5}>
{item.name}
</Text>
</Box>
<Box>
<Text fz="xs" fw={600} lh={1.4} c="dimmed">
Email
</Text>
<Text fz="sm" lh={1.5}>
{item.email || '-'}
</Text>
</Box>
<Box>
<Text fz="xs" fw={600} lh={1.4} c="dimmed">
Telepon
</Text>
<Text fz="sm" lh={1.5}>
{item.notelp || '-'}
</Text>
</Box>
<Box>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() => router.push(`/admin/ppid/permohonan-keberatan-informasi-publik/${item.id}`)}
onClick={() =>
router.push(
`/admin/ppid/permohonan-keberatan-informasi-publik/${item.id}`
)
}
mt={4}
>
Detail
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</Box>
</Stack>
</Paper>
))}
</Stack>
</>
)}
</Stack>
<Center mt="xl">
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={totalPages}
withEdges
withControls
radius="md"
/>
</Center>
</Paper>
</Box>
);
)
}
export default Page;
export default Page

View File

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

View File

@@ -7,13 +7,13 @@ import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import stateProfilePPID from '../../_state/ppid/profile_ppid/profile_PPID';
function Page() {
const router = useRouter()
const allList = useProxy(stateProfilePPID)
const router = useRouter();
const allList = useProxy(stateProfilePPID);
useShallowEffect(() => {
allList.profile.load("edit") // Assuming "1" is your default ID, adjust as needed
}, [])
allList.profile.load("edit");
}, []);
if (!allList.profile.data) {
return (
@@ -32,19 +32,19 @@ function Page() {
<Stack gap="md">
<Grid>
<GridCol span={{ base: 12, md: 11 }}>
<Title order={3} c={colors['blue-button']}>Preview Profil PPID</Title>
<Title order={3} c={colors['blue-button']} lh={1.2}>Preview Profil PPID</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button
w={{base: '100%', md: "110%"}}
c="green"
variant="light"
leftSection={<IconEdit size={18} stroke={2} />}
radius="md"
onClick={() => router.push(`/admin/ppid/profil-ppid/${allList.profile.data?.id}`)}
>
Edit
</Button>
<Button
w={{ base: '100%', md: "110%" }}
c="green"
variant="light"
leftSection={<IconEdit size={18} stroke={2} />}
radius="md"
onClick={() => router.push(`/admin/ppid/profil-ppid/${allList.profile.data?.id}`)}
>
Edit
</Button>
</GridCol>
</Grid>
{dataArray.map((item) => (
@@ -57,9 +57,14 @@ function Page() {
</Center>
</GridCol>
<GridCol span={12}>
<Text ta="center" fz={{ base: "1.2rem", md: "1.8rem" }} fw="bold" c={colors['blue-button']}>
<Title
order={2}
c={colors['blue-button']}
ta="center"
lh={1.15}
>
PROFIL PIMPINAN BADAN PUBLIK DESA DARMASABA
</Text>
</Title>
</GridCol>
</Grid>
</Box>
@@ -87,34 +92,77 @@ function Page() {
className="glass3"
style={{ mt: -30, boxShadow: '0 4px 20px rgba(0,0,0,0.15)' }}
>
<Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "1.2rem", md: "1.6rem" }}>
<Title
order={3}
c={colors['white-1']}
ta="center"
lh={1.2}
>
{item.name}
</Text>
</Title>
</Paper>
</Stack>
</Paper>
<Box mt="lg">
<Text fz={{ base: "1.125rem", md: "1.5rem" }} fw="bold" mb={4}>Biodata</Text>
<Text fz={{ base: "1rem", md: "1.4rem" }} ta="justify" c={colors['blue-button']} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: item.biodata }} />
<Title order={3} lh={1.2} mb={4}>
Biodata
</Title>
<Text
fz={{ base: 'sm', md: 'md' }}
ta="justify"
c={colors['blue-button']}
lh={1.5}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
dangerouslySetInnerHTML={{ __html: item.biodata }}
/>
</Box>
<Box mt="xl">
<Text fz={{ base: "1.125rem", md: "1.5rem" }} fw="bold" mb={4}>Riwayat Karir</Text>
<Text fz={{ base: "1rem", md: "1.4rem" }} ta="justify" c={colors['blue-button']} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: item.riwayat }} />
</Box>
<Box mt="xl">
<Text fz={{ base: "1.125rem", md: "1.5rem" }} fw="bold" mb={4}>Pengalaman Organisasi</Text>
<Title order={3} lh={1.2} mb={4}>
Riwayat Karir
</Title>
<Box px={20}>
<Text fz={{ base: "1rem", md: "1.4rem" }} ta="justify" c={colors['blue-button']} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: item.pengalaman }} />
<Text
fz={{ base: 'sm', md: 'md' }}
ta="justify"
c={colors['blue-button']}
lh={1.5}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
dangerouslySetInnerHTML={{ __html: item.riwayat }}
/>
</Box>
</Box>
<Box mt="xl" mb="lg">
<Text fz={{ base: "1.125rem", md: "1.5rem" }} fw="bold" mb={4}>Program Kerja Unggulan</Text>
<Box mt="xl">
<Title order={3} lh={1.2} mb={4}>
Pengalaman Organisasi
</Title>
<Box px={20}>
<Text fz={{ base: "1rem", md: "1.4rem" }} ta="justify" c={colors['blue-button']} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: item.unggulan }} />
<Text
fz={{ base: 'sm', md: 'md' }}
ta="justify"
c={colors['blue-button']}
lh={1.5}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
dangerouslySetInnerHTML={{ __html: item.pengalaman }}
/>
</Box>
</Box>
<Box mt="xl" mb="lg">
<Title order={3} lh={1.2} mb={4}>
Program Kerja Unggulan
</Title>
<Box px={20}>
<Text
fz={{ base: 'sm', md: 'md' }}
ta="justify"
c={colors['blue-button']}
lh={1.5}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
dangerouslySetInnerHTML={{ __html: item.unggulan }}
/>
</Box>
</Box>
</Box>
@@ -122,9 +170,7 @@ function Page() {
))}
</Stack>
</Paper>
)
);
}
export default Page;

View File

@@ -1,7 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { Box, ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { IconBuildingCommunity, IconHierarchy2, IconUsers } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
@@ -63,51 +63,92 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
keepMounted={false}
>
{/* ✅ Scroll horizontal wrapper */}
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
<Box visibleFrom='md' pb={10}>
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}}
>
{tabs.map((tab, i) => (
<TabsTab
key={i}
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
}}
>
{tab.label}
</TabsTab>
))}
</TabsList>
</ScrollArea>
</Box>
<Box hiddenFrom='md' pb={10}>
<ScrollArea
type="auto"
offsetScrollbars={false}
w="100%"
>
<TabsList
p="xs" // lebih kecil
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
width: "max-content", // ⬅️ kunci
maxWidth: "100%", // ⬅️ penting
}}
>
{tabs.map((tab, i) => (
<TabsTab
key={i}
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
paddingInline: "0.75rem", // ⬅️ lebih ramping
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
}}
>
{tab.label}
</TabsTab>
))}
</TabsList>
</ScrollArea>
</Box>
{tabs.map((tab, i) => (
<TabsPanel
key={i}
value={tab.value}
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
padding: "1.5rem",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
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
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
}}
>
{tabs.map((tab, i) => (
<TabsTab
key={i}
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
))}
</TabsList>
</ScrollArea>
{tabs.map((tab, i) => (
<TabsPanel
key={i}
value={tab.value}
style={{
padding: "1.5rem",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
borderRadius: "1rem",
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
}}
>
{children}
</TabsPanel>
))}
</Tabs>
{children}
</TabsPanel>
))}
</Tabs>
</Stack >
);
}

View File

@@ -153,7 +153,7 @@ export default function EditPegawaiPPID() {
};
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

@@ -51,7 +51,7 @@ function DetailPegawai() {
const data = statePegawai.findUnique.data;
return (
<Box>
<Box px={{ base: 0, md: 'xs' }} py="xs">
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
@@ -59,7 +59,7 @@ function DetailPegawai() {
</Box>
<Paper
withBorder
w={{ base: "100%", md: "60%" }}
w={{ base: "100%", md: "70%" }}
bg={colors['white-1']}
p="lg"
radius="md"

View File

@@ -78,7 +78,7 @@ function CreatePegawaiPPID() {
};
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,13 +1,32 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Badge, Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, ThemeIcon, Title } from '@mantine/core';
import { IconCheck, IconDeviceImacCog, IconPlus, IconSearch, IconX } from '@tabler/icons-react';
import {
Badge,
Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title
} from '@mantine/core';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import stateStrukturPPID from '../../../_state/ppid/struktur_ppid/struktur_PPID';
import { useDebouncedValue } from '@mantine/hooks';
function PegawaiPPID() {
const [search, setSearch] = useState("");
@@ -28,6 +47,7 @@ function PegawaiPPID() {
function ListPegawaiPPID({ search }: { search: string }) {
const stateOrganisasi = useProxy(stateStrukturPPID.pegawai);
const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const {
data,
@@ -38,47 +58,28 @@ function ListPegawaiPPID({ search }: { search: string }) {
} = stateOrganisasi.findMany;
useEffect(() => {
load(page, 10, search);
}, [page, search]);
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
const filteredData = data || []
const filteredData = data || [];
// Handle loading state
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={300} />
<Stack py="xl">
<Skeleton height={300} radius="md" />
</Stack>
);
}
if (data.length === 0) {
return (
<Box py={10}>
<Box py="xl">
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Pegawai PPID</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ppid/struktur-ppid/pegawai/create')}
>
Tambah Baru
</Button>
</Group>
<Center py="xl">
<Text c="dimmed">Tidak ada data pegawai yang ditemukan</Text>
</Center>
</Paper>
</Box>
);
}
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 Pegawai PPID</Title>
<Title order={2} lh={1.2}>
Daftar Pegawai PPID
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
@@ -87,53 +88,70 @@ function ListPegawaiPPID({ search }: { search: string }) {
>
Tambah Baru
</Button>
</Group>
<Center py="xl">
<Text c="dimmed" fz={{ base: 'sm', md: 'md' }} lh={1.5}>
Tidak ada data pegawai yang ditemukan
</Text>
</Center>
</Paper>
</Box>
);
}
return (
<Box py="xl">
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={2} lh={1.2}>
Daftar Pegawai PPID
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ppid/struktur-ppid/pegawai/create')}
>
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 Lengkap</TableTh>
<TableTh style={{ width: '20%' }}>Posisi</TableTh>
<TableTh style={{ width: '10%' }}>Status</TableTh>
<TableTh style={{ width: '10%' }}>Aksi</TableTh>
<TableTh fz="sm" fw={600} lh={1.4}>
Nama Lengkap
</TableTh>
<TableTh fz="sm" fw={600} lh={1.4}>
Posisi
</TableTh>
<TableTh fz="sm" fw={600} lh={1.4}>
Status
</TableTh>
<TableTh fz="sm" fw={600} lh={1.4}>
Aksi
</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={150}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.namaLengkap}
</Text>
</Box>
<Text fz="md" fw={500} lh={1.5} truncate="end">
{item.namaLengkap}
</Text>
</TableTd>
<TableTd>
<Box w={150}>
<Badge variant="light" color="blue">
{item.posisi?.nama || 'Belum diatur'}
</Badge>
</Box>
<Badge variant="light" color="blue" fz="sm" lh={1.4}>
{item.posisi?.nama || 'Belum diatur'}
</Badge>
</TableTd>
<TableTd>
<Group gap="xs" wrap="nowrap">
<Box visibleFrom="sm">
<Badge color={item.isActive ? "green" : "red"}>
{item.isActive ? "Aktif" : "Tidak Aktif"}
</Badge>
</Box>
<Box hiddenFrom="sm">
{item.isActive ? (
<ThemeIcon color="green" variant="light" size="sm">
<IconCheck size={16} />
</ThemeIcon>
) : (
<ThemeIcon color="red" variant="light" size="sm">
<IconX size={16} />
</ThemeIcon>
)}
</Box>
</Group>
<Badge color={item.isActive ? "green" : "red"} fz="sm" lh={1.4}>
{item.isActive ? "Aktif" : "Tidak Aktif"}
</Badge>
</TableTd>
<TableTd>
<Button
@@ -143,6 +161,7 @@ function ListPegawaiPPID({ search }: { search: string }) {
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() => router.push(`/admin/ppid/struktur-ppid/pegawai/${item.id}`)}
fz="sm"
>
Detail
</Button>
@@ -152,7 +171,47 @@ function ListPegawaiPPID({ search }: { search: string }) {
</TableTbody>
</Table>
</Box>
<Center mt="lg">
{/* Mobile: Card List */}
<Stack hiddenFrom="md" gap="sm" mt="md">
{filteredData.map((item) => (
<Paper key={item.id} withBorder p="md" radius="md">
<Stack gap={"xs"}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Nama Lengkap</Text>
<Text fz="md" fw={500} lh={1.4}>
{item.namaLengkap}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Posisi</Text>
<Badge variant="light" color="blue" fz="xs" lh={1.4}>
{item.posisi?.nama || 'Belum diatur'}
</Badge>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Status</Text>
<Badge color={item.isActive ? "green" : "red"} fz="xs" lh={1.4}>
{item.isActive ? "Aktif" : "Tidak Aktif"}
</Badge>
</Box>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() => router.push(`/admin/ppid/struktur-ppid/pegawai/${item.id}`)}
fz="xs"
>
Detail
</Button>
</Stack>
</Paper>
))}
</Stack>
<Center mt="xl">
<Pagination
value={page}
onChange={(newPage) => {
@@ -170,4 +229,4 @@ function ListPegawaiPPID({ search }: { search: string }) {
);
}
export default PegawaiPPID;
export default PegawaiPPID;

View File

@@ -107,7 +107,7 @@ function EditPosisiOrganisasiPPID() {
};
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

@@ -46,7 +46,7 @@ function CreatePosisiOrganisasiPPID() {
};
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 { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
@@ -31,6 +31,7 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
const router = useRouter();
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const [debouncedSearch] = useDebouncedValue(search, 1000);
const {
data,
@@ -41,8 +42,8 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
} = stateOrganisasi.findMany;
useShallowEffect(() => {
load(page, 10, search);
}, [page, search]);
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
const handleHapus = async () => {
if (selectedId) {
@@ -56,63 +57,63 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
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 Posisi Organisasi PPID</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ppid/struktur-ppid/posisi-organisasi/create')}
>
Tambah Baru
</Button>
<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}>Daftar Posisi Organisasi PPID</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ppid/struktur-ppid/posisi-organisasi/create')}
>
Tambah Baru
</Button>
</Group>
<Box style={{ overflowX: 'auto' }}>
{/* Desktop Table */}
<Box visibleFrom="md">
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '20%' }}>Nama Posisi</TableTh>
<TableTh style={{ width: '20%' }}>Deskripsi</TableTh>
<TableTh style={{ width: '20%' }}>Hierarki</TableTh>
<TableTh style={{ width: '20%' }}>Edit</TableTh>
<TableTh style={{ width: '20%' }}>Hapus</TableTh>
<TableTh fz="sm" fw={600} lh={1.4}>Nama Posisi</TableTh>
<TableTh fz="sm" fw={600} lh={1.4}>Deskripsi</TableTh>
<TableTh fz="sm" fw={600} lh={1.4}>Hierarki</TableTh>
<TableTh fz="sm" fw={600} lh={1.4}>Edit</TableTh>
<TableTh fz="sm" fw={600} lh={1.4}>Hapus</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd style={{ width: '20%' }}>
<Text fw={500} truncate="end" lineClamp={1}>{item.nama}</Text>
<TableTd>
<Text fz="md" fw={600} lh={1.5} truncate="end" lineClamp={1}>{item.nama}</Text>
</TableTd>
<TableTd style={{ width: '20%' }}>
<Box w={200}>
<Text lineClamp={1} fz="sm" c="dimmed" dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }} />
</Box>
<TableTd w={200}>
<Text fz="sm" lh={1.5} c="dimmed" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }} />
</TableTd>
<TableTd style={{ width: '20%' }}>
<Text>{item.hierarki || '-'}</Text>
<TableTd>
<Text fz="md" lh={1.5}>{item.hierarki || '-'}</Text>
</TableTd>
<TableTd style={{ width: '20%' }}>
<Button
variant="light"
color="green"
size="sm"
onClick={() => router.push(`/admin/ppid/struktur-ppid/posisi-organisasi/${item.id}`)}
>
<IconEdit size={18} />
</Button>
<TableTd>
<Button
variant="light"
color="green"
size="sm"
onClick={() => router.push(`/admin/ppid/struktur-ppid/posisi-organisasi/${item.id}`)}
>
<IconEdit size={18} />
</Button>
</TableTd>
<TableTd style={{ width: '20%' }}>
<TableTd>
<Button
variant="light"
color="red"
@@ -129,9 +130,11 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">Tidak ada data posisi organisasi yang cocok</Text>
<TableTd colSpan={5}>
<Center py={{ base: 'sm', md: 'md' }}>
<Text fz="sm" c="dimmed" ta="center" lh={1.5}>
Tidak ada data posisi organisasi yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
@@ -139,7 +142,59 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
</TableTbody>
</Table>
</Box>
{/* Mobile Card View */}
<Stack gap="xs" hiddenFrom="md">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} withBorder p="sm" radius="sm">
<Stack gap={4}>
<Box>
<Text fz="xs" fw={600} lh={1.4}>Nama Posisi</Text>
<Text fz="sm" fw={600} lh={1.5}>{item.nama}</Text>
</Box>
<Box>
<Text fz="xs" fw={600} lh={1.4}>Deskripsi</Text>
<Text fz="sm" lh={1.5} dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }} />
</Box>
<Box>
<Text fz="xs" fw={600} lh={1.4}>Hierarki</Text>
<Text fz="sm" lh={1.5}>{item.hierarki || '-'}</Text>
</Box>
<Group justify="flex-end" gap="xs">
<Button
variant="light"
color="green"
size="xs"
onClick={() => router.push(`/admin/ppid/struktur-ppid/posisi-organisasi/${item.id}`)}
>
<IconEdit size={16} />
</Button>
<Button
variant="light"
color="red"
size="xs"
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={16} />
</Button>
</Group>
</Stack>
</Paper>
))
) : (
<Center py="sm">
<Text fz="sm" c="dimmed" ta="center" lh={1.5}>
Tidak ada data posisi organisasi yang cocok
</Text>
</Center>
)}
</Stack>
</Paper>
<Center>
<Pagination
value={page}
@@ -154,6 +209,7 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
radius="md"
/>
</Center>
{/* Modal Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
@@ -165,4 +221,4 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
);
}
export default PosisiOrganisasiPPID;
export default PosisiOrganisasiPPID;

View File

@@ -71,7 +71,7 @@ function VisiMisiPPIDEdit() {
};
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

@@ -7,9 +7,8 @@ import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import stateVisiMisiPPID from '../../_state/ppid/visi_misi_ppid/visimisiPPID';
function VisiMisiPPIDList() {
const router = useRouter()
const router = useRouter();
const listVisiMisi = useProxy(stateVisiMisiPPID);
useShallowEffect(() => {
listVisiMisi.findById.load('1');
@@ -41,15 +40,16 @@ function VisiMisiPPIDList() {
<Title order={3} c={colors['blue-button']}>Preview Visi Misi PPID</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button
c="green"
variant="light"
leftSection={<IconEdit size={18} stroke={2} />}
radius="md"
onClick={() => router.push('/admin/ppid/visi-misi-ppid/edit')}
>
Edit
</Button>
<Button
w={{ base: '100%', md: "110%" }}
c="green"
variant="light"
leftSection={<IconEdit size={18} stroke={2} />}
radius="md"
onClick={() => router.push('/admin/ppid/visi-misi-ppid/edit')}
>
Edit
</Button>
</GridCol>
</Grid>
@@ -58,14 +58,25 @@ function VisiMisiPPIDList() {
<Grid>
<GridCol span={12}>
<Center>
<Image loading='lazy' src="/darmasaba-icon.png" w={{ base: 100, md: 150 }} alt="Logo PPID" />
<Image loading="lazy" src="/darmasaba-icon.png" w={{ base: 100, md: 150 }} alt="Logo PPID" />
</Center>
</GridCol>
<GridCol span={12}>
<Text ta="center" fz={{ base: '1.2rem', md: '1.8rem' }} fw="bold" c={colors['blue-button']}>
<Title
order={2}
ta="center"
c={colors['blue-button']}
style={{ lineHeight: 1.15 }}
>
MOTO PPID DESA DARMASABA
</Text>
<Text ta="center" fz={{ base: '1rem', md: '1.2rem' }} mt="sm">
</Title>
<Text
ta="center"
fz={{ base: 'sm', md: 'md' }}
lh={{ base: 1.5, md: 1.5 }}
mt="sm"
c="black"
>
MEMBERIKAN INFORMASI YANG CEPAT, MUDAH, TEPAT DAN TRANSPARAN
</Text>
</GridCol>
@@ -74,26 +85,50 @@ function VisiMisiPPIDList() {
<Divider my="xl" color={colors['blue-button']} />
<Box>
<Text fz={{ base: '1.5rem', md: '1.75rem' }} fw="bold" ta="center" mb="lg" c={colors['blue-button']}>
<Title
order={2}
ta="center"
mb="lg"
c={colors['blue-button']}
style={{ lineHeight: 1.15 }}
>
VISI PPID
</Text>
<Box
className="prose max-w-none"
</Title>
<Text
ta={{ base: "center", md: "justify" }}
dangerouslySetInnerHTML={{ __html: listVisiMisi.findById.data.visi }}
style={{wordBreak: "break-word", whiteSpace: "normal", textAlign: "justify"}}
style={{
wordBreak: 'break-word',
whiteSpace: 'normal',
fontSize: '1rem',
lineHeight: 1.55,
color: 'black',
}}
/>
</Box>
<Divider my="xl" color={colors['blue-button']} />
<Box mt="xl">
<Text fz={{ base: '1.5rem', md: '1.75rem' }} fw="bold" ta="center" mb="lg" c={colors['blue-button']}>
<Title
order={2}
ta="center"
mb="lg"
c={colors['blue-button']}
style={{ lineHeight: 1.15 }}
>
MISI PPID
</Text>
<Box
className="prose max-w-none"
</Title>
<Text
ta={"justify"}
dangerouslySetInnerHTML={{ __html: listVisiMisi.findById.data.misi }}
style={{wordBreak: "break-word", whiteSpace: "normal", textAlign: "justify"}}
style={{
wordBreak: 'break-word',
whiteSpace: 'normal',
fontSize: '1rem',
lineHeight: 1.55,
color: 'black',
}}
/>
</Box>
</Box>
@@ -103,4 +138,4 @@ function VisiMisiPPIDList() {
);
}
export default VisiMisiPPIDList;
export default VisiMisiPPIDList;

View File

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

View File

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

View File

@@ -1,14 +1,56 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function permohonanInformasiPublikFindMany() {
const res = await prisma.permohonanInformasiPublik.findMany({
include: {
jenisInformasiDiminta: true,
caraMemperolehInformasi: true,
caraMemperolehSalinanInformasi: true,
}
});
return {
data: res,
};
export default async function permohonanInformasiPublikFindMany(
context: Context
) {
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || "";
const skip = (page - 1) * limit;
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ name: { contains: search, mode: "insensitive" } },
{ email: { contains: search, mode: "insensitive" } },
];
}
try {
const [data, total] = await Promise.all([
prisma.permohonanInformasiPublik.findMany({
where,
skip,
include: {
jenisInformasiDiminta: true,
caraMemperolehInformasi: true,
caraMemperolehSalinanInformasi: true,
},
take: limit,
orderBy: { name: "asc" }, // opsional, kalau mau urut berdasarkan waktu
}),
prisma.permohonanInformasiPublik.count({
where: { isActive: true },
}),
]);
return {
success: true,
message: "Success fetch formulir permohonan keberatan with pagination",
data,
page,
totalPages: Math.ceil(total / limit),
total,
};
} catch (e) {
console.error("Find many paginated error:", e);
return {
success: false,
message: "Failed fetch formulir permohonan keberatan with pagination",
};
}
}

View File

@@ -1,8 +1,49 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function permohonanKeberatanInformasiPublikFindMany() {
const res = await prisma.formulirPermohonanKeberatan.findMany();
return {
data: res,
};
export default async function permohonanKeberatanInformasiPublikFindMany(context: Context) {
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || '';
const skip = (page - 1) * limit;
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ name: { contains: search, mode: 'insensitive' } },
{email: { contains: search, mode: 'insensitive' } },
];
}
try {
const [data, total] = await Promise.all([
prisma.formulirPermohonanKeberatan.findMany({
where,
skip,
take: limit,
orderBy: { name: "asc" }, // opsional, kalau mau urut berdasarkan waktu
}),
prisma.formulirPermohonanKeberatan.count({
where: { isActive: true },
}),
]);
return {
success: true,
message: "Success fetch formulir permohonan keberatan with pagination",
data,
page,
totalPages: Math.ceil(total / limit),
total,
};
} catch (e) {
console.error("Find many paginated error:", e);
return {
success: false,
message: "Failed fetch formulir permohonan keberatan with pagination",
};
}
}

View File

@@ -1,5 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
'use client';
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
import {
Badge,
@@ -51,10 +51,14 @@ export default function Content({ kategori }: { kategori: string }) {
<Container size="xl" px={{ base: 'md', md: 'xl' }}>
{/* === Berita Utama === */}
{featuredState.loading ? (
<Center><Skeleton h={400} /></Center>
<Center>
<Skeleton h={400} />
</Center>
) : featured ? (
<Box mb={50}>
<Text fz="h2" fw={700} mb="md">Berita Utama</Text>
<Title order={2} mb="md">
Berita Utama
</Title>
<Paper shadow="md" radius="md" withBorder>
<Grid gutter={0}>
<GridCol span={{ base: 12, md: 6 }}>
@@ -74,13 +78,29 @@ export default function Content({ kategori }: { kategori: string }) {
<Badge color="blue" variant="light" mb="md">
{featured.kategoriBerita?.name || kategori}
</Badge>
<Title order={2} mb="md">{featured.judul}</Title>
<Text c="dimmed" lineClamp={3} mb="md" dangerouslySetInnerHTML={{ __html: featured.deskripsi }} />
<Title order={3} mb="md">
{featured.judul}
</Title>
<Text
c="dimmed"
lineClamp={3}
mb="md"
style={{ lineHeight: 1.6 }}
dangerouslySetInnerHTML={{ __html: featured.deskripsi }}
/>
</div>
<Group justify="apart" mt="auto">
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">
<Text
fz={{ base: 'xs', md: 'sm' }}
c="dimmed"
lh={1.5}
style={{
fontSize: '0.875rem',
lineHeight: '1.5rem',
}}
>
{new Date(featured.createdAt).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'long',
@@ -91,7 +111,9 @@ export default function Content({ kategori }: { kategori: string }) {
<Button
variant="light"
rightSection={<IconArrowRight size={16} />}
onClick={() => router.push(`/darmasaba/desa/berita/${kategori}/${featured.id}`)}
onClick={() =>
router.push(`/darmasaba/desa/berita/${kategori}/${featured.id}`)
}
>
Baca Selengkapnya
</Button>
@@ -105,19 +127,29 @@ export default function Content({ kategori }: { kategori: string }) {
{/* === Daftar Berita === */}
<Box mt={50}>
<Title order={2} mb="md">Daftar Berita</Title>
<Title order={2} mb="md">
Daftar Berita
</Title>
<Divider mb="xl" />
{state.findMany.loading ? (
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl">
{Array(3).fill(0).map((_, i) => (
<Skeleton key={i} h={300} radius="md" />
))}
{Array(3)
.fill(0)
.map((_, i) => (
<Skeleton key={i} h={300} radius="md" />
))}
</SimpleGrid>
) : paginatedNews.length === 0 ? (
<Text c="dimmed" ta="center">Belum ada berita di kategori &quot;{kategori}&quot;.</Text>
<Text c="dimmed" ta="center" fz={{ base: 'sm', md: 'md' }} lh={1.5}>
Belum ada berita di kategori &quot;{kategori}&quot;.
</Text>
) : (
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl" verticalSpacing="xl">
<SimpleGrid
cols={{ base: 1, sm: 2, lg: 3 }}
spacing="xl"
verticalSpacing="xl"
>
{paginatedNews.map((item) => (
<Card
key={item.id}
@@ -125,19 +157,51 @@ export default function Content({ kategori }: { kategori: string }) {
p="lg"
radius="md"
withBorder
onClick={() => router.push(`/darmasaba/desa/berita/${kategori}/${item.id}`)}
onClick={() =>
router.push(`/darmasaba/desa/berita/${kategori}/${item.id}`)
}
style={{ cursor: 'pointer' }}
>
<Card.Section>
<Image src={item.image?.link} height={200} alt={item.judul} fit="cover" loading="lazy"/>
<Image
src={item.image?.link}
height={200}
alt={item.judul}
fit="cover"
loading="lazy"
/>
</Card.Section>
<Badge color="blue" variant="light" mt="md">
{item.kategoriBerita?.name || kategori}
</Badge>
<Text fw={600} size="lg" mt="sm" lineClamp={2}>{item.judul}</Text>
<Text size="sm" c="dimmed" lineClamp={3} style={{wordBreak: "break-word", whiteSpace: "normal"}} mt="xs" dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
<Title
order={4}
mt="sm"
fz={{ base: 'sm', md: 'md' }}
style={{ lineHeight: 1.4 }}
lineClamp={2}
>
{item.judul}
</Title>
<Text
fz={{ base: 'xs', md: 'sm' }}
c="dimmed"
lineClamp={3}
style={{
wordBreak: 'break-word',
whiteSpace: 'normal',
lineHeight: 1.5,
}}
mt="xs"
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
/>
<Group justify="apart" mt="md" gap="xs">
<Text size="xs" c="dimmed">
<Text
fz={{ base: 'xs', md: 'xs' }}
c="dimmed"
lh={1.4}
style={{ fontSize: '0.75rem', lineHeight: '1.125rem' }}
>
{new Date(item.createdAt).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'short',

View File

@@ -3,18 +3,16 @@
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
import NewsReader from '@/app/darmasaba/_com/NewsReader';
import colors from '@/con/colors';
import { Box, Center, Container, Group, Image, Skeleton, Stack, Text } from '@mantine/core';
import { Box, Center, Container, Group, Image, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useParams } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
function Page() {
const params = useParams<{ id: string }>();
const id = Array.isArray(params.id) ? params.id[0] : params.id;
const state = useProxy(stateDashboardBerita.berita)
const [loading, setLoading] = useState(true)
const state = useProxy(stateDashboardBerita.berita);
const [loading, setLoading] = useState(true);
useEffect(() => {
const loadData = async () => {
@@ -27,9 +25,9 @@ function Page() {
} finally {
setLoading(false);
}
}
loadData()
}, [id])
};
loadData();
}, [id]);
if (loading) {
return (
@@ -47,41 +45,49 @@ function Page() {
);
}
return (
<Stack pos={"relative"} bg={colors.Bg} pb={"xl"} gap={"xs"} px={{ base: "md", md: 0 }}>
<Group px={{ base: "md", md: 100 }}>
<Stack pos="relative" bg={colors.Bg} pb="xl" gap="xs" px={{ base: 'md', md: 0 }}>
<Group px={{ base: 'md', md: 100 }}>
<NewsReader />
</Group>
<Container w={{ base: "100%", md: "50%" }} >
<Container w={{ base: '100%', md: '50%' }}>
<Box pb={20}>
<Text id='news-title' ta={"center"} fz={"2.4rem"} c={colors["blue-button"]} fw={"bold"}>
{state.findUnique.data?.judul}
</Text>
<Text
ta={"center"}
fw={"bold"}
fz={"1.5rem"}
<Title
id="news-title"
order={1}
ta="center"
c={colors['blue-button']}
fw="bold"
lh={{ base: 1.2, md: 1.25 }}
>
{state.findUnique.data.judul}
</Title>
<Title
order={2}
ta="center"
fw="bold"
fz={{ base: 'md', md: 'lg' }}
lh={{ base: 1.3, md: 1.35 }}
>
Informasi dan Pelayanan Administrasi Digital
</Text>
</Title>
</Box>
<Image src={state.findUnique.data?.image?.link || ''} alt='' w={"100%"} loading="lazy" />
<Image src={state.findUnique.data.image?.link || ''} alt="" w="100%" loading="lazy" />
</Container>
<Box px={{ base: "md", md: 100 }}>
<Stack gap={"xs"}>
<Box px={{ base: 'md', md: 100 }}>
<Stack gap="xs">
<Text
id='news-content'
id="news-content"
py={20}
fz={{ base: "sm", md: "lg" }}
lh={{ base: 1.6, md: 1.8 }} // ✅ line-height lebih rapat dan responsif
fz={{ base: 'sm', md: 'md' }}
lh={{ base: 1.6, md: 1.8 }}
ta="justify"
style={{
wordBreak: "break-word",
whiteSpace: "normal",
wordBreak: 'break-word',
whiteSpace: 'normal',
}}
dangerouslySetInnerHTML={{
__html: state.findUnique.data?.content || "",
__html: state.findUnique.data.content || '',
}}
/>
</Stack>
@@ -90,4 +96,4 @@ function Page() {
);
}
export default Page;
export default Page;

View File

@@ -16,35 +16,30 @@ function Semua() {
const searchParams = useSearchParams();
const router = useTransitionRouter();
// Ambil parameter langsung dari URL
const search = searchParams.get('search') || '';
const page = parseInt(searchParams.get('page') || '1');
// Gunakan proxy untuk state global
const state = useProxy(stateDashboardBerita.berita);
const featured = useProxy(stateDashboardBerita.berita.findFirst);
const loadingGrid = state.findMany.loading;
const loadingFeatured = featured.loading;
// Load berita utama sekali saja
useEffect(() => {
if (!featured.data && !loadingFeatured) {
stateDashboardBerita.berita.findFirst.load();
}
}, [featured.data, loadingFeatured]);
// Load berita terbaru tiap page / search berubah
useEffect(() => {
const limit = 3;
state.findMany.load(page, limit, search);
}, [page, search]);
// Handler pagination → langsung update URL
const handlePageChange = (newPage: number) => {
const url = new URLSearchParams(searchParams.toString());
if (search) url.set('search', search);
if (newPage > 1) url.set('page', newPage.toString());
else url.delete('page'); // biar page=1 ga muncul di URL
else url.delete('page');
router.replace(`?${url.toString()}`);
};
@@ -61,7 +56,7 @@ function Semua() {
<Center><Skeleton h={400} /></Center>
) : featuredData ? (
<Box mb={50}>
<Text fz="h2" fw={700} mb="md">Berita Utama</Text>
<Title order={2} mb="md">Berita Utama</Title>
<Paper shadow="md" radius="md" withBorder>
<Grid gutter={0}>
<GridCol span={{ base: 12, md: 6 }}>
@@ -81,13 +76,24 @@ function Semua() {
<Badge color="blue" variant="light" mb="md">
{featuredData.kategoriBerita?.name || 'Berita'}
</Badge>
<Title order={2} mb="md">{featuredData.judul}</Title>
<Text c="dimmed" lineClamp={3} mb="md" dangerouslySetInnerHTML={{ __html: featuredData.deskripsi }} />
<Title order={3} mb="md">{featuredData.judul}</Title>
<Text
c="dimmed"
lineClamp={3}
mb="md"
dangerouslySetInnerHTML={{ __html: featuredData.deskripsi }}
fz={{ base: 'sm', md: 'md' }}
lh={{ base: 1.5, md: 1.6 }}
/>
</div>
<Group justify="apart" mt="auto">
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">
<Text
c="dimmed"
fz={{ base: 'xs', md: 'sm' }}
lh={{ base: 1.4, md: 1.5 }}
>
{new Date(featuredData.createdAt).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'long',
@@ -124,7 +130,9 @@ function Semua() {
))}
</SimpleGrid>
) : paginatedNews.length === 0 ? (
<Text c="dimmed" ta="center">Tidak ada berita ditemukan.</Text>
<Text c="dimmed" ta="center" fz={{ base: 'sm', md: 'md' }} lh={{ base: 1.5, md: 1.6 }}>
Tidak ada berita ditemukan.
</Text>
) : (
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl" verticalSpacing="xl">
{paginatedNews.map((item) => (
@@ -143,11 +151,24 @@ function Semua() {
{item.kategoriBerita?.name || 'Berita'}
</Badge>
<Text fw={600} size="lg" mt="sm" lineClamp={2}>{item.judul}</Text>
<Text size="sm" c="dimmed" lineClamp={3} mt="xs" dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
<Title order={4} mt="sm" lineClamp={2}>
{item.judul}
</Title>
<Text
c="dimmed"
lineClamp={3}
mt="xs"
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
fz={{ base: 'xs', md: 'sm' }}
lh={{ base: 1.5, md: 1.6 }}
/>
<Flex align="center" justify="apart" mt="md" gap="xs">
<Text size="xs" c="dimmed">
<Text
c="dimmed"
fz={{ base: 'xs', md: 'xs' }}
lh={{ base: 1.4, md: 1.4 }}
>
{new Date(item.createdAt).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'short',
@@ -187,4 +208,4 @@ function Semua() {
);
}
export default Semua;
export default Semua;

View File

@@ -17,17 +17,11 @@ import {
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconPhoto } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
// Komponen kartu foto
function FotoCard({ item }: { item: any }) {
const router = useRouter();
const handleClick = () => {
router.push(`/darmasaba/galeri/foto/${item.id}`);
};
return (
<Grid.Col span={{ base: 12, xs: 6, md: 4 }}>
@@ -35,19 +29,19 @@ function FotoCard({ item }: { item: any }) {
shadow="sm"
radius="md"
p={0}
onClick={handleClick}
style={{ cursor: 'pointer', transition: 'transform 0.2s' }}
style={{ transition: 'transform 0.2s' }}
onMouseEnter={(e) => (e.currentTarget.style.transform = 'scale(1.02)')}
onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')}
>
{item.imageGalleryFoto?.link ? (
<Box
pos="relative"
style={{
paddingBottom: '100%', // ✅ Ubah ke 1:1 (square) — atau sesuaikan
paddingBottom: '100%',
overflow: 'hidden',
borderRadius: '4px 4px 0 0',
backgroundColor: '#f9f9f9', // ✅ background netral
backgroundColor: '#f9f9f9',
}}
>
<Image
@@ -61,8 +55,8 @@ function FotoCard({ item }: { item: any }) {
left: 0,
width: '100%',
height: '100%',
objectFit: 'contain', // ✅ Tampilkan utuh, jangan crop
objectPosition: 'center', // rata tengah
objectFit: 'contain',
objectPosition: 'center',
}}
loading="lazy"
/>
@@ -74,13 +68,23 @@ function FotoCard({ item }: { item: any }) {
)}
<Stack p="md" gap={4}>
<Text fw={600} lineClamp={1}>
<Text fw={600} lineClamp={1} fz={{ base: 'sm', md: 'md' }} lh={{ base: '1.4', md: '1.5' }}>
{item.name || 'Tanpa Judul'}
</Text>
{item.deskripsi && (
<Text fz="sm" c="dimmed" lineClamp={2} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
<Text
fz={{ base: 'xs', md: 'sm' }}
c="dimmed"
lineClamp={2}
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
lh={{ base: '1.4', md: '1.5' }}
/>
)}
<Text fz="xs" c="dimmed">
<Text
fz={{ base: 11, md: 'xs' }}
c="dimmed"
lh={{ base: '1.3', md: '1.4' }}
>
{new Date(item.createdAt).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'short',
@@ -99,7 +103,7 @@ export default function GaleriFotoUser() {
return (
<Box py="xl" px={{ base: 'md', md: 'lg' }}>
{/* Header */}
<Title order={2} c={colors['blue-button']} mb="lg">
<Title order={2} c={colors['blue-button']} mb="lg" ta="center">
Galeri Foto Desa Darmasaba
</Title>
@@ -115,7 +119,7 @@ function FotoList({ search }: { search: string }) {
const { data, page, totalPages, loading, load } = FotoState.findMany;
useShallowEffect(() => {
load(page, 3, search); // ✅ 9 item per halaman
load(page, 3, search);
}, [page, search]);
if (loading) {
@@ -135,7 +139,9 @@ function FotoList({ search }: { search: string }) {
<Center py="xl">
<Stack align="center" c="dimmed">
<IconPhoto size={48} />
<Text>Tidak ada foto ditemukan</Text>
<Text fz={{ base: 'sm', md: 'md' }} lh={{ base: '1.4', md: '1.5' }}>
Tidak ada foto ditemukan
</Text>
</Stack>
</Center>
);
@@ -150,19 +156,18 @@ function FotoList({ search }: { search: string }) {
</Grid>
{/* Pagination */}
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 3, search);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
color="blue"
radius="md"
/>
</Center>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 3, search);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
color="blue"
radius="md"
/>
</Center>
</Stack>
);
}

View File

@@ -11,7 +11,8 @@ import {
Paper,
SimpleGrid,
Stack,
Text
Text,
Title
} from '@mantine/core';
import { useTransitionRouter } from 'next-view-transitions';
import { useCallback, useEffect } from 'react';
@@ -19,15 +20,13 @@ import { useSnapshot } from 'valtio';
export default function VideoContent() {
const videoState = useSnapshot(stateGallery.video);
const router = useTransitionRouter()
const router = useTransitionRouter();
const { data, page, totalPages, loading } = videoState.findMany;
// Handle search and pagination changes
const loadData = useCallback((pageNum: number, searchTerm: string) => {
stateGallery.video.findMany.load(pageNum, 3, searchTerm.trim());
}, []);
// Initial load and URL change handler
useEffect(() => {
const handleRouteChange = () => {
const urlParams = new URLSearchParams(window.location.search);
@@ -57,13 +56,14 @@ export default function VideoContent() {
loadData(newPage, search);
};
const dataVideo = data || [];
if (loading && !data) {
return (
<Box py={10}>
<Text>Memuat Video...</Text>
<Text fz={{ base: 'sm', md: 'md' }} c="dimmed" ta="center">
Memuat Video...
</Text>
</Box>
);
}
@@ -78,55 +78,71 @@ export default function VideoContent() {
p="md"
radius={26}
bg={colors['white-trans-1']}
w={{ base: '100%', md: '100%' }}
w="100%"
>
<Box>
<Center>
<Box
component="iframe"
src={convertToEmbedUrl(v.linkVideo)}
width="100%"
height={300}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
style={{ borderRadius: 8 }}
/>
</Center>
</Box>
<Box>
<Stack gap="sm" py={10}>
<Text fz="sm" c="dimmed">
{new Date(v.createdAt).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'long',
year: 'numeric',
})}
</Text>
<Text fw="bold" fz="sm" lineClamp={1}>
{v.name}
</Text>
<Text
ta="justify"
fz="sm"
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
lineClamp={3}
truncate="end"
/>
<Group justify={"right"}>
<Button
<Center>
<Box
component="iframe"
src={convertToEmbedUrl(v.linkVideo)}
width="100%"
height={300}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
style={{ borderRadius: 8 }}
/>
</Center>
<Stack gap="sm" py={10}>
{/* Tanggal: Caption */}
<Text
fz={{ base: 12, md: 14 }}
c="dimmed"
ta="left"
>
{new Date(v.createdAt).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'long',
year: 'numeric',
})}
</Text>
{/* Judul Video: Subsection (H3) */}
<Title
order={3}
c="dark"
ta="left"
lh={1.3}
style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}
>
{v.name}
</Title>
{/* Deskripsi: Body kecil */}
<Text
ta="justify"
fz={{ base: 13, md: 14 }}
c="dimmed"
style={{ wordBreak: 'break-word' }}
lineClamp={3}
>
<span dangerouslySetInnerHTML={{ __html: v.deskripsi }} />
</Text>
<Group justify="right">
<Button
onClick={() => router.push(`/darmasaba/desa/galery/video/${v.id}`)}
bg={colors['blue-button']}
fz={{ base: 'sm', md: 'md' }}
>
Detail
</Button>
</Group>
</Stack>
</Box>
</Group>
</Stack>
</Paper>
</Box>
))}
</SimpleGrid>
<Center>
<Pagination
value={page}
@@ -140,7 +156,6 @@ export default function VideoContent() {
);
}
// ✅ Fix: convert YouTube URL ke embed
function convertToEmbedUrl(youtubeUrl: string): string {
try {
const url = new URL(youtubeUrl);
@@ -151,4 +166,4 @@ function convertToEmbedUrl(youtubeUrl: string): string {
console.error('Error converting YouTube URL to embed:', err);
return youtubeUrl;
}
}
}

View File

@@ -12,16 +12,17 @@ import {
Stack,
Text,
ThemeIcon,
Title,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconInfoCircle, IconVideo } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery'; // pastikan state bisa dipakai di publik
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
import BackButton from '../../../layanan/_com/BackButto';
// Fungsi helper: aman dan tanpa spasi
function convertToEmbedUrl(youtubeUrl: string): string {
try {
const url = new URL(youtubeUrl);
@@ -72,7 +73,9 @@ export default function DetailVideoUser() {
color="red"
radius="md"
>
Video yang Anda cari tidak tersedia.
<Text fz={{ base: 'sm', md: 'md' }} c="red.9">
Video yang Anda cari tidak tersedia.
</Text>
</Alert>
<Button
leftSection={<IconArrowBack size={16} />}
@@ -91,20 +94,20 @@ export default function DetailVideoUser() {
return (
<Box py="xl" px={{ base: 'md', md: 100 }}>
{/* Tombol Kembali */}
<Box >
<Box>
<BackButton />
</Box>
{/* Header */}
<Text
{/* Header - Dijadikan Title */}
<Title
order={1}
ta="center"
fz={{ base: 'xl', md: '2xl' }}
fw={700}
c={colors['blue-button']}
mb="lg"
lh={{ base: 1.2, md: 1.25 }}
>
{data.name || 'Video Galeri Desa'}
</Text>
</Title>
{/* Konten Utama */}
<Card
@@ -118,7 +121,7 @@ export default function DetailVideoUser() {
{embedUrl ? (
<Box
pos="relative"
style={{ paddingBottom: '56.25%', height: 0, overflow: 'hidden' }} // 16:9 aspect ratio
style={{ paddingBottom: '56.25%', height: 0, overflow: 'hidden' }}
>
<iframe
src={embedUrl}
@@ -144,7 +147,9 @@ export default function DetailVideoUser() {
title="Gagal memuat video"
radius="md"
>
Mohon maaf, video tidak dapat diputar.
<Text fz={{ base: 'xs', md: 'sm' }} c="orange.9">
Mohon maaf, video tidak dapat diputar.
</Text>
</Alert>
) : (
<Alert
@@ -153,7 +158,9 @@ export default function DetailVideoUser() {
title="Tidak ada video"
radius="md"
>
Konten video belum tersedia.
<Text fz={{ base: 'xs', md: 'sm' }} c="dimmed">
Konten video belum tersedia.
</Text>
</Alert>
)}
@@ -163,7 +170,11 @@ export default function DetailVideoUser() {
<ThemeIcon variant="light" size="sm" radius="xl">
<IconInfoCircle size={14} />
</ThemeIcon>
<Text fz="sm" c="dimmed">
<Text
fz={{ base: 'xs', md: 'sm' }}
c="dimmed"
lh={{ base: 1.4, md: 1.5 }}
>
Diunggah pada{' '}
{new Date(data.createdAt).toLocaleDateString('id-ID', {
weekday: 'long',
@@ -179,8 +190,9 @@ export default function DetailVideoUser() {
{data.deskripsi && (
<Paper p="md" bg="gray.0" radius="md">
<Text
fz="md"
fz={{ base: 'sm', md: 'md' }}
c="dark"
ta={"justify"}
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
style={{
wordBreak: 'break-word',

View File

@@ -3,7 +3,7 @@
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors';
import { ActionIcon, Box, Divider, Flex, Group, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { ActionIcon, Box, Divider, Flex, Group, Skeleton, Stack, Text, Title, Tooltip } from '@mantine/core';
import { IconBrandFacebook, IconBrandInstagram, IconBrandTwitter, IconBrandWhatsapp } from '@tabler/icons-react';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
@@ -39,30 +39,38 @@ function PelayananPendudukNonPermanent() {
) : (
<Stack gap="xl">
<Box>
<Text fz={{ base: "xl", md: "2xl" }} fw={700} lh={1.3} c="dark">
<Title
order={1}
fz={{ base: 'lg', md: 'xl' }}
fw={700}
lh={{ base: 1.3, md: 1.3 }}
c="dark"
>
{data?.name || "Judul belum tersedia"}
</Text>
</Title>
</Box>
<Box>
{data?.deskripsi ? (
<Text
fz={{ base: "sm", md: "md" }}
lh={1.7}
fz={{ base: 'sm', md: 'md' }}
lh={{ base: 1.6, md: 1.7 }}
ta="justify"
c="dimmed"
c="black"
dangerouslySetInnerHTML={{ __html: data?.deskripsi }}
style={{wordBreak: "break-word", whiteSpace: "normal"}}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/>
) : (
<Text fz="sm" c="gray">Deskripsi belum tersedia.</Text>
<Text fz="xs" c="gray">
Deskripsi belum tersedia.
</Text>
)}
</Box>
<Divider color={colors["blue-button"]} size="sm" />
<Flex justify="space-between" align="center" wrap="wrap" gap="md">
<Text fz={{ base: "xs", md: "sm" }} c="dimmed">
<Text fz={{ base: 'xs', md: 'sm' }} lh={{ base: 1.4, md: 1.5 }} c="black">
25 Mei 2021 Darmasaba
</Text>
<Group gap="md">
@@ -96,4 +104,4 @@ function PelayananPendudukNonPermanent() {
);
}
export default PelayananPendudukNonPermanent;
export default PelayananPendudukNonPermanent;

View File

@@ -47,7 +47,7 @@ function PelayananPerizinanBerusaha() {
return (
<Center mih={300}>
<Stack align="center" gap="sm">
<Text fz="lg" fw={500} c="dimmed">
<Text fz={{ base: 'md', md: 'lg' }} fw={500} c="dimmed" lh="sm">
Belum ada informasi layanan yang tersedia
</Text>
<Button component="a" href="https://oss.go.id" target="_blank" radius="xl">
@@ -67,10 +67,10 @@ function PelayananPerizinanBerusaha() {
) : (
<Stack gap="lg">
<Box>
<Title order={2} fw={700} fz={{ base: 22, md: 32 }} mb="sm">
<Title order={2} fw={700} mb="sm">
Perizinan Berusaha Berbasis Risiko melalui OSS
</Title>
<Text fz={{ base: 'sm', md: 'md' }} c="dimmed">
<Text fz={{ base: 'sm', md: 'md' }} c="black" lh="sm">
Sistem Online Single Submission (OSS) untuk pendaftaran NIB
</Text>
</Box>
@@ -83,13 +83,13 @@ function PelayananPerizinanBerusaha() {
/>
<Box>
<Text fw={600} mb="sm" fz={{ base: 'sm', md: 'lg' }}>
<Title order={3} fw={600} mb="sm">
Alur pendaftaran NIB:
</Text>
</Title>
<Stepper
active={active}
onStepClick={(step) => {
if (step <= active) { // Only allow clicking on previous or current steps
if (step <= active) {
setActive(step);
}
}}
@@ -102,28 +102,42 @@ function PelayananPerizinanBerusaha() {
}}
>
<StepperStep label="Langkah 1" description="Daftar Akun">
<Text fz="sm">Membuat akun di portal OSS</Text>
<Text fz={{ base: 'xs', md: 'sm' }} lh="sm">
Membuat akun di portal OSS
</Text>
</StepperStep>
<StepperStep label="Langkah 2" description="Isi Data Perusahaan">
<Text fz="sm">Lengkapi informasi perusahaan, data pemegang saham, dan alamat</Text>
<Text fz={{ base: 'xs', md: 'sm' }} lh="sm">
Lengkapi informasi perusahaan, data pemegang saham, dan alamat
</Text>
</StepperStep>
<StepperStep label="Langkah 3" description="Pilih KBLI">
<Text fz="sm">Menentukan kode KBLI sesuai jenis usaha</Text>
<Text fz={{ base: 'xs', md: 'sm' }} lh="sm">
Menentukan kode KBLI sesuai jenis usaha
</Text>
</StepperStep>
<StepperStep label="Langkah 4" description="Unggah Dokumen">
<Text fz="sm">Unggah akta pendirian, surat izin, dan dokumen wajib lainnya</Text>
<Text fz={{ base: 'xs', md: 'sm' }} lh="sm">
Unggah akta pendirian, surat izin, dan dokumen wajib lainnya
</Text>
</StepperStep>
<StepperStep label="Langkah 5" description="Verifikasi Instansi">
<Text fz="sm">Menunggu verifikasi dan persetujuan dari pihak berwenang</Text>
<Text fz={{ base: 'xs', md: 'sm' }} lh="sm">
Menunggu verifikasi dan persetujuan dari pihak berwenang
</Text>
</StepperStep>
<StepperStep label="Langkah 6" description="Terbit NIB">
<Text fz="sm">Menerima NIB sebagai identitas resmi usaha</Text>
<Text fz={{ base: 'xs', md: 'sm' }} lh="sm">
Menerima NIB sebagai identitas resmi usaha
</Text>
</StepperStep>
<StepperCompleted>
<Center>
<Stack align="center" gap="xs">
<IconCheck size={40} color="green" />
<Text fz="sm" fw={500}>Proses pendaftaran selesai</Text>
<Text fz={{ base: 'xs', md: 'sm' }} fw={500} lh="sm">
Proses pendaftaran selesai
</Text>
</Stack>
</Center>
</StepperCompleted>
@@ -159,7 +173,7 @@ function PelayananPerizinanBerusaha() {
)}
</Box>
<Text fz="sm" ta="justify" c="dimmed" mt="md">
<Text fz={{ base: 'xs', md: 'sm' }} ta="justify" c="black" lh="sm" mt="md">
Catatan: Persyaratan dan prosedur dapat berubah sewaktu-waktu. Untuk informasi resmi terbaru, silakan kunjungi situs{' '}
<a href="https://oss.go.id/" target="_blank" rel="noopener noreferrer">
oss.go.id

View File

@@ -2,7 +2,7 @@
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors';
import { BackgroundImage, Box, Button, Center, Group, Pagination, SimpleGrid, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { BackgroundImage, Box, Button, Center, Group, Pagination, SimpleGrid, Skeleton, Stack, Text, Title, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconFileDescription, IconInfoCircle } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
@@ -35,7 +35,7 @@ function PelayananSuratKeterangan({ search }: { search: string }) {
<Center py="xl">
<Stack align="center" gap="xs">
<IconFileDescription size={40} stroke={1.5} color={colors["blue-button"]} />
<Text c="dimmed" ta="center">
<Text c="dimmed" ta="center" fz={{ base: 'sm', md: 'md' }} lh="sm">
Tidak ada layanan surat keterangan yang ditemukan
</Text>
</Stack>
@@ -48,9 +48,9 @@ function PelayananSuratKeterangan({ search }: { search: string }) {
<Group justify="space-between" align="center" mb="md">
<Group gap="xs">
<IconFileDescription size={28} stroke={1.8} />
<Text fz={{ base: "h4", md: "h2" }} fw={700}>
<Title order={2} c="black">
Layanan Surat Keterangan
</Text>
</Title>
</Group>
<Tooltip label="Pilih layanan surat keterangan sesuai kebutuhan Anda" withArrow>
<IconInfoCircle size={22} stroke={1.8} />
@@ -82,15 +82,15 @@ function PelayananSuratKeterangan({ search }: { search: string }) {
style={{ borderRadius: 16 }}
/>
<Stack justify="space-between" h="100%" gap="md" p="lg" pos="relative">
<Text
<Title
order={3}
c="white"
fw={600}
fz="lg"
ta="center"
lineClamp={2}
lh="sm"
>
{v.name}
</Text>
</Title>
<Group justify="center">
<Button
size="md"
@@ -128,4 +128,4 @@ function PelayananSuratKeterangan({ search }: { search: string }) {
);
}
export default PelayananSuratKeterangan;
export default PelayananSuratKeterangan;

View File

@@ -42,9 +42,10 @@ function PelayananTelunjukSaktiDesa() {
return (
<Box>
<Title order={2} mb="lg" fz={{ base: 22, md: 28 }} fw={700} style={{ lineHeight: 1.4 }}>
Layanan Telunjuk Sakti Desa <br />
<Text span c="dimmed" fz="lg" fw={400}>
<Title order={2} mb="lg" fw={700} style={{ lineHeight: 1.3 }} ta="left">
Layanan Telunjuk Sakti Desa
<Text span c="black" fz={{ base: 'sm', md: 'md' }} fw={400} style={{ lineHeight: 1.5 }}>
{' '}
Terwujudnya sistem administrasi kependudukan terintegrasi berbasis elektronik, cerdas, dan aman
</Text>
</Title>
@@ -53,7 +54,7 @@ function PelayananTelunjukSaktiDesa() {
<Skeleton h={400} radius="lg" />
) : data.length === 0 ? (
<Card shadow="sm" radius="lg" withBorder>
<Text c="dimmed" ta="center" py="xl">
<Text c="black" ta="center" py="xl" fz={{ base: 'sm', md: 'md' }} lh={1.5}>
Belum ada layanan tersedia untuk saat ini
</Text>
</Card>
@@ -72,9 +73,9 @@ function PelayananTelunjukSaktiDesa() {
}}
>
<Stack gap="sm">
<Text fw={700} fz="lg" lh={1.4}>
<Title order={3} fw={700} lh={1.3}>
{v.name}
</Text>
</Title>
<Flex gap="xs" align="center">
<IconExternalLink size={18} stroke={1.5} />
<Text
@@ -82,7 +83,7 @@ function PelayananTelunjukSaktiDesa() {
href={v.link}
target="_blank"
rel="noopener noreferrer"
fz="sm"
fz={{ base: 'xs', md: 'sm' }}
c="blue"
td="underline"
style={{ cursor: 'pointer' }}
@@ -100,4 +101,4 @@ function PelayananTelunjukSaktiDesa() {
);
}
export default PelayananTelunjukSaktiDesa;
export default PelayananTelunjukSaktiDesa;

View File

@@ -1,58 +1,94 @@
'use client'
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
import colors from '@/con/colors';
import { Box, Container, Group, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { Box, Container, Group, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useParams } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import BackButton from '../../../layanan/_com/BackButto';
import NewsReader from '@/app/darmasaba/_com/NewsReader';
import BackButton from '../../../layanan/_com/BackButto';
function Page() {
const detail = useProxy(stateDesaPengumuman.pengumuman.findUnique)
const params = useParams()
const detail = useProxy(stateDesaPengumuman.pengumuman.findUnique);
const params = useParams();
useShallowEffect(() => {
stateDesaPengumuman.pengumuman.findUnique.load(params?.id as string)
}, [])
stateDesaPengumuman.pengumuman.findUnique.load(params?.id as string);
}, []);
if (!detail.data) {
return (
<Box>
<Skeleton h={400} />
</Box>
)
);
}
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="md">
{/* Header */}
<Box px={{ base: "md", md: 100 }}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Container size="lg" px="md">
<Group>
<NewsReader />
</Group>
<Stack gap="xs" >
<Group justify={"space-between"} align={"center"}>
<Text fz={{ base: "2rem", md: "2rem" }} c={colors["blue-button"]} fw="bold" >
{detail.data?.judul}
<Stack gap="xs">
<Group justify="space-between" align="flex-start" wrap="wrap">
<Title
order={1}
c={colors['blue-button']}
fz={{ base: 28, md: 36 }}
style={{
wordBreak: 'break-word',
flex: '1 1 auto',
minWidth: 0
}}
>
{detail.data?.judul}
</Title>
<Paper bg={colors['blue-button']} p={8} style={{ flexShrink: 0 }}>
<Text c={colors['white-1']} fz={{ base: 'xs', md: 'sm' }} lh={1.2}>
{detail.data?.CategoryPengumuman?.name}
</Text>
<Group justify='end'>
<Paper bg={colors['blue-button']} p={5}>
<Text c={colors['white-1']}>{detail.data?.CategoryPengumuman?.name}</Text>
</Paper>
</Group>
</Group>
<Paper bg={colors["white-1"]} p="md">
<Text px="lg" id='news-content' fz={"md"} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: detail.data?.content }} />
<Text px="lg" fz={"md"} c={colors["blue-button"]} fw="bold" >
</Paper>
</Group>
<Paper
bg={colors['white-1']}
p="md"
w="100%"
mih={{ base: 200, md: 300 }}
>
<Text
px="lg"
id="news-content"
fz={{ base: 14, md: 16 }}
lh={{ base: 1.6, md: 1.6 }}
style={{
wordBreak: 'break-word',
whiteSpace: 'normal',
width: '100%'
}}
dangerouslySetInnerHTML={{ __html: detail.data?.content }}
/>
<Text
px="lg"
fz={{ base: 12, md: 14 }}
c={colors['blue-button']}
fw="bold"
lh={{ base: 1.4, md: 1.4 }}
mt="md"
>
{new Date(detail.data?.createdAt).toLocaleDateString('id-ID', {
weekday: 'long',
day: 'numeric',
month: 'long',
year: 'numeric'
year: 'numeric',
})}
</Text>
</Paper>
@@ -62,4 +98,4 @@ function Page() {
);
}
export default Page;
export default Page;

View File

@@ -2,14 +2,13 @@
/* eslint-disable react-hooks/exhaustive-deps */
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
import colors from '@/con/colors';
import { Box, Container, Group, Paper, Stack, Text } from '@mantine/core';
import { Box, Container, Group, Paper, Stack, Text, Title } from '@mantine/core';
import { IconCalendar } from '@tabler/icons-react';
import { useProxy } from 'valtio/utils';
import BackButton from '../../layanan/_com/BackButto';
import { useEffect } from 'react';
import { useParams } from 'next/navigation';
function Page() {
const unwrappedParams = useParams();
const kategoriState = useProxy(stateDesaPengumuman);
@@ -26,48 +25,85 @@ function Page() {
<Box px={{ base: "md", md: 100 }}>
<BackButton />
</Box>
<Container size="lg" px="md" >
<Stack align="center" gap="0" >
<Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" ta="center">
{categoryName.split('-').map(word =>
<Container size="lg" px="md">
<Stack align="center" gap="xs">
<Title
order={1}
c={colors["blue-button"]}
ta="center"
style={{ fontWeight: 'bold' }}
>
{categoryName.split('-').map(word =>
word.charAt(0).toUpperCase() + word.slice(1)
).join(' ')}
</Text>
<Text ta="center" px="md" pb={10}>
</Title>
<Text
ta="center"
px="md"
pb="sm"
fz={{ base: 'sm', md: 'md' }}
lh={{ base: 1.5, md: 1.6 }}
c="dimmed"
>
Informasi dan pengumuman resmi terkait {categoryName.split('-').join(' ')}
</Text>
</Stack>
</Container>
<Box px={{ base: 'md', md: 100 }}>
{!kategoriState.pengumuman.findMany.data?.length ? (
<Paper p="lg" radius="md" shadow="md" bg={colors["white-1"]}>
Tidak ada pengumuman yang ditemukan
<Text
fz={{ base: 'sm', md: 'md' }}
ta="center"
c="dimmed"
>
Tidak ada pengumuman yang ditemukan
</Text>
</Paper>
) : kategoriState.pengumuman.findMany.data?.map((v, k) => {
return (
<Paper mb={10} key={k} withBorder p="lg" radius="md" shadow="md" bg={colors["white-1"]}>
<Text fz={'h3'}>{v.judul}</Text>
<Group style={{ color: 'black' }} pb={20}>
) : (
kategoriState.pengumuman.findMany.data?.map((v, k) => (
<Paper
mb="md"
key={k}
withBorder
p="lg"
radius="md"
shadow="md"
bg={colors["white-1"]}
>
<Title order={3}>{v.judul}</Title>
<Group style={{ color: 'black' }} pb="sm">
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">
{v.createdAt ? new Date(v.createdAt).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'long',
year: 'numeric',
}) : 'No date available'}
<Text
fz={{ base: 'xs', md: 'sm' }}
lh={{ base: 1.4, md: 1.5 }}
>
{v.createdAt
? new Date(v.createdAt).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'long',
year: 'numeric',
})
: 'No date available'}
</Text>
</Group>
</Group>
<Text ta={'justify'}>
<Text
ta="justify"
fz={{ base: 'sm', md: 'md' }}
lh={{ base: 1.6, md: 1.7 }}
>
{v.deskripsi}
</Text>
</Paper>
)
})}
))
)}
</Box>
</Stack>
);
}
export default Page;
export default Page;

View File

@@ -9,7 +9,6 @@ import {
Center,
Container,
Divider,
Flex,
Grid,
GridCol,
Group,
@@ -22,7 +21,7 @@ import {
Text,
TextInput,
Title,
UnstyledButton,
UnstyledButton
} from '@mantine/core';
import { IconCalendar, IconClock, IconSearch } from '@tabler/icons-react';
import { useTransitionRouter } from 'next-view-transitions';
@@ -98,10 +97,14 @@ function Page() {
<Container size="lg" px="md">
<Stack align="center" gap="0">
<Text fz={{ base: '2rem', md: '3.4rem' }} c={colors['blue-button']} fw="bold" ta="center">
<Title
order={1}
c={colors['blue-button']}
ta="center"
>
Pengumuman Desa Darmasaba
</Text>
<Text ta="center" px="md" pb={10}>
</Title>
<Text ta="center" px="md" pb={10} fz={{ base: 'sm', md: 'md' }} lh="sm">
Informasi dan pengumuman resmi terkait kegiatan dan kebijakan Desa Darmasaba
</Text>
</Stack>
@@ -126,17 +129,17 @@ function Page() {
withCloseButton={false}
title={item.CategoryPengumuman?.name || 'Pengumuman'}
>
<Stack gap={"xs"}>
<Text fz="sm" fw="bold" c="black" style={{ textTransform: 'uppercase' }}>
<Stack gap="xs">
<Text fz={{ base: 'sm', md: 'sm' }} fw="bold" c="black" style={{ textTransform: 'uppercase' }}>
{item.judul}
</Text>
<Text ta="justify" fz="sm" c="black" lineClamp={3} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
<Text ta="justify" fz={{ base: 'xs', md: 'sm' }} c="black" lineClamp={3} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</Stack>
<Flex pt={20} gap="md" justify="space-between">
<Group pt={20} gap="md" justify="space-between">
<Group style={{ color: 'black' }}>
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">
<Text fz={{ base: 'xs', md: 'sm' }}>
{new Date(item.createdAt).toLocaleDateString('id-ID', {
weekday: 'long',
day: 'numeric',
@@ -147,7 +150,7 @@ function Page() {
</Group>
<Group gap="xs">
<IconClock size={18} />
<Text size="sm">
<Text fz={{ base: 'xs', md: 'sm' }}>
{new Date(item.createdAt).toLocaleTimeString('id-ID', {
hour: '2-digit',
minute: '2-digit',
@@ -157,11 +160,11 @@ function Page() {
</Group>
</Group>
<Anchor variant="transparent" href={`/darmasaba/desa/pengumuman/${item.CategoryPengumuman?.name}/${item.id}`}>
<Text fs="unset" c={colors['blue-button']} fz="sm">
<Text fs="unset" c={colors['blue-button']} fz={{ base: 'xs', md: 'sm' }}>
Baca Selengkapnya
</Text>
</Anchor>
</Flex>
</Group>
</Notification>
))
)}
@@ -169,19 +172,19 @@ function Page() {
<Paper p="md">
<Stack gap="xs">
<Text fw="bold" fz="lg" c={colors['blue-button']}>
<Title order={3} c={colors['blue-button']}>
Kategori
</Text>
</Title>
{stateDesaPengumuman.category.findMany.data?.map((v: any, k) => {
const count = v._count?.pengumumans || 0;
return (
<UnstyledButton component={Link} href={`/darmasaba/desa/pengumuman/${v.name}`} key={k}>
<Paper bg={colors['BG-trans']} p={5}>
<Group px={3} justify="space-between">
<Text fz="md" c="black">
<Text fz={{ base: 'sm', md: 'md' }} c="black">
{v.name}
</Text>
<Text fz="md" c="black">
<Text fz={{ base: 'sm', md: 'md' }} c="black">
{count}
</Text>
</Group>
@@ -200,7 +203,7 @@ function Page() {
<Divider mb={10} color={colors['blue-button']} />
<Grid>
<GridCol span={{ base: 12, md: 8 }}>
<Title order={3}>Daftar Pengumuman</Title>
<Title order={2}>Daftar Pengumuman</Title>
</GridCol>
<GridCol span={{ base: 12, md: 4 }}>
<TextInput
@@ -210,6 +213,7 @@ function Page() {
w="100%"
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)}
fz={{ base: 'sm', md: 'md' }}
/>
</GridCol>
</Grid>
@@ -223,7 +227,9 @@ function Page() {
</SimpleGrid>
) : !state.findMany.data?.length ? (
<Notification withCloseButton={false} h={100}>
Tidak ada pengumuman yang ditemukan
<Text fz={{ base: 'sm', md: 'md' }} ta="center">
Tidak ada pengumuman yang ditemukan
</Text>
</Notification>
) : (
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg" verticalSpacing="lg">
@@ -231,26 +237,26 @@ function Page() {
<Paper key={item.id} p="md" withBorder radius="md" h="100%">
<Stack h="100%" justify="space-between">
<div>
<Text fw={600} c={colors['blue-button']} mb={5}>
<Text fw={600} c={colors['blue-button']} mb={5} fz={{ base: 'sm', md: 'md' }}>
{item.CategoryPengumuman?.name || 'Pengumuman'}
</Text>
<Text fz="lg" fw={700} mb="sm" lineClamp={2} style={{ textTransform: 'uppercase' }}>
<Text fw={700} mb="sm" lineClamp={2} style={{ textTransform: 'uppercase' }} fz={{ base: 'sm', md: 'lg' }}>
{item.judul}
</Text>
<Text
fz="sm"
c="dimmed"
lineClamp={4}
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
mb="md"
style={{wordBreak: "break-word", whiteSpace: "normal"}}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
fz={{ base: 'xs', md: 'sm' }}
/>
</div>
<div>
<Group mb="sm" c="dimmed">
<Group gap={5}>
<IconCalendar size={16} />
<Text size="xs">
<Text fz={{ base: 'xs', md: 'xs' }}>
{new Date(item.createdAt).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'short',
@@ -260,19 +266,19 @@ function Page() {
</Group>
<Group gap={5}>
<IconClock size={16} />
<Text size="xs">
<Text fz={{ base: 'xs', md: 'xs' }}>
{new Date(item.createdAt).toLocaleTimeString('id-ID', {
hour: '2-digit',
minute: '2-digit',
})}
</Text>
</Group>
<Anchor variant="transparent" href={`/darmasaba/desa/pengumuman/${item.CategoryPengumuman?.name}/${item.id}`}>
<Text fw={600} c={colors['blue-button']} fz={{ base: 'sm', md: 'sm' }}>
Baca Selengkapnya
</Text>
</Anchor>
</Group>
<Anchor variant="transparent" href={`/darmasaba/desa/pengumuman/${item.CategoryPengumuman?.name}/${item.id}`}>
<Text fw={600} c={colors['blue-button']} size="sm">
Baca Selengkapnya
</Text>
</Anchor>
</div>
</Stack>
</Paper>
@@ -289,6 +295,7 @@ function Page() {
siblings={1}
boundaries={1}
withEdges
fz={{ base: 'xs', md: 'sm' }}
/>
</Center>
</Stack>

View File

@@ -9,6 +9,7 @@ import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import BackButton from '../../layanan/_com/BackButto';
function Page() {
const params = useParams<{ id: string }>();
const id = Array.isArray(params.id) ? params.id[0] : params.id;
@@ -35,7 +36,9 @@ function Page() {
<Center h="80vh">
<Stack align="center" gap="md">
<Loader size="lg" color="blue" />
<Text c="dimmed" fz="sm">Sedang memuat informasi...</Text>
<Text c="dimmed" fz={{ base: 'xs', md: 'sm' }} ta="center">
Sedang memuat informasi...
</Text>
</Stack>
</Center>
);
@@ -46,28 +49,31 @@ function Page() {
<Center h="80vh">
<Stack align="center" gap="sm">
<IconMoodSad size={64} stroke={1.5} color="var(--mantine-color-blue-6)" />
<Title order={3}>Data Tidak Ditemukan</Title>
<Text c="dimmed" fz="sm">Mohon periksa kembali atau coba beberapa saat lagi</Text>
<Title order={3} ta="center">
Data Tidak Ditemukan
</Title>
<Text c="dimmed" fz={{ base: 'xs', md: 'sm' }} ta="center">
Mohon periksa kembali atau coba beberapa saat lagi
</Text>
</Stack>
</Center>
);
}
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="xl" px={{ base: "md", md: 0 }}>
<Box px={{ base: "md", md: 100 }}>
<Stack pos="relative" bg={colors.Bg} py="xl" gap="xl" px={{ base: 'md', md: 0 }}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Container w={{ base: "100%", md: "60%" }}>
<Container w={{ base: '100%', md: '60%' }}>
<Paper radius="2xl" shadow="lg" p="xl" withBorder>
<Stack gap="lg" align="center">
<Title ta="center" fz={{ base: "2rem", md: "3rem" }} c={colors["blue-button"]} fw={800}>
<Title order={1} ta="center" c={colors['blue-button']} fw={800}>
{state.findUnique.data?.name}
</Title>
<Text ta="center" fw={600} fz={{ base: "md", md: "lg" }} c="dimmed">
<Text ta="center" fw={600} fz={{ base: 'md', md: 'lg' }} c="dimmed">
Informasi & Pelayanan Potensi Desa Digital
</Text>
{/* ✅ Bagian gambar dibuat konsisten tanpa CSS manual */}
<Box
w="100%"
h={{ base: 220, md: 400 }}
@@ -87,7 +93,15 @@ function Page() {
radius="lg"
/>
</Box>
<Text py="md" fz={{ base: "sm", md: "md" }} ta="justify" lh={1.8} dangerouslySetInnerHTML={{ __html: state.findUnique.data?.deskripsi || 'Belum ada deskripsi untuk potensi desa ini.' }} />
<Text
py="md"
fz={{ base: 'sm', md: 'md' }}
ta="justify"
lh={{ base: 1.6, md: 1.8 }}
dangerouslySetInnerHTML={{
__html: state.findUnique.data?.content || 'Belum ada deskripsi untuk potensi desa ini.',
}}
/>
</Stack>
</Paper>
</Container>
@@ -95,4 +109,4 @@ function Page() {
);
}
export default Page;
export default Page;

View File

@@ -2,7 +2,7 @@
'use client'
import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi';
import colors from '@/con/colors';
import { BackgroundImage, Box, Button, Center, Flex, Group, Paper, SimpleGrid, Skeleton, Stack, Text } from '@mantine/core';
import { BackgroundImage, Box, Button, Flex, Group, Paper, SimpleGrid, Skeleton, Stack, Text, Title } from '@mantine/core';
import { IconEye } from '@tabler/icons-react';
import { useTransitionRouter } from 'next-view-transitions';
import { useEffect, useState } from 'react';
@@ -41,10 +41,10 @@ function Page() {
<Box px={{ base: "md", md: 100 }}>
<Flex justify="space-between" align="center" direction={{ base: "column", md: "row" }} gap="lg">
<Stack gap="sm" maw={600}>
<Text fz={{ base: "2rem", md: "3rem" }} fw={900} c={colors["blue-button"]} lh={1.2}>
<Title order={1} fz={{ base: 28, md: 36 }} lh={1.2} c={colors["blue-button"]}>
Potensi Desa Darmasaba
</Text>
<Text fz="lg" ta="justify">
</Title>
<Text fz={{ base: 14, md: 16 }} lh={1.6} ta="justify">
Temukan berbagai potensi unggulan, peluang, dan daya tarik yang menjadikan Desa Darmasaba istimewa.
</Text>
</Stack>
@@ -58,18 +58,18 @@ function Page() {
>
<Flex justify="center" align="center" gap="xl">
<Box>
<Text ta="center" fz="2rem" fw={800} c="white">
<Text ta="center" fz={{ base: 20, md: 32 }} fw={800} c="white" lh={1.2}>
{data?.filter(item => item.kategori?.nama.toLowerCase() !== 'wisata').length || 0}
</Text>
<Text ta="center" fz="sm" c="white" fw={500}>
<Text ta="center" fz={{ base: 12, md: 14 }} c="white" fw={500}>
Potensi
</Text>
</Box>
<Box>
<Text ta="center" fz="2rem" fw={800} c="white">
<Text ta="center" fz={{ base: 20, md: 32 }} fw={800} c="white" lh={1.2}>
{data?.filter(item => item.kategori?.nama.toLowerCase() === 'wisata').length || 0}
</Text>
<Text ta="center" fz="sm" c="white" fw={500}>
<Text ta="center" fz={{ base: 12, md: 14 }} c="white" fw={500}>
Wisata
</Text>
</Box>
@@ -91,45 +91,40 @@ function Page() {
radius="xl"
onMouseEnter={() => setHoveredId(v.id)}
onMouseLeave={() => setHoveredId(null)}
style={{
overflow: 'hidden',
style={{
overflow: 'hidden',
position: 'relative',
cursor: 'pointer',
transition: 'transform 0.3s ease'
}}
>
{/* Overlay with smooth transition */}
<Box
pos="absolute"
inset={0}
bg={hoveredId === v.id
? "linear-gradient(180deg, rgba(0,0,0,0.4) 0%, rgba(0,0,0,0.75) 100%)"
bg={hoveredId === v.id
? "linear-gradient(180deg, rgba(0,0,0,0.4) 0%, rgba(0,0,0,0.75) 100%)"
: "linear-gradient(180deg, rgba(0,0,0,0.1) 0%, rgba(0,0,0,0.15) 100%)"
}
style={{
transition: 'background 0.3s ease'
}}
/>
<Stack justify="space-between" h="100%" gap="md" p="lg" pos="relative">
{/* Kategori badge - always visible */}
<Group>
<Paper
radius="lg"
py={6}
px={12}
shadow="md"
withBorder
<Paper
radius="lg"
py={6}
px={12}
shadow="md"
withBorder
bg="rgba(255,255,255,0.9)"
style={{
transition: 'all 0.3s ease'
}}
style={{ transition: 'all 0.3s ease' }}
>
<Text fz="sm" fw={600}>{v.kategori?.nama}</Text>
<Text fz={{ base: 11, md: 14 }} fw={600}>{v.kategori?.nama}</Text>
</Paper>
</Group>
{/* Nama potensi - visible on hover */}
<Box
style={{
opacity: hoveredId === v.id ? 1 : 0,
@@ -138,20 +133,20 @@ function Page() {
pointerEvents: hoveredId === v.id ? 'auto' : 'none'
}}
>
<Text
<Title
order={3}
fw={800}
c="white"
fz="xl"
fz={{ base: 18, md: 20 }}
ta="center"
lineClamp={2}
lh={1.3}
>
{v.name}
</Text>
</Title>
</Box>
{/* Button - visible on hover */}
<Group
<Group
justify="center"
style={{
opacity: hoveredId === v.id ? 1 : 0,
@@ -169,23 +164,21 @@ function Page() {
gradient={{ from: colors["blue-button"], to: "#4dabf7", deg: 45 }}
onClick={() => router.push(`/darmasaba/desa/potensi/${v.id}`)}
>
Lihat Detail
<Text c={'white'} fz={{ base: 12, md: 14 }} fw={500}>Lihat Detail</Text>
</Button>
</Group>
</Stack>
</BackgroundImage>
))
) : (
<Center h={240}>
<Stack align="center" gap="xs">
<Text fz="lg" fw={600} c="dimmed">
Belum ada potensi desa
</Text>
<Text fz="sm" c="dimmed">
Data potensi akan tampil di sini setelah tersedia.
</Text>
</Stack>
</Center>
<Stack align="center" gap="xs">
<Text fz={{ base: 14, md: 16 }} fw={600} c="dimmed">
Belum ada potensi desa
</Text>
<Text fz={{ base: 12, md: 14 }} c="dimmed">
Data potensi akan tampil di sini setelah tersedia.
</Text>
</Stack>
)}
</SimpleGrid>
</Box>
@@ -193,4 +186,4 @@ function Page() {
);
}
export default Page;
export default Page;

View File

@@ -26,7 +26,6 @@ function DetailPegawaiUser() {
statePegawai.findUnique.load(params?.id as string);
}, []);
if (!statePegawai.findUnique.data) {
return (
<Stack py="lg">
@@ -41,7 +40,7 @@ function DetailPegawaiUser() {
<Box px={{ base: 'md', md: 100 }} py="xl">
{/* Back button */}
<Group mb="lg" px={{ base: 'md', md: 100 }}>
<BackButton/>
<BackButton />
</Group>
<Paper
@@ -69,11 +68,17 @@ function DetailPegawaiUser() {
/>
{/* Nama & Jabatan */}
<Stack align="center" gap={2}>
<Title order={3} fw={700} c={colors['blue-button']}>
<Stack align="center" gap={4}>
{/* Title utama → H2 karena ini judul profil */}
<Title order={2} c={colors['blue-button']} lh={1.2}>
{data.namaLengkap || '-'} {data.gelarAkademik || ''}
</Title>
<Text fz="sm" c="dimmed">
<Text
fz={{ base: 'sm', md: 'md' }}
lh={1.4}
c="dimmed"
>
{data.posisi?.nama || 'Posisi tidak tersedia'}
</Text>
</Stack>
@@ -82,7 +87,11 @@ function DetailPegawaiUser() {
<Divider my="lg" />
{/* Informasi Detail */}
<Stack gap="md">
<Stack gap="lg">
<Title order={3} lh={1.3}>
Informasi Pegawai
</Title>
<InfoRow label="Email" value={data.email} />
<InfoRow label="Telepon" value={data.telepon} />
<InfoRow label="Alamat" value={data.alamat} multiline />
@@ -91,10 +100,10 @@ function DetailPegawaiUser() {
value={
data.tanggalMasuk
? new Date(data.tanggalMasuk).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric',
})
day: '2-digit',
month: 'long',
year: 'numeric',
})
: '-'
}
/>
@@ -123,11 +132,18 @@ function InfoRow({
}) {
return (
<Box>
<Text fz="sm" fw={600} c="dark">
<Text
fz={{ base: 'sm', md: 'md' }}
fw={600}
lh={1.3}
c="dark"
>
{label}
</Text>
<Text
fz="sm"
fz={{ base: 'sm', md: 'md' }}
lh={1.5}
c={valueColor || 'dimmed'}
style={{
whiteSpace: multiline ? 'normal' : 'nowrap',

View File

@@ -36,11 +36,12 @@ import { useTransitionRouter } from 'next-view-transitions'
import { OrganizationChart } from 'primereact/organizationchart'
import { useEffect, useRef, useState } from 'react'
import { useProxy } from 'valtio/utils'
import './struktur.css'
import BackButton from '../_com/BackButto'
import { useMediaQuery } from '@mantine/hooks'
export default function StrukturPerangkatDesa() {
import './struktur.css'
import { useMediaQuery } from '@mantine/hooks'
import BackButton from '../_com/BackButto'
export default function Page() {
return (
<Box
style={{
@@ -59,10 +60,11 @@ export default function StrukturPerangkatDesa() {
ta="center"
c={colors['blue-button']}
fz={{ base: 28, md: 36, lg: 44 }}
lh={{ base: 1.05, md: 1.03 }}
>
Struktur Perangkat Desa
</Title>
<Text ta="center" c="black" maw={800}>
<Text ta="center" c="black" maw={800} fz={{ base: 13, md: 15 }} lh={1.45}>
Gambaran visual peran dan pegawai yang ditugaskan. Arahkan kursor
untuk melihat detail atau klik node untuk fokus tampilan.
</Text>
@@ -105,8 +107,8 @@ function StrukturPerangkatDesaNode() {
<Center py={48}>
<Stack align="center" gap="sm">
<Loader size="lg" />
<Text fw={600}>Memuat struktur organisasi</Text>
<Text c="dimmed" size="sm">
<Text fw={600} fz={{ base: 15, md: 16 }} lh={1.2}>Memuat struktur organisasi</Text>
<Text c="dimmed" fz={{ base: 12, md: 13 }} lh={1.4}>
Mengambil data pegawai dan posisi. Mohon tunggu sebentar.
</Text>
</Stack>
@@ -132,10 +134,10 @@ function StrukturPerangkatDesaNode() {
<Center>
<IconUsers size={56} />
</Center>
<Title order={3} mt="md">
<Title order={3} mt="md" fz={{ base: 16, md: 18 }} lh={1.15}>
Data pegawai belum tersedia
</Title>
<Text c="dimmed" mt="xs">
<Text c="dimmed" mt="xs" fz={{ base: 13, md: 14 }} lh={1.4}>
Belum ada data pegawai yang tercatat untuk PPID.
</Text>
<Group justify="center" mt="lg">
@@ -232,11 +234,18 @@ function StrukturPerangkatDesaNode() {
{/* 🔍 Controls */}
<Paper
shadow="xs"
w={{
base: '100%', // Mobile: 100%
sm: '40%', // Tablet: 95%
md: '39%', // Desktop: 70%
lg: '38%', // Desktop L: 60%
xl: '37%', // 4K: 50%
'2xl': '36%', // Ultra-wide: 45%
}}
p="md"
radius="md"
style={{
background: colors['blue-button'],
width: '100%', // ⬅️ penting
background: colors['blue-button'], // ⬅️ penting
maxWidth: '100%', // ⬅️ penting
overflowX: 'auto' // ⬅️ untuk mencegah overflow
}}
@@ -269,30 +278,33 @@ function StrukturPerangkatDesaNode() {
fontSize: '0.875rem',
padding: '6px 12px',
minHeight: 'auto',
flexShrink: 0, // 👈 PENTING: mencegah tab mengecil
flexShrink: 0,
},
}}
style={{ width: '100%' }} // 👈 penting
>
<TabsList
style={{
display: 'flex',
overflowX: 'auto',
overflowY: 'hidden', // 👈 tambahkan ini
overflowY: 'hidden',
gap: '4px',
paddingBottom: '4px',
flexWrap: 'nowrap',
WebkitOverflowScrolling: 'touch', // 👈 smooth scroll di iOS
scrollbarWidth: 'thin', // 👈 scrollbar tipis di Firefox
msOverflowStyle: '-ms-autohiding-scrollbar', // 👈 untuk IE/Edge
WebkitOverflowScrolling: 'touch',
scrollbarWidth: 'thin',
msOverflowStyle: '-ms-autohiding-scrollbar',
maxWidth: '100%',
scrollBehavior: 'smooth', // 👈 smooth scroll
}}
>
<TabsTab
value="zoom-out"
onClick={handleZoomOut}
leftSection={<IconZoomOut size={16} />}
style={{ flexShrink: 0 }} // 👈 pastikan tidak mengecil
style={{ flexShrink: 0 }}
>
Zoom Out
<Text fz={{ base: 12, sm: 13 }} lh={1} ta="center">Zoom Out</Text>
</TabsTab>
<Box
@@ -301,7 +313,6 @@ function StrukturPerangkatDesaNode() {
px={12}
py={6}
style={{
fontSize: 14,
fontWeight: 700,
borderRadius: '6px',
minWidth: 60,
@@ -310,10 +321,12 @@ function StrukturPerangkatDesaNode() {
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
whiteSpace: 'nowrap', // 👈 mencegah text wrap
whiteSpace: 'nowrap',
}}
>
{Math.round(scale * 100)}%
<Text fz={{ base: 12, sm: 13 }} lh={1} c={colors['blue-button']}>
{Math.round(scale * 100)}%
</Text>
</Box>
<TabsTab
@@ -322,7 +335,7 @@ function StrukturPerangkatDesaNode() {
leftSection={<IconZoomIn size={16} />}
style={{ flexShrink: 0 }}
>
Zoom In
<Text fz={{ base: 12, sm: 13 }} lh={1} ta="center">Zoom In</Text>
</TabsTab>
<TabsTab
@@ -330,7 +343,7 @@ function StrukturPerangkatDesaNode() {
onClick={resetZoom}
style={{ flexShrink: 0 }}
>
Reset
<Text fz={{ base: 12, sm: 13 }} lh={1} ta="center">Reset</Text>
</TabsTab>
<TabsTab
@@ -345,7 +358,9 @@ function StrukturPerangkatDesaNode() {
}
style={{ flexShrink: 0 }}
>
{isFullscreen ? 'Exit' : 'Fullscreen'}
<Text fz={{ base: 12, sm: 13 }} lh={1} ta="center">
{isFullscreen ? 'Exit' : 'Fullscreen'}
</Text>
</TabsTab>
</TabsList>
</Tabs>
@@ -451,17 +466,17 @@ function NodeCard({ node, router }: any) {
{/* Name */}
<Text
fw={700}
size="sm"
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',
lineHeight: 1.3,
}}
>
{name}
@@ -469,18 +484,18 @@ function NodeCard({ node, router }: any) {
{/* Title/Position */}
<Text
size="xs"
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',
lineHeight: 1.2,
}}
>
{title}
@@ -496,14 +511,14 @@ function NodeCard({ node, router }: any) {
mt={8}
radius="md"
onClick={() =>
router.push(`/darmasaba/desa/profile/struktur-perangkat-desa/${node.data.id}`)
router.push(`/darmasaba/desa/profil/struktur-perangkat-desa/${node.data.id}`)
}
style={{
height: 32,
fontWeight: 600,
}}
>
Lihat Detail
<Text fz={{ base: 12, md: 13 }} lh={1} ta="center">Lihat Detail</Text>
</Button>
)}
</Stack>

View File

@@ -2,7 +2,7 @@
'use client'
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'
import colors from '@/con/colors'
import { Box, Center, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'
import { Box, Center, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core'
import { useEffect } from 'react'
import { useProxy } from 'valtio/utils'
@@ -26,6 +26,8 @@ function LambangDesa() {
return (
<Box>
<Stack align="center" gap="lg">
{/* HEADER */}
<Box pb="lg">
<Center>
<Image
@@ -36,17 +38,20 @@ function LambangDesa() {
loading="lazy"
/>
</Center>
<Text
{/* TITLE - H1 */}
<Title
order={1}
c={colors['blue-button']}
ta="center"
fw={800}
fz={{ base: 28, md: 40 }}
mt="sm"
style={{ letterSpacing: '-0.5px' }}
>
Lambang Desa
</Text>
</Title>
</Box>
{/* DESKRIPSI */}
<Paper
p="xl"
radius="xl"
@@ -58,15 +63,20 @@ function LambangDesa() {
borderColor: '#e0e9ff',
}}
>
<Text
fz={{ base: '1.125rem', md: '1.375rem' }}
lh={1.8}
c="dark"
ta="justify"
style={{ fontWeight: 400, wordBreak: "break-word", whiteSpace: "normal", }}
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
/>
<Text
fz={{ base: 'sm', md: 'md' }} // Body text mobile & desktop
lh={1.7}
c="dark"
ta="justify"
style={{
fontWeight: 400,
wordBreak: "break-word",
whiteSpace: "normal",
}}
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
/>
</Paper>
</Stack>
</Box>
)

View File

@@ -2,7 +2,7 @@
'use client'
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors';
import { Box, Card, Center, Group, Image, Loader, Paper, Stack, Text } from '@mantine/core';
import { Box, Card, Center, Group, Image, Loader, Paper, Stack, Text, Title } from '@mantine/core';
import { IconPhoto } from '@tabler/icons-react';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
@@ -21,7 +21,9 @@ function MaskotDesa() {
<Center mih={500}>
<Stack align="center" gap="sm">
<Loader size="lg" color="blue" />
<Text c="dimmed" fz="sm">Sedang memuat data maskot desa...</Text>
<Text c="dimmed" fz={{ base: 'xs', md: 'sm' }}>
Sedang memuat data maskot desa...
</Text>
</Stack>
</Center>
);
@@ -31,8 +33,21 @@ function MaskotDesa() {
<Box>
<Stack align="center" gap="xl">
<Stack align="center" gap={10}>
<Image src="/pudak-icon.png" alt="Ikon Desa" w={{ base: 160, md: 240 }} loading="lazy"/>
<Text c={colors['blue-button']} ta="center" fw={700} fz={{ base: 28, md: 36 }}>Maskot Desa</Text>
<Image
src="/pudak-icon.png"
alt="Ikon Desa"
w={{ base: 160, md: 240 }}
loading="lazy"
/>
{/* Page Title */}
<Title
order={1}
ta="center"
c={colors['blue-button']}
>
Maskot Desa
</Title>
</Stack>
<Paper
@@ -42,48 +57,60 @@ function MaskotDesa() {
withBorder
style={{ background: 'linear-gradient(145deg, #ffffff, #f8f9fa)' }}
>
{/* Body Description */}
<Text
fz={{ base: 'sm', md: 'lg' }}
lh={1.7}
ta="justify"
c="dark"
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
style={{wordBreak: "break-word", whiteSpace: "normal"}}
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
/>
<Group justify="center" gap="lg" mt="lg">
{data.images.length > 0 ? (
data.images.map((img, index) => (
<Card
<Card
key={index}
radius="lg"
shadow="md"
withBorder
w={220}
p="sm"
style={{
transition: 'transform 200ms ease, box-shadow 200ms ease',
}}
className="hover:scale-105 hover:shadow-lg"
radius="lg"
shadow="md"
withBorder
w={220}
p="sm"
style={{
transition: 'transform 200ms ease, box-shadow 200ms ease',
}}
className="hover:scale-105 hover:shadow-lg"
>
<Image
src={img.image.link}
alt={img.label}
w="100%"
h={200}
fit="cover"
radius="md"
loading="lazy"
/>
{/* Image Label */}
<Text
ta="center"
mt="sm"
fw={600}
fz={{ base: 'xs', md: 'sm' }}
c="dark"
>
<Image
src={img.image.link}
alt={img.label}
w="100%"
h={200}
fit="cover"
radius="md"
loading="lazy"
/>
<Text ta="center" mt="sm" fw={600} fz="sm" c="dark">
{img.label}
</Text>
</Card>
{img.label}
</Text>
</Card>
))
) : (
<Stack align="center" gap="xs" mt="lg">
<IconPhoto size={48} stroke={1.5} color="gray" />
<Text c="dimmed" fz="sm">Belum ada gambar maskot yang ditambahkan</Text>
<Text c="dimmed" fz={{ base: 'xs', md: 'sm' }}>
Belum ada gambar maskot yang ditambahkan
</Text>
</Stack>
)}
</Group>

View File

@@ -1,35 +1,15 @@
'use client'
import { ActionIcon, Box, Flex, Paper, SimpleGrid, Stack, Text } from '@mantine/core';
import { ActionIcon, Box, Flex, Paper, SimpleGrid, Stack, Text, Title } from '@mantine/core';
import { motion } from 'framer-motion';
import { IconSparkles } from '@tabler/icons-react';
import colors from '@/con/colors';
const dataText = [
{
id: 1,
title: "Santun",
description: "Pelayanan ramah, penuh empati, sopan, dan beretika."
},
{
id: 2,
title: "Adaptif",
description: "Cepat menyesuaikan diri terhadap perubahan dan selalu proaktif."
},
{
id: 3,
title: "Inovatif",
description: "Berani menciptakan pembaruan dan ide-ide kreatif."
},
{
id: 4,
title: "Profesional",
description: "Berpengetahuan luas, terampil, dan bertanggung jawab."
},
{
id: 5,
title: "Gesit",
description: "Cekatan, sigap, dan penuh inisiatif dalam bekerja."
},
{ id: 1, title: "Santun", description: "Pelayanan ramah, penuh empati, sopan, dan beretika." },
{ id: 2, title: "Adaptif", description: "Cepat menyesuaikan diri terhadap perubahan dan selalu proaktif." },
{ id: 3, title: "Inovatif", description: "Berani menciptakan pembaruan dan ide-ide kreatif." },
{ id: 4, title: "Profesional", description: "Berpengetahuan luas, terampil, dan bertanggung jawab." },
{ id: 5, title: "Gesit", description: "Cekatan, sigap, dan penuh inisiatif dalam bekerja." },
];
const letters = ["S", "I", "G", "A", "P"];
@@ -38,11 +18,14 @@ function MotoDesa() {
return (
<Box px={{ base: "md", md: "xl" }}>
<Stack align="center" gap="lg">
{/* Page Title */}
<Box>
<Text
<Title
order={1}
ta="center"
fw={800}
fz={{ base: "2rem", md: "2.8rem" }}
fz={{ base: 28, md: 36 }}
lh={{ base: 1.2, md: 1.3 }}
style={{
background: "linear-gradient(90deg, #0D5594FF, #094678FF)",
WebkitBackgroundClip: "text",
@@ -50,9 +33,10 @@ function MotoDesa() {
}}
>
Moto Desa Darmasaba
</Text>
</Title>
</Box>
{/* Letter Icons */}
<Flex gap={30} pb={40} pt={10} wrap="wrap" justify="center">
{letters.map((letter, i) => (
<motion.div
@@ -71,7 +55,7 @@ function MotoDesa() {
backdropFilter: "blur(6px)",
}}
>
<Text c="white" fw={800} fz="xl">
<Text c="white" fw={800} fz={{ base: 20, md: 24 }}>
{letter}
</Text>
</ActionIcon>
@@ -79,6 +63,7 @@ function MotoDesa() {
))}
</Flex>
{/* Values Card */}
<Paper
radius="lg"
p="xl"
@@ -90,19 +75,22 @@ function MotoDesa() {
>
<SimpleGrid cols={{ base: 1, md: 2 }} spacing="xl">
{dataText.map((v) => (
<motion.div
key={v.id}
whileHover={{ scale: 1.02 }}
transition={{ duration: 0.2 }}
>
<motion.div key={v.id} whileHover={{ scale: 1.02 }} transition={{ duration: 0.2 }}>
<Stack gap={4}>
{/* Section Title */}
<Flex align="center" gap="sm">
<IconSparkles size={20} color={colors['blue-button']} />
<Text fw={700} fz={{ base: "lg", md: "xl" }} c={colors['blue-button']}>
<Title
order={3}
fw={700}
fz={{ base: 20, md: 24 }}
c={colors['blue-button']}
>
{v.title}
</Text>
</Title>
</Flex>
<Text fz={{ base: "sm", md: "md" }} c="gray.7">
{/* Body Text */}
<Text fz={{ base: 14, md: 16 }} lh={{ base: 1.5, md: 1.6 }} c="gray.7">
{v.description}
</Text>
</Stack>
@@ -111,16 +99,15 @@ function MotoDesa() {
</SimpleGrid>
</Paper>
{/* Motto Description */}
<Text
ta="center"
fw={700}
fz={{ base: "md", md: "xl" }}
fz={{ base: 15, md: 20 }}
lh={{ base: 1.6, md: 1.8 }}
c="blue.8"
mt="md"
style={{
maxWidth: 720,
lineHeight: 1.6,
}}
style={{ maxWidth: 720 }}
>
&quot;Berkomitmen menghadirkan pelayanan terbaik dengan semangat{" "}
<Text span fw={800} c="cyan.6">

View File

@@ -2,44 +2,45 @@
'use client'
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors';
import { Box, Divider, Image, Paper, SimpleGrid, Skeleton, Stack, Text } from '@mantine/core';
import { Box, Divider, Image, Paper, SimpleGrid, Skeleton, Stack, Text, Title } from '@mantine/core';
import { IconBriefcase, IconTargetArrow, IconUser, IconUsers } from '@tabler/icons-react';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
function ProfilPerbekel() {
const state = useProxy(stateProfileDesa.profilPerbekel)
const state = useProxy(stateProfileDesa.profilPerbekel);
useEffect(() => {
state.findUnique.load("edit")
}, [])
state.findUnique.load("edit");
}, []);
const { data, loading } = state.findUnique
const { data, loading } = state.findUnique;
if (loading || !data) {
return (
<Box py={20} px="md">
<Skeleton h={500} radius="lg" />
</Box>
)
);
}
return (
<Box px="md">
{/* ===== PAGE TITLE ===== */}
<Stack align="center" gap={0} mb={40}>
<Text
<Title
order={1}
c={colors['blue-button']}
ta="center"
fw="bold"
fz={{ base: "2rem", md: "2.8rem" }}
style={{ letterSpacing: "0.5px" }}
>
Profil Perbekel
</Text>
</Title>
<Divider w={120} size="sm" color={colors['blue-button']} mt={10} />
</Stack>
<SimpleGrid cols={{ base: 1, md: 2 }} spacing="xl" pb={50}>
{/* ========== FOTO PERBEKEL ========== */}
<Box>
<Paper
bg={colors['white-trans-1']}
@@ -60,6 +61,8 @@ function ProfilPerbekel() {
}}
loading="lazy"
/>
{/* ===== NAMA DAN JABATAN ===== */}
<Paper
bg={colors['blue-button']}
px="lg"
@@ -67,22 +70,23 @@ function ProfilPerbekel() {
className="glass3"
py={{ base: 20, md: 50 }}
>
<Text c={colors['white-1']} fz={{ base: "lg", md: "h3" }}>
<Title order={3} c={colors['white-1']}>
Perbekel Desa Darmasaba
</Text>
<Text
</Title>
<Title
order={2}
c={colors['white-1']}
fw="bolder"
fz={{ base: "xl", md: "h2" }}
mt={8}
>
{"I.B. Surya Prabhawa Manuaba, S.H.,M.H.,NL.P."}
</Text>
</Title>
</Paper>
</Stack>
</Paper>
</Box>
{/* ========== BIODATA & PENGALAMAN ========== */}
<Paper
p="xl"
bg={colors['white-trans-1']}
@@ -92,34 +96,39 @@ function ProfilPerbekel() {
withBorder
>
<Stack gap="xl">
{/* ===== BIODATA ===== */}
<Box>
<Stack gap={6}>
<Stack align="center" gap={6}>
<IconUser size={22} />
<Text fz={{ base: "1.2rem", md: "1.5rem" }} fw="bold">Biodata</Text>
<Title order={3}>Biodata</Title>
</Stack>
<Text
fz={{ base: "1rem", md: "1.2rem" }}
fz={{ base: "sm", md: "md" }}
ta="justify"
lh={1.6}
lh={1.7}
dangerouslySetInnerHTML={{ __html: data.biodata }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
style={{ wordBreak: "break-word" }}
/>
</Stack>
</Box>
{/* ===== PENGALAMAN ===== */}
<Box>
<Stack gap={6}>
<Stack align="center" gap={6}>
<IconBriefcase size={22} />
<Text fz={{ base: "1.2rem", md: "1.5rem" }} fw="bold">Pengalaman</Text>
<Title order={3}>Pengalaman</Title>
</Stack>
<Text
fz={{ base: "1rem", md: "1.2rem" }}
fz={{ base: "sm", md: "md" }}
ta="left"
lh={1.6}
lh={1.7}
dangerouslySetInnerHTML={{ __html: data.pengalaman }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
style={{ wordBreak: "break-word" }}
/>
</Stack>
</Box>
@@ -127,6 +136,7 @@ function ProfilPerbekel() {
</Paper>
</SimpleGrid>
{/* ========== ORGANISASI & PROGRAM UNGGULAN ========== */}
<Paper
p="xl"
bg={colors['white-trans-1']}
@@ -136,35 +146,41 @@ function ProfilPerbekel() {
withBorder
>
<Stack gap="xl">
{/* ===== PENGALAMAN ORGANISASI ===== */}
<Box>
<Stack align="center" gap={6} >
<Stack align="center" gap={6}>
<IconUsers size={22} />
<Text fz={{ base: "1.2rem", md: "1.5rem" }} fw="bold">Pengalaman Organisasi</Text>
<Title order={3}>Pengalaman Organisasi</Title>
</Stack>
<Text
fz={{ base: "1rem", md: "1.2rem" }}
fz={{ base: "sm", md: "md" }}
ta="justify"
lh={1.6}
lh={1.7}
dangerouslySetInnerHTML={{ __html: data.pengalamanOrganisasi }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
style={{ wordBreak: "break-word" }}
/>
</Box>
{/* ===== PROGRAM UNGGULAN ===== */}
<Box>
<Stack align="center" gap={6} mb={6}>
<IconTargetArrow size={22} />
<Text fz={{ base: "1.2rem", md: "1.5rem" }} fw="bold">Program Kerja Unggulan</Text>
<Title order={3}>Program Kerja Unggulan</Title>
</Stack>
<Box px={10}>
<Text
fz={{ base: "1rem", md: "1.2rem" }}
fz={{ base: "sm", md: "md" }}
ta="justify"
lh={1.6}
lh={1.7}
dangerouslySetInnerHTML={{ __html: data.programUnggulan }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
style={{ wordBreak: "break-word" }}
/>
</Box>
</Box>
</Stack>
</Paper>
</Box>

View File

@@ -2,7 +2,7 @@
'use client'
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors';
import { Box, Center, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { Box, Center, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
@@ -26,29 +26,32 @@ function SejarahDesa() {
return (
<Box>
<Stack align="center" gap="xl">
{/* HEADER ICON + TITLE */}
<Stack align="center" gap="sm">
<Center>
<Image
src="/darmasaba-icon.png"
alt="Ikon Desa Darmasaba"
w={{ base: 180, md: 260 }}
w={{ base: 160, md: 240 }}
radius="md"
style={{ filter: 'drop-shadow(0 4px 12px rgba(0,0,0,0.15))' }}
loading="lazy"
/>
</Center>
<Center>
<Text
<Title
order={1}
c={colors['blue-button']}
ta="center"
fw={700}
fz={{ base: '2rem', md: '2.8rem' }}
style={{ letterSpacing: '-0.5px' }}
>
Sejarah Desa
</Text>
</Title>
</Center>
</Stack>
{/* CONTENT */}
<Paper
p="xl"
radius="lg"
@@ -61,10 +64,14 @@ function SejarahDesa() {
>
<Stack gap="md">
<Text
fz={{ base: 'md', md: 'lg' }}
lh={1.8}
fz={{ base: 'sm', md: 'md' }}
lh={1.75}
ta="justify"
style={{ color: '#2a2a2a', wordBreak: "break-word", whiteSpace: "normal" }}
c="dark.7"
style={{
wordBreak: 'break-word',
whiteSpace: 'normal',
}}
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
/>
</Stack>

View File

@@ -28,8 +28,10 @@ function SemuaPerbekel() {
<Center py="xl">
<Stack align="center" gap="sm">
<IconUser size={48} stroke={1.5} />
<Title fw="bold" order={2}>Belum ada data Perbekel</Title>
<Text c="dimmed" fz="sm" ta="center">Data mantan Perbekel akan muncul di sini ketika sudah tersedia</Text>
<Title order={2} ta="center">Belum ada data Perbekel</Title>
<Text c="dimmed" fz={{ base: 'xs', md: 'sm' }} lh={{ base: 1.4, md: 1.6 }} ta="center">
Data mantan Perbekel akan muncul di sini ketika sudah tersedia
</Text>
</Stack>
</Center>
);
@@ -38,17 +40,20 @@ function SemuaPerbekel() {
return (
<Box>
<Stack align="center" gap="lg">
<Box>
<Text
ta="center"
fw={900}
fz={{ base: "2rem", md: "2.5rem" }}
variant="gradient"
gradient={{ from: "blue", to: "cyan", deg: 45 }}
>
Perbekel Dari Masa ke Masa
</Text>
</Box>
<Title
order={1}
ta="center"
style={{
background: 'linear-gradient(45deg, blue, cyan)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
}}
fz={{ base: 28, md: 36 }}
lh={{ base: 1.2, md: 1.3 }}
fw={900}
>
Perbekel Dari Masa ke Masa
</Title>
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="xl" w="100%">
{data.map((v: any, k: number) => (
@@ -59,9 +64,7 @@ function SemuaPerbekel() {
withBorder
p="lg"
bg="white"
style={{
transition: "all 250ms ease",
}}
style={{ transition: "all 250ms ease" }}
className="hover:shadow-xl hover:scale-[1.02]"
>
<Stack gap="md" align="center">
@@ -77,17 +80,17 @@ function SemuaPerbekel() {
</Box>
<Stack gap={4} align="center">
<Text fw={700} fz="lg" ta="center">
{v.nama}
</Text>
<Title order={3} fz={{ base: 18, md: 20 }} ta="center" fw={700}>
{v.nama}
</Title>
<Text c="dimmed" fz="sm" ta="center">
{v.daerah}
</Text>
<Text c="dimmed" fz={{ base: 12, md: 14 }} lh={{ base: 1.4, md: 1.6 }} ta="center">
{v.daerah}
</Text>
<Text c="blue" fw={600} fz="sm" ta="center">
{v.periode}
</Text>
<Text c="blue" fw={600} fz={{ base: 12, md: 14 }} lh={{ base: 1.4, md: 1.6 }} ta="center">
{v.periode}
</Text>
</Stack>
</Stack>
</Paper>

View File

@@ -2,7 +2,7 @@
'use client'
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors';
import { Box, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { Box, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
@@ -34,60 +34,57 @@ function VisiMisiDesa() {
loading="lazy"
/>
{/* VISI */}
<Paper
p="xl"
radius="lg"
shadow="md"
withBorder
w="100%"
style={{
background: 'linear-gradient(145deg, #ffffff, #f5f7fa)',
}}
style={{ background: 'linear-gradient(145deg, #ffffff, #f5f7fa)' }}
>
<Text
<Title
order={1}
c={colors['blue-button']}
ta="center"
fw={700}
fz={{ base: '2rem', md: '2.5rem' }}
mb="md"
>
Visi Desa
</Text>
</Title>
<Text
fz={{ base: '1.125rem', md: '1.375rem' }}
fz={{ base: 'sm', md: 'md' }} // body text responsive
lh={1.7}
ta="center"
fw={500}
lh={1.6}
dangerouslySetInnerHTML={{ __html: data.visi }}
style={{wordBreak: "break-word", whiteSpace: "normal"}}
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
/>
</Paper>
{/* MISI */}
<Paper
p="xl"
radius="lg"
shadow="md"
withBorder
w="100%"
style={{
background: 'linear-gradient(145deg, #ffffff, #f5f7fa)',
}}
style={{ background: 'linear-gradient(145deg, #ffffff, #f5f7fa)' }}
>
<Text
<Title
order={1}
c={colors['blue-button']}
ta="center"
fw={700}
fz={{ base: '2rem', md: '2.5rem' }}
mb="md"
>
Misi Desa
</Text>
</Title>
<Text
fz={{ base: '1.125rem', md: '1.375rem' }}
fw={500}
lh={1.6}
fz={{ base: 'sm', md: 'md' }} // body text responsive
lh={1.7}
ta="left"
dangerouslySetInnerHTML={{ __html: data.misi }}
style={{wordBreak: "break-word", whiteSpace: "normal"}}
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
/>
</Paper>
</Stack>

View File

@@ -1,7 +1,7 @@
'use client'
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
import colors from '@/con/colors';
import { Box, Grid, GridCol, Paper, SimpleGrid, Stack, Table, Text, Title } from '@mantine/core';
import { Box, Flex, Group, Paper, SimpleGrid, Stack, Table, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto';
@@ -30,196 +30,265 @@ function Page() {
// Hasil akhir
const sisaAnggaran = totalPendapatan - totalBelanja - totalPembiayaan;
const formatCurrency = (value: number) => {
return new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(value);
};
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="lg">
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Text ta="center" fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw="bold">
{/* Page Title */}
<Title
ta="center"
c={colors["blue-button"]}
fw="bold"
order={1}
fz={{ base: 28, md: 36 }}
>
Pendapatan Asli Desa
</Text>
</Title>
<Box px={{ base: "md", md: 100 }}>
<Stack gap="lg" justify="center">
<Paper bg={colors['white-1']} p="xl">
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="md">
<Paper bg={colors['white-1']} p={{ base: 'md', md: 'xl' }}>
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg">
{/* Pendapatan Card */}
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
<Stack gap={"xs"}>
<Title order={3}>Pendapatan</Title>
{latestApb?.pendapatan?.map((item) => (
<Box key={item.id}>
<Grid>
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text fz="md" fw={500}>{item.name}</Text>
</GridCol>
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Box
p="md"
style={{
border: '1px solid #e9ecef',
borderRadius: '8px',
height: '100%'
}}
>
<Stack gap="md">
<Title order={3} fz={{ base: 18, md: 20 }} c={colors['blue-button']}>
Pendapatan
</Title>
<Stack gap="sm">
{latestApb?.pendapatan?.map((item) => (
<Box key={item.id}>
<Flex gap={1}>
<Text
fz="md"
fz={{ base: 13, md: 14 }}
fw={500}
style={{
wordBreak: 'break-word',
whiteSpace: 'normal',
textAlign: 'right',
}}
lh={1.4}
c="black"
style={{ wordBreak: 'break-word' }}
>
{new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(item.value)}
{item.name} {formatCurrency(item.value)}
</Text>
</GridCol>
</Grid>
</Box>
))}
<Grid>
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text fz="lg" fw={600} mb="xs">Total Pendapatan</Text>
</GridCol>
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text style={{
wordBreak: 'break-word',
whiteSpace: 'normal'
}} fz="xl" fw={700} c={colors['blue-button']}>
{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(totalPendapatan)}
</Flex>
</Box>
))}
</Stack>
<Box
pt="sm"
mt="auto"
style={{
borderTop: `2px solid ${colors['blue-button']}`
}}
>
<Flex direction="column" gap={4}>
<Text fz={{ base: 14, md: 16 }} fw={600} lh={1.4}>
Total Pendapatan
</Text>
</GridCol>
</Grid>
<Text
fz={{ base: 18, md: 22 }}
fw={700}
c={colors['blue-button']}
lh={1.4}
>
{formatCurrency(totalPendapatan)}
</Text>
</Flex>
</Box>
</Stack>
</Box>
{/* Belanja Card */}
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
<Stack gap={"xs"}>
<Title order={3}>Belanja</Title>
{latestApb?.belanja?.map((item) => (
<Box key={item.id}>
<Grid>
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text fz="md" fw={500}>{item.name}</Text>
</GridCol>
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text
fz="md"
<Box
p="md"
style={{
border: '1px solid #e9ecef',
borderRadius: '8px',
height: '100%'
}}
>
<Stack gap="md">
<Title order={3} fz={{ base: 18, md: 20 }} c="orange">
Belanja
</Title>
<Stack gap="sm">
{latestApb?.belanja?.map((item) => (
<Box key={item.id}>
<Group gap={1}>
<Text
fz={{ base: 13, md: 14 }}
fw={500}
style={{
wordBreak: 'break-word',
whiteSpace: 'normal',
textAlign: 'right',
}}
lh={1.4}
c="black"
style={{ wordBreak: 'break-word' }}
>
{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(item.value)}
{item.name} {formatCurrency(item.value)}
</Text>
</GridCol>
</Grid>
</Box>
))}
<Grid>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="lg" fw={600} mb="xs">Total Belanja</Text>
</GridCol>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="xl" fw={700} c="orange">
{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(totalBelanja)}
</Group>
</Box>
))}
</Stack>
<Box
pt="sm"
mt="auto"
style={{
borderTop: '2px solid orange'
}}
>
<Flex direction="column" gap={4}>
<Text fz={{ base: 14, md: 16 }} fw={600} lh={1.4}>
Total Belanja
</Text>
</GridCol>
</Grid>
<Text
fz={{ base: 18, md: 22 }}
fw={700}
c="orange"
lh={1.4}
>
{formatCurrency(totalBelanja)}
</Text>
</Flex>
</Box>
</Stack>
</Box>
{/* Pembiayaan Card */}
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
<Stack gap={"xs"}>
<Title order={3}>Pembiayaan</Title>
{latestApb?.pembiayaan?.map((item) => (
<Box key={item.id}>
<Grid>
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text fz="md" fw={500}>{item.name}</Text>
</GridCol>
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text
fz="md"
<Box
p="md"
style={{
border: '1px solid #e9ecef',
borderRadius: '8px',
height: '100%'
}}
>
<Stack gap="md">
<Title order={3} fz={{ base: 18, md: 20 }} c="green">
Pembiayaan
</Title>
<Stack gap="sm">
{latestApb?.pembiayaan?.map((item) => (
<Box key={item.id}>
<Group gap={1}>
<Text
fz={{ base: 13, md: 14 }}
fw={500}
style={{
wordBreak: 'break-word',
whiteSpace: 'normal',
textAlign: 'right',
}}
lh={1.4}
c="black"
style={{ wordBreak: 'break-word' }}
>
{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(item.value)}
{item.name} {formatCurrency(item.value)}
</Text>
</GridCol>
</Grid>
</Box>
))}
<Grid>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="lg" fw={600} mb="xs">Total Pembiayaan</Text>
</GridCol>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="xl" fw={700} c="green">
{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(totalPembiayaan)}
</Group>
</Box>
))}
</Stack>
<Box
pt="sm"
mt="auto"
style={{
borderTop: '2px solid green'
}}
>
<Flex direction="column" gap={4}>
<Text fz={{ base: 14, md: 16 }} fw={600} lh={1.4}>
Total Pembiayaan
</Text>
</GridCol>
</Grid>
<Text
fz={{ base: 18, md: 22 }}
fw={700}
c="green"
lh={1.4}
>
{formatCurrency(totalPembiayaan)}
</Text>
</Flex>
</Box>
</Stack>
</Box>
</SimpleGrid>
</Paper>
{/* 🔽 Tambahan Ringkasan Anggaran */}
<Paper bg={colors['white-1']} p="xl" shadow="sm" withBorder>
<Title order={3} mb="md">Ringkasan Anggaran</Title>
{/* Ringkasan Anggaran */}
<Paper bg={colors['white-1']} p={{ base: 'md', md: 'xl' }} shadow="sm" withBorder>
<Title order={3} mb="md" fz={{ base: 18, md: 20 }}>
Ringkasan Anggaran
</Title>
<Table striped highlightOnHover withTableBorder>
<Table.Thead>
<Table.Tr>
<Table.Th>Keterangan</Table.Th>
<Table.Th ta={"right"}>Jumlah</Table.Th>
<Table.Th>
<Text fz={{ base: 13, md: 14 }} fw={600}>Keterangan</Text>
</Table.Th>
<Table.Th ta="right">
<Text fz={{ base: 13, md: 14 }} fw={600}>Jumlah</Text>
</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
<Table.Tr>
<Table.Td>Total Pendapatan</Table.Td>
<Table.Td align="right">
{new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(totalPendapatan)}
<Table.Td>
<Text fz={{ base: 13, md: 14 }} lh={1.4}>Total Pendapatan</Text>
</Table.Td>
<Table.Td ta="right">
<Text fz={{ base: 13, md: 14 }} fw={600} lh={1.4}>
{formatCurrency(totalPendapatan)}
</Text>
</Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Td>Total Belanja</Table.Td>
<Table.Td align="right" c="orange">
{new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(totalBelanja)}
<Table.Td>
<Text fz={{ base: 13, md: 14 }} lh={1.4} c="orange">Total Belanja</Text>
</Table.Td>
<Table.Td ta="right">
<Text fz={{ base: 13, md: 14 }} fw={600} lh={1.4} c="orange">
{formatCurrency(totalBelanja)}
</Text>
</Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Td>Total Pembiayaan</Table.Td>
<Table.Td align="right" c="green">
{new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(totalPembiayaan)}
<Table.Td>
<Text fz={{ base: 13, md: 14 }} lh={1.4} c="green">Total Pembiayaan</Text>
</Table.Td>
<Table.Td ta="right">
<Text fz={{ base: 13, md: 14 }} fw={600} lh={1.4} c="green">
{formatCurrency(totalPembiayaan)}
</Text>
</Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Td><b>Sisa Anggaran</b></Table.Td>
<Table.Td align="right" c={sisaAnggaran >= 0 ? "blue" : "red"}>
<b>
{new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(sisaAnggaran)}
</b>
<Table.Tr style={{ backgroundColor: '#f8f9fa' }}>
<Table.Td>
<Text fz={{ base: 14, md: 15 }} fw={700} lh={1.4}>Sisa Anggaran</Text>
</Table.Td>
<Table.Td ta="right">
<Text
fz={{ base: 14, md: 15 }}
fw={700}
c={sisaAnggaran >= 0 ? colors['blue-button'] : "red"}
lh={1.4}
>
{formatCurrency(sisaAnggaran)}
</Text>
</Table.Td>
</Table.Tr>
</Table.Tbody>

Some files were not shown because too many files have changed in this diff Show More