Merge pull request 'nico/17-des-25' (#43) from nico/17-des-25 into staggingweb

Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/43
This commit is contained in:
2025-12-17 17:39:29 +08:00
44 changed files with 2028 additions and 833 deletions

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch"; import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
@@ -136,12 +137,43 @@ const statepermohonanInformasiPublik = proxy({
}; };
}>[] }>[]
| null, | null,
async load() { 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[ const res = await ApiFetch.api.ppid.permohonaninformasipublik[
"find-many" "find-many"
].get(); ].get({
if (res.status === 200) { query,
statepermohonanInformasiPublik.findMany.data = res.data?.data ?? []; });
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 ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
@@ -58,16 +59,47 @@ const permohonanKeberatanInformasi = proxy({
}, },
findMany: { findMany: {
data: null as data: null as
| null
| Prisma.FormulirPermohonanKeberatanGetPayload<{ | Prisma.FormulirPermohonanKeberatanGetPayload<{
omit: { isActive: true }; omit: { isActive: true };
}>[] }>[],
| null, page: 1,
async load() { 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[ const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik[
"find-many" "find-many"
].get(); ].get({
if (res.status === 200) { query,
permohonanKeberatanInformasi.findMany.data = res.data?.data ?? []; });
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 colors from '@/con/colors';
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; 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 { useParams, useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
@@ -108,7 +108,7 @@ function DetailPerbekelDariMasa() {
radius="md" radius="md"
size="md" size="md"
> >
<IconX size={20} /> <IconTrash size={20} />
</Button> </Button>
<Button <Button

View File

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

View File

@@ -1,7 +1,7 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -28,6 +28,8 @@ function SdgsDesa() {
function ListSdgsDesa({ search }: { search: string }) { function ListSdgsDesa({ search }: { search: string }) {
const listState = useProxy(sdgsDesa); const listState = useProxy(sdgsDesa);
const router = useRouter(); const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { const {
data, data,
@@ -38,8 +40,8 @@ function ListSdgsDesa({ search }: { search: string }) {
} = listState.findMany; } = listState.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
const filteredData = data || []; const filteredData = data || [];
@@ -63,7 +65,7 @@ function ListSdgsDesa({ search }: { search: string }) {
</Title> </Title>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color={colors['blue-button']} color='blue'
variant="light" variant="light"
onClick={() => router.push('/admin/landing-page/SDGs/create')} onClick={() => router.push('/admin/landing-page/SDGs/create')}
> >
@@ -147,12 +149,18 @@ function ListSdgsDesa({ search }: { search: string }) {
{filteredData.map((item) => ( {filteredData.map((item) => (
<Paper key={item.id} withBorder p="md" radius="md"> <Paper key={item.id} withBorder p="md" radius="md">
<Stack gap={4}> <Stack gap={4}>
<Text fz="sm" fw={600} lh={1.4}> <Box>
<Text fz="sm" fw={600} lh={1.4}>Nama SDGs Desa</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.name} {item.name}
</Text> </Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Jumlah</Text>
<Text fz="xs" c="dark.6" lh={1.4}> <Text fz="xs" c="dark.6" lh={1.4}>
Jumlah: {item.jumlah || '0'} {item.jumlah || '0'}
</Text> </Text>
</Box>
<Group justify="flex-end" mt="xs"> <Group justify="flex-end" mt="xs">
<Button <Button
size="xs" size="xs"

View File

@@ -368,6 +368,13 @@ function EditAPBDes() {
{ value: '2', label: 'Level 2 (Sub-kelompok)' }, { value: '2', label: 'Level 2 (Sub-kelompok)' },
{ value: '3', label: 'Level 3 (Detail)' }, { value: '3', label: 'Level 3 (Detail)' },
]} ]}
styles={{
option: {
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
},
}}
value={String(newItem.level)} value={String(newItem.level)}
onChange={(val) => setNewItem({ ...newItem, level: Number(val) || 1 })} onChange={(val) => setNewItem({ ...newItem, level: Number(val) || 1 })}
/> />
@@ -378,6 +385,13 @@ function EditAPBDes() {
{ value: 'belanja', label: 'Belanja' }, { value: 'belanja', label: 'Belanja' },
{ value: 'pembiayaan', label: 'Pembiayaan' }, { value: 'pembiayaan', label: 'Pembiayaan' },
]} ]}
styles={{
option: {
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
},
}}
value={newItem.tipe} value={newItem.tipe}
onChange={(val) => setNewItem({ ...newItem, tipe: (val as any) || 'pendapatan' })} onChange={(val) => setNewItem({ ...newItem, tipe: (val as any) || 'pendapatan' })}
/> />

View File

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

View File

@@ -18,7 +18,7 @@ import {
Text, Text,
Title, Title,
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconFile, IconPlus, IconSearch } from '@tabler/icons-react'; import { IconDeviceImacCog, IconFile, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -45,12 +45,13 @@ function APBDes() {
function ListAPBDes({ search }: { search: string }) { function ListAPBDes({ search }: { search: string }) {
const listState = useProxy(apbdes); const listState = useProxy(apbdes);
const router = useRouter(); const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { data, page, totalPages, loading, load } = listState.findMany; const { data, page, totalPages, loading, load } = listState.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
const filteredData = data || []; const filteredData = data || [];
@@ -197,17 +198,17 @@ function ListAPBDes({ search }: { search: string }) {
<Text fz="sm" fw={600} lh={1.4}> <Text fz="sm" fw={600} lh={1.4}>
APBDes {item.tahun} APBDes {item.tahun}
</Text> </Text>
<Group justify="space-between" wrap="nowrap"> <Box>
<Text fz="sm" c="dimmed" lh={1.4}> <Text fz="sm"fw={600} lh={1.4}>
Tahun Tahun
</Text> </Text>
<Text fz="sm" fw={500} lh={1.4}> <Text fz="sm" fw={500} lh={1.4}>
{item.tahun || '-'} {item.tahun || '-'}
</Text> </Text>
</Group> </Box>
<Group justify="space-between" wrap="nowrap"> <Box>
<Text fz="sm" c="dimmed" lh={1.4}> <Text fz="sm"fw={600} lh={1.4}>
Dokumen Dokumen
</Text> </Text>
{item.file?.link ? ( {item.file?.link ? (
@@ -230,7 +231,7 @@ function ListAPBDes({ search }: { search: string }) {
Tidak ada Tidak ada
</Text> </Text>
)} )}
</Group> </Box>
<Button <Button
size="xs" size="xs"

View File

@@ -151,7 +151,7 @@ function ListKategoriKegiatan({ search }: { search: string }) {
{item.name} {item.name}
</Text> </Text>
</TableTd> </TableTd>
<TableTd w={60}> <TableTd w={120}>
<Button <Button
variant="light" variant="light"
color="green" color="green"
@@ -161,7 +161,7 @@ function ListKategoriKegiatan({ search }: { search: string }) {
<IconEdit size={18} /> <IconEdit size={18} />
</Button> </Button>
</TableTd> </TableTd>
<TableTd w={60}> <TableTd w={120}>
<Button <Button
variant="light" variant="light"
color="red" color="red"
@@ -191,7 +191,7 @@ function ListKategoriKegiatan({ search }: { search: string }) {
); );
return ( return (
<Box py={{ base: 'xl', md: 'xl' }}> <Box py={{ base: 20, md: 20 }}>
<Paper bg={colors['white-1']} p={{ base: 'md', md: 'xl' }} shadow="md" radius="md"> <Paper bg={colors['white-1']} p={{ base: 'md', md: 'xl' }} shadow="md" radius="md">
<Group justify="space-between" mb={{ base: 'md', md: 'lg' }}> <Group justify="space-between" mb={{ base: 'md', md: 'lg' }}>
<Title order={2} lh={1.2}> <Title order={2} lh={1.2}>

View File

@@ -150,12 +150,18 @@ function ListDesaAntiKorupsi({ search }: { search: string }) {
filteredData.map((item) => ( filteredData.map((item) => (
<Paper key={item.id} p="sm" radius="md" withBorder shadow="xs"> <Paper key={item.id} p="sm" radius="md" withBorder shadow="xs">
<Stack gap="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}> <Text fw={500} fz="sm" lh={1.5} lineClamp={1}>
{item.name || '-'} {item.name || '-'}
</Text> </Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Kategori</Text>
<Text fz="xs" c="dimmed" lh={1.5} lineClamp={1}> <Text fz="xs" c="dimmed" lh={1.5} lineClamp={1}>
Kategori: {item.kategori?.name || '-'} {item.kategori?.name || '-'}
</Text> </Text>
</Box>
<Group justify="flex-end"> <Group justify="flex-end">
<Button <Button
size="xs" size="xs"

View File

@@ -1,7 +1,7 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -32,6 +32,7 @@ function ListKategoriPrestasi({ search }: { search: string }) {
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter() const router = useRouter()
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
@@ -50,8 +51,8 @@ function ListKategoriPrestasi({ search }: { search: string }) {
} = stateKategori.findMany } = stateKategori.findMany
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search) load(page, 10, debouncedSearch)
}, [page, search]) }, [page, debouncedSearch])
const filteredData = data || [] const filteredData = data || []

View File

@@ -1,7 +1,7 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { useMediaQuery, useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useMediaQuery, useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -29,6 +29,7 @@ function ListPrestasi({ search }: { search: string }) {
const listState = useProxy(prestasiState.prestasiDesa) const listState = useProxy(prestasiState.prestasiDesa)
const router = useRouter(); const router = useRouter();
const isMobile = useMediaQuery('(max-width: 768px)'); const isMobile = useMediaQuery('(max-width: 768px)');
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const { const {
data, data,
@@ -39,8 +40,8 @@ function ListPrestasi({ search }: { search: string }) {
} = listState.findMany } = listState.findMany
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search); load(page, 10, debouncedSearch);
}, []); }, [page, debouncedSearch]);
const filteredData = data || [] const filteredData = data || []
@@ -71,36 +72,37 @@ function ListPrestasi({ search }: { search: string }) {
</Group> </Group>
{/* Desktop Table */} {/* Desktop Table */}
<Box visibleFrom="md"> <Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table highlightOnHover miw={0}> <Table highlightOnHover striped verticalSpacing="sm" miw={800}>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh w="25%">Nama Prestasi</TableTh> <TableTh>Nama Prestasi</TableTh>
<TableTh w="25%">Deskripsi</TableTh> <TableTh>Deskripsi</TableTh>
<TableTh w="25%">Kategori</TableTh> <TableTh>Kategori</TableTh>
<TableTh w="25%" ta="center">Aksi</TableTh> <TableTh ta="center">Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.length > 0 ? ( {filteredData.length > 0 ? (
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd w="25%"> <TableTd style={{ maxWidth: 250 }}>
<Text truncate="end" fz="md" lh={1.5}> <Text truncate="end" fz="md" lh={1.5}>
{item.name} {item.name}
</Text> </Text>
</TableTd> </TableTd>
<TableTd w="25%"> <TableTd style={{ maxWidth: 250 }}>
<Text lineClamp={1} fz="md" c="dimmed" lh={1.5} dangerouslySetInnerHTML={{ __html: item.deskripsi }} /> <Text lineClamp={1} fz="md" c="dimmed" lh={1.5} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd> </TableTd>
<TableTd w="25%"> <TableTd>
<Text truncate="end" fz="md" lh={1.5}> <Text truncate="end" fz="md" lh={1.5}>
{item.kategori?.name || 'Tidak ada kategori'} {item.kategori?.name || 'Tidak ada kategori'}
</Text> </Text>
</TableTd> </TableTd>
<TableTd w="25%" ta="center"> <TableTd ta="center">
<Center>
<Button <Button
size="xs" size="sm"
radius="md" radius="md"
variant="light" variant="light"
color="blue" color="blue"
@@ -109,6 +111,7 @@ function ListPrestasi({ search }: { search: string }) {
> >
Detail Detail
</Button> </Button>
</Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
)) ))
@@ -130,14 +133,23 @@ function ListPrestasi({ search }: { search: string }) {
{filteredData.length > 0 ? ( {filteredData.length > 0 ? (
filteredData.map((item) => ( filteredData.map((item) => (
<Paper key={item.id} withBorder p="sm" radius="sm"> <Paper key={item.id} withBorder p="sm" radius="sm">
<Stack gap={4}> <Stack gap={"xs"}>
<Text fz="sm" fw={600} lh={1.4}> <Box>
<Text fz="sm" fw={600} lh={1.4}>Nama Prestasi</Text>
<Text fz="sm" fw={500} lh={1.4} lineClamp={2}>
{item.name} {item.name}
</Text> </Text>
<Text fz="xs" c="dimmed" lh={1.5} lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} /> </Box>
<Text fz="xs" c="dimmed" lh={1.4}> <Box>
Kategori: {item.kategori?.name || 'Tidak ada kategori'} <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> </Text>
</Box>
<Group justify="flex-end" mt="xs"> <Group justify="flex-end" mt="xs">
<Button <Button
size="xs" size="xs"

View File

@@ -20,7 +20,7 @@ import {
Text, Text,
Title, Title,
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -48,6 +48,7 @@ function MediaSosial() {
function ListMediaSosial({ search }: { search: string }) { function ListMediaSosial({ search }: { search: string }) {
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial); const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial);
const router = useRouter(); const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const getIconSource = (item: any) => { const getIconSource = (item: any) => {
if (item.image?.link) return item.image.link; if (item.image?.link) return item.image.link;
@@ -66,8 +67,8 @@ function ListMediaSosial({ search }: { search: string }) {
} = stateMediaSosial.findMany; } = stateMediaSosial.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
const filteredData = data || []; const filteredData = data || [];
@@ -195,12 +196,15 @@ function ListMediaSosial({ search }: { search: string }) {
{filteredData.length > 0 ? ( {filteredData.length > 0 ? (
filteredData.map((item) => ( filteredData.map((item) => (
<Paper key={item.id} withBorder p="sm" radius="md"> <Paper key={item.id} withBorder p="sm" radius="md">
<Group justify="space-between" wrap="nowrap" align='center'>
<Box> <Box>
<Text fw={600} fz="sm" lh={1.45}> <Text fz="sm" fw={600} lh={1.4}>Nama Media Sosial / Kontak</Text>
<Text fw={500} fz="sm" lh={1.45}>
{item.name} {item.name}
</Text> </Text>
</Box> </Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>Gambar</Text>
</Box>
<Box w={40} h={40} style={{ borderRadius: 6, overflow: 'hidden' }}> <Box w={40} h={40} style={{ borderRadius: 6, overflow: 'hidden' }}>
{(() => { {(() => {
const src = getIconSource(item); const src = getIconSource(item);
@@ -217,8 +221,8 @@ function ListMediaSosial({ search }: { search: string }) {
return <Box bg={colors['blue-button']} w="100%" h="100%" />; return <Box bg={colors['blue-button']} w="100%" h="100%" />;
})()} })()}
</Box> </Box>
</Group>
<Box> <Box>
<Text fz="sm" fw={600} lh={1.4}>Link / No. Telepon</Text>
<a <a
href={item.link} href={item.link}
target="_blank" target="_blank"

View File

@@ -1,7 +1,7 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core'; 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 { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -29,12 +29,13 @@ function ProgramInovasi() {
function ListProgramInovasi({ search }: { search: string }) { function ListProgramInovasi({ search }: { search: string }) {
const stateProgramInovasi = useProxy(profileLandingPageState.programInovasi); const stateProgramInovasi = useProxy(profileLandingPageState.programInovasi);
const router = useRouter(); const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const { data, page, totalPages, loading, load } = stateProgramInovasi.findMany; const { data, page, totalPages, loading, load } = stateProgramInovasi.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
const filteredData = data || []; const filteredData = data || [];
@@ -135,15 +136,22 @@ function ListProgramInovasi({ search }: { search: string }) {
> >
<Stack gap={6}> <Stack gap={6}>
{/* Title */} {/* Title */}
<Text fw={600}>{item.name}</Text> <Box>
<Text fz="sm" fw={600} lh={1.4}>Nama Program</Text>
<Text fw={500} lh={1.4}>{item.name}</Text>
</Box>
{/* Description */} {/* Description */}
<Box>
<Text fz="sm" fw={600} lh={1.4}>Deskripsi</Text>
<Text fz="sm" c="gray.7" lineClamp={2}> <Text fz="sm" c="gray.7" lineClamp={2}>
{item.description || '-'} {item.description || '-'}
</Text> </Text>
</Box>
{/* Link */} {/* Link */}
<Box> <Box>
<Text fz="sm" fw={600} lh={1.4}>Link</Text>
<a <a
href={item.link} href={item.link}
target="_blank" target="_blank"

View File

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

View File

@@ -52,7 +52,7 @@ function DetailDaftarInformasiPublik() {
<Paper <Paper
withBorder withBorder
w={{ base: '100%', md: '60%' }} w={{ base: '100%', md: '70%' }}
bg={colors['white-1']} bg={colors['white-1']}
p="lg" p="lg"
radius="md" radius="md"
@@ -83,11 +83,11 @@ function DetailDaftarInformasiPublik() {
<Box> <Box>
<Text fz="lg" fw="bold" mb={4}>Deskripsi</Text> <Text fz="lg" fw="bold" mb={4}>Deskripsi</Text>
<Box <Text
px={"xs"}
fz="md" fz="md"
c="dimmed" c="dimmed"
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }} dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
className="prose max-w-none"
style={{ wordBreak: "break-word", whiteSpace: "normal" }} style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/> />
</Box> </Box>

View File

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

View File

@@ -1,7 +1,24 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import {
import { useShallowEffect, useViewportSize } from '@mantine/hooks'; 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 { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -10,7 +27,7 @@ import HeaderSearch from '../../_com/header';
import daftarInformasiPublik from '../../_state/ppid/daftar_informasi_publik/daftarInformasiPublik'; import daftarInformasiPublik from '../../_state/ppid/daftar_informasi_publik/daftarInformasiPublik';
function DaftarInformasiPublik() { function DaftarInformasiPublik() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState('');
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
@@ -26,32 +43,35 @@ function DaftarInformasiPublik() {
} }
function ListDaftarInformasi({ search }: { search: string }) { function ListDaftarInformasi({ search }: { search: string }) {
const listData = useProxy(daftarInformasiPublik) const listData = useProxy(daftarInformasiPublik);
const router = useRouter() const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { data, page, totalPages, loading, load } = listData.findMany const { data, page, totalPages, loading, load } = listData.findMany;
const { width } = useViewportSize() const { width } = useViewportSize();
const isMobile = width < 768 const isMobile = width < 768;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search) load(page, 10, debouncedSearch);
}, [page, search]) }, [page, debouncedSearch]);
const filteredData = data || [] const filteredData = data || [];
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py="md">
<Skeleton height={600} radius="md" /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
) );
} }
return ( return (
<Box py="md"> <Box py={{ base: 'md', md: 'lg' }}>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p={{ base: 'sm', md: 'lg' }} shadow="md" radius="md">
<Group justify="space-between" mb="md"> <Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
<Title order={4}>List Daftar Informasi Publik</Title> <Title order={2} lh={1.2}>
List Daftar Informasi Publik
</Title>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
@@ -65,10 +85,14 @@ function ListDaftarInformasi({ search }: { search: string }) {
{filteredData.length === 0 ? ( {filteredData.length === 0 ? (
<Stack align="center" py="xl"> <Stack align="center" py="xl">
<IconDeviceImacCog size={40} stroke={1.5} color={colors['blue-button']} /> <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> </Stack>
) : ( ) : (
<Box style={{ overflowX: 'auto' }}> <>
{/* Desktop Table */}
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table <Table
highlightOnHover highlightOnHover
striped striped
@@ -77,39 +101,46 @@ function ListDaftarInformasi({ search }: { search: string }) {
> >
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh> <TableTh w="25%">
<TableTh style={{ width: '25%' }}>Jenis Informasi</TableTh> <Text fw={600} lh={1.4}>
<TableTh style={{ width: '40%' }}>Deskripsi</TableTh> Jenis Informasi
<TableTh style={{ width: '20%', textAlign: 'center' }}>Aksi</TableTh> </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> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item, index) => ( {filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd style={{ textAlign: 'center' }}>
<Text fz="sm">{(page - 1) * 10 + index + 1}</Text>
</TableTd>
<TableTd> <TableTd>
<Box w={200}> <Text fz="sm" fw={600} lh={1.5} lineClamp={1}>
<Text fw={500} lineClamp={1}>{item.jenisInformasi}</Text> {item.jenisInformasi}
</Box>
</TableTd>
<TableTd>
<Box w={200}>
<Text lineClamp={1} fz="sm" c="dimmed">
{item.deskripsi?.replace(/<[^>]*>?/gm, '').substring(0, 80) + '...'}
</Text> </Text>
</Box>
</TableTd> </TableTd>
<TableTd style={{ textAlign: 'center' }}> <TableTd>
<Text fz="sm" lh={1.5} c="dimmed" lineClamp={1}>
{item.deskripsi?.replace(/<[^>]*>?/gm, '').substring(0, 80)}...
</Text>
</TableTd>
<TableTd ta="center">
<Button <Button
size="xs" size="xs"
radius="md" radius="md"
variant="light" variant="light"
color="blue" color="blue"
leftSection={<IconDeviceImacCog size={16} />} 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 Detail
</Button> </Button>
@@ -119,9 +150,51 @@ function ListDaftarInformasi({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </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> </Paper>
<Center mt="lg">
<Center mt={{ base: 'lg', md: 'xl' }}>
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => { onChange={(newPage) => {
@@ -129,14 +202,12 @@ function ListDaftarInformasi({ search }: { search: string }) {
window.scrollTo({ top: 0, behavior: 'smooth' }); window.scrollTo({ top: 0, behavior: 'smooth' });
}} }}
total={totalPages} total={totalPages}
mt="md"
mb="md"
color="blue" color="blue"
radius="md" radius="md"
/> />
</Center> </Center>
</Box> </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'; import stateDasarHukumPPID from '../../_state/ppid/dasar_hukum/dasarHukum';
function Page() { function Page() {
const router = useRouter() const router = useRouter();
const listDasarHukum = useProxy(stateDasarHukumPPID) const listDasarHukum = useProxy(stateDasarHukumPPID);
useShallowEffect(() => { useShallowEffect(() => {
listDasarHukum.findById.load('1') listDasarHukum.findById.load('1');
}, []) }, []);
if (listDasarHukum.findById.loading) { if (listDasarHukum.findById.loading) {
return ( return (
@@ -41,6 +41,7 @@ function Page() {
</GridCol> </GridCol>
<GridCol span={{ base: 12, md: 1 }}> <GridCol span={{ base: 12, md: 1 }}>
<Button <Button
w={{ base: '100%', md: "110%" }}
c="green" c="green"
variant="light" variant="light"
leftSection={<IconEdit size={18} stroke={2} />} leftSection={<IconEdit size={18} stroke={2} />}
@@ -57,33 +58,39 @@ function Page() {
<Grid> <Grid>
<GridCol span={12}> <GridCol span={12}>
<Center> <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> </Center>
</GridCol> </GridCol>
<GridCol span={12}> <GridCol span={12}>
<Text <Title
order={3}
ta="center" ta="center"
fz={{ base: '1.5rem', md: '2rem' }} lh={{ base: 1.15, md: 1.1 }}
fw="bold" fw="bold"
c={colors['blue-button']} c={colors['blue-button']}
dangerouslySetInnerHTML={{ __html: listDasarHukum.findById.data.judul }} dangerouslySetInnerHTML={{ __html: listDasarHukum.findById.data.judul }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }} style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
/> />
</GridCol> </GridCol>
</Grid> </Grid>
<Divider my="xl" color={colors['blue-button']} /> <Divider my="xl" color={colors['blue-button']} />
<Box <Text
className="prose max-w-none"
dangerouslySetInnerHTML={{ __html: listDasarHukum.findById.data.content }} 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> </Box>
</Paper> </Paper>
</Stack> </Stack>
</Paper> </Paper>
) );
} }
export default Page; export default Page;

View File

@@ -1,7 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client'; 'use client';
import colors from '@/con/colors'; 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 { IconChartBar, IconUsers } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
@@ -56,6 +56,8 @@ function LayoutTabsIKM({ children }: { children: React.ReactNode }) {
radius="lg" radius="lg"
keepMounted={false} keepMounted={false}
> >
{/* ✅ Scroll horizontal wrapper */}
<Box visibleFrom='md' pb={10}>
<ScrollArea type="auto" offsetScrollbars> <ScrollArea type="auto" offsetScrollbars>
<TabsList <TabsList
p="sm" p="sm"
@@ -69,29 +71,62 @@ function LayoutTabsIKM({ children }: { children: React.ReactNode }) {
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}} }}
> >
{tabs.map((e, i) => ( {tabs.map((tab, i) => (
<Tooltip
key={i}
label={e.description}
withArrow
position="bottom"
transitionProps={{ transition: 'pop', duration: 200 }}
>
<TabsTab <TabsTab
value={e.value} key={i}
leftSection={e.icon} value={tab.value}
leftSection={tab.icon}
style={{ style={{
fontWeight: 500, fontWeight: 600,
fontSize: "0.9rem", fontSize: "0.9rem",
transition: "all 0.2s ease", transition: "all 0.2s ease",
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
}} }}
> >
{e.label} {tab.label}
</TabsTab> </TabsTab>
</Tooltip>
))} ))}
</TabsList> </TabsList>
</ScrollArea> </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) => ( {tabs.map((e, i) => (
<TabsPanel key={i} value={e.value} mt="md"> <TabsPanel key={i} value={e.value} mt="md">
{/* Konten dummy, bisa diganti tergantung routing */} {/* Konten dummy, bisa diganti tergantung routing */}

View File

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

View File

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

View File

@@ -51,7 +51,7 @@ function RespondenCreate() {
} }
} }
return ( return (
<Box> <Box px={{ base: 0, md: 'lg' }} py="xs">
<Box mb={10}> <Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}> <Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack size={20} /> <IconArrowBack size={20} />

View File

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

View File

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

View File

@@ -1,82 +1,182 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors' import colors from '@/con/colors'
import { Box, Button, Group, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core' import {
import { useShallowEffect } from '@mantine/hooks' Box,
import { IconDeviceImacCog, IconId, IconInfoCircle, IconPhone, IconUser } from '@tabler/icons-react' 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 { useRouter } from 'next/navigation'
import { useEffect, useState } from 'react'
import { useProxy } from 'valtio/utils' import { useProxy } from 'valtio/utils'
import statepermohonanInformasiPublikForm from '../../_state/ppid/permohonan_informasi_publik/permohonanInformasiPublik' import statepermohonanInformasiPublikForm from '../../_state/ppid/permohonan_informasi_publik/permohonanInformasiPublik'
import { useDebouncedValue } from '@mantine/hooks'
function Page() { function Page() {
const permohonanInformasiPublikState = useProxy(statepermohonanInformasiPublikForm) const permohonanInformasiPublikState = useProxy(statepermohonanInformasiPublikForm)
const router = useRouter() const router = useRouter()
const { data, page, totalPages, loading, load } = permohonanInformasiPublikState.statepermohonanInformasiPublik.findMany
const [search, setSearch] = useState('')
const [debouncedSearch] = useDebouncedValue(search, 1000);
useShallowEffect(() => { useEffect(() => {
permohonanInformasiPublikState.statepermohonanInformasiPublik.findMany.load() load(page, 10, debouncedSearch);
}, []) }, [page, debouncedSearch]);
if (!permohonanInformasiPublikState.statepermohonanInformasiPublik.findMany.data) { if (loading) {
return ( return (
<Stack pos="relative" bg={colors.Bg} p="lg" align="center"> <Stack pos="relative" p="lg" align="center">
<Skeleton radius="md" h={40} w="60%" />
<Skeleton radius="md" h={200} w="100%" /> <Skeleton radius="md" h={200} w="100%" />
</Stack> </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 ( return (
<Box py="md"> <Box py={{ base: 'sm', md: 'md' }}>
<Paper bg={colors['white-1']} p="lg" radius="xl" shadow="sm" withBorder> <Paper bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} radius="xl" shadow="sm" withBorder>
<Stack gap="md"> <Stack gap={'sm'}>
<Group justify="space-between"> <Grid mb={10}>
<Title order={2} c="dark">Daftar Permohonan Informasi Publik</Title> <GridCol span={{ base: 12, md: 9 }}>
<IconInfoCircle size={20} stroke={1.5} /> <Title order={2} lh={1.2} c="dark">
</Group> 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 ? ( {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']} /> <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> </Stack>
) : ( ) : (
<Box style={{ overflowX: 'auto' }}> <>
<Table {/* Desktop Table */}
highlightOnHover <Box visibleFrom="md">
withRowBorders <Table highlightOnHover>
withColumnBorders
withTableBorder
striped
stickyHeader
>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>No</TableTh> <TableTh fz="sm" fw={600} ta="center" w={60}>
<TableTh><Group gap={5}><IconUser size={16} /> Nama</Group></TableTh> No
<TableTh><Group gap={5}><IconId size={16} /> NIK</Group></TableTh> </TableTh>
<TableTh><Group gap={5}><IconPhone size={16} /> Telepon</Group></TableTh> <TableTh fz="sm" fw={600}>
<TableTh><Group gap={5}><IconInfoCircle size={16} /> Detail</Group></TableTh> <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> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{data.map((item, index) => ( {data.map((item, index) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd>{index + 1}</TableTd> <TableTd ta="center" fz="sm" lh={1.5}>
<TableTd> {index + 1}
<Box w={200}>
<Text lineClamp={1} fw={500}>{item.name}</Text>
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={200}> <Text fz="sm" fw={500} lh={1.5} lineClamp={1}>
{item.nik} {item.name}
</Box> </Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={200}> <Text fz="sm" lh={1.5}>{item.nik}</Text>
{item.notelp} </TableTd>
</Box> <TableTd>
<Text fz="sm" lh={1.5}>{item.notelp}</Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button <Button
@@ -85,7 +185,9 @@ function Page() {
variant="light" variant="light"
color="blue" color="blue"
leftSection={<IconDeviceImacCog size={16} />} leftSection={<IconDeviceImacCog size={16} />}
onClick={() => router.push(`/admin/ppid/permohonan-informasi-publik/${item.id}`)} onClick={() =>
router.push(`/admin/ppid/permohonan-informasi-publik/${item.id}`)
}
> >
Detail Detail
</Button> </Button>
@@ -95,8 +197,78 @@ function Page() {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </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}`)
}
mt={2}
>
Lihat Detail
</Button>
</Box>
</Stack>
</Paper>
))}
</Stack>
</>
)} )}
</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> </Paper>
</Box> </Box>
) )

View File

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

View File

@@ -1,75 +1,185 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors' import colors from '@/con/colors'
import { Box, Button, Group, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core' import {
import { useShallowEffect } from '@mantine/hooks' Box,
import { IconDeviceImacCog, IconInfoCircle, IconMail, IconPhone, IconUser } from '@tabler/icons-react' 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 { useRouter } from 'next/navigation'
import { useProxy } from 'valtio/utils' import { useProxy } from 'valtio/utils'
import statePermohonanKeberatan from '../../_state/ppid/permohonan_keberatan_informasi_publik/permohonanKeberatanInformasi' import statePermohonanKeberatan from '../../_state/ppid/permohonan_keberatan_informasi_publik/permohonanKeberatanInformasi'
import { useEffect, useState } from 'react'
import { useDebouncedValue } from '@mantine/hooks'
function Page() { function Page() {
const listState = useProxy(statePermohonanKeberatan)
const router = useRouter() const router = useRouter()
useShallowEffect(() => { const listState = useProxy(statePermohonanKeberatan)
listState.findMany.load() 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 ( return (
<Stack pos="relative" bg={colors.Bg} p="lg" align="center"> <Stack pos="relative" p="lg" align="center">
<Skeleton radius="md" h={40} w="60%" />
<Skeleton radius="md" h={200} w="100%" /> <Skeleton radius="md" h={200} w="100%" />
</Stack> </Stack>
) )
} }
if (!data || data.length === 0) {
const data = listState.findMany.data 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 ( return (
<Box py="md"> <Box py={{ base: 'md', md: 'lg' }}>
<Paper bg={colors['white-1']} p="lg" radius="xl" shadow="sm" withBorder> <Paper bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} radius="xl" shadow="sm" withBorder>
<Stack gap="md"> <Stack gap={'sm'}>
<Group justify="space-between"> <Grid mb={10}>
<Title order={2} c="dark">Daftar Permohonan Keberatan Informasi Publik</Title> <GridCol span={{ base: 12, md: 9 }}>
<IconInfoCircle size={20} stroke={1.5} /> <Title order={2} lh={1.2} c="dark">
</Group> 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 ? ( {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']} /> <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> </Stack>
) : ( ) : (
<Box style={{ overflowX: 'auto' }}> <>
<Table {/* Desktop Table */}
highlightOnHover <Box visibleFrom="md">
withRowBorders <Table highlightOnHover>
withColumnBorders
withTableBorder
striped
stickyHeader
>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>No</TableTh> <TableTh fz="sm" fw={600} lh={1.4} ta="center">
<TableTh><Group gap={5}><IconUser size={16} /> Nama</Group></TableTh> No
<TableTh><Group gap={5}><IconMail size={16} /> Email</Group></TableTh> </TableTh>
<TableTh><Group gap={5}><IconPhone size={16} /> Telepon</Group></TableTh> <TableTh fz="sm" fw={600} lh={1.4}>
<TableTh><Group gap={5}><IconInfoCircle size={16} /> Detail</Group></TableTh> <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> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{data.map((item, index) => ( {data.map((item, index) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd>{index + 1}</TableTd> <TableTd ta="center" fz="sm" lh={1.5}>
<TableTd> {index + 1}
<Text lineClamp={1} fw={500}>{item.name}</Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Text size="sm">{item.email || '-'}</Text> <Text fz="sm" fw={500} lh={1.5} lineClamp={1}>
{item.name}
</Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Text>{item.notelp || '-'}</Text> <Text fz="sm" lh={1.5}>
{item.email || '-'}
</Text>
</TableTd>
<TableTd>
<Text fz="sm" lh={1.5}>
{item.notelp || '-'}
</Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button <Button
@@ -78,7 +188,11 @@ function Page() {
variant="light" variant="light"
color="blue" color="blue"
leftSection={<IconDeviceImacCog size={16} />} 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}`
)
}
> >
Detail Detail
</Button> </Button>
@@ -88,10 +202,84 @@ function Page() {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </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}`
)
}
mt={4}
>
Detail
</Button>
</Box>
</Stack> </Stack>
</Paper> </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> </Box>
); )
} }
export default Page;
export default Page

View File

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

View File

@@ -1,7 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; 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 { IconBuildingCommunity, IconHierarchy2, IconUsers } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
@@ -63,6 +63,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
keepMounted={false} keepMounted={false}
> >
{/* ✅ Scroll horizontal wrapper */} {/* ✅ Scroll horizontal wrapper */}
<Box visibleFrom='md' pb={10}>
<ScrollArea type="auto" offsetScrollbars> <ScrollArea type="auto" offsetScrollbars>
<TabsList <TabsList
p="sm" p="sm"
@@ -85,6 +86,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
fontWeight: 600, fontWeight: 600,
fontSize: "0.9rem", fontSize: "0.9rem",
transition: "all 0.2s ease", transition: "all 0.2s ease",
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
}} }}
> >
{tab.label} {tab.label}
@@ -92,6 +94,45 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
))} ))}
</TabsList> </TabsList>
</ScrollArea> </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) => ( {tabs.map((tab, i) => (
<TabsPanel <TabsPanel

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -31,6 +31,7 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
const router = useRouter(); const router = useRouter();
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null)
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { const {
data, data,
@@ -41,8 +42,8 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
} = stateOrganisasi.findMany; } = stateOrganisasi.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search); load(page, 10, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
const handleHapus = async () => { const handleHapus = async () => {
if (selectedId) { if (selectedId) {
@@ -56,17 +57,17 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={{ base: 'sm', md: 'md' }}>
<Skeleton height={600} radius="md" /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
); );
} }
return ( return (
<Box py={10}> <Box py={{ base: 'sm', md: 'md' }}>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
<Group justify="space-between" mb="md"> <Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
<Title order={4}>Daftar Posisi Organisasi PPID</Title> <Title order={2}>Daftar Posisi Organisasi PPID</Title>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
@@ -76,33 +77,33 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
Tambah Baru Tambah Baru
</Button> </Button>
</Group> </Group>
<Box style={{ overflowX: 'auto' }}>
{/* Desktop Table */}
<Box visibleFrom="md">
<Table highlightOnHover> <Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ width: '20%' }}>Nama Posisi</TableTh> <TableTh fz="sm" fw={600} lh={1.4}>Nama Posisi</TableTh>
<TableTh style={{ width: '20%' }}>Deskripsi</TableTh> <TableTh fz="sm" fw={600} lh={1.4}>Deskripsi</TableTh>
<TableTh style={{ width: '20%' }}>Hierarki</TableTh> <TableTh fz="sm" fw={600} lh={1.4}>Hierarki</TableTh>
<TableTh style={{ width: '20%' }}>Edit</TableTh> <TableTh fz="sm" fw={600} lh={1.4}>Edit</TableTh>
<TableTh style={{ width: '20%' }}>Hapus</TableTh> <TableTh fz="sm" fw={600} lh={1.4}>Hapus</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.length > 0 ? ( {filteredData.length > 0 ? (
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd style={{ width: '20%' }}> <TableTd>
<Text fw={500} truncate="end" lineClamp={1}>{item.nama}</Text> <Text fz="md" fw={600} lh={1.5} truncate="end" lineClamp={1}>{item.nama}</Text>
</TableTd> </TableTd>
<TableTd style={{ width: '20%' }}> <TableTd w={200}>
<Box w={200}> <Text fz="sm" lh={1.5} c="dimmed" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }} />
<Text lineClamp={1} fz="sm" c="dimmed" dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }} />
</Box>
</TableTd> </TableTd>
<TableTd style={{ width: '20%' }}> <TableTd>
<Text>{item.hierarki || '-'}</Text> <Text fz="md" lh={1.5}>{item.hierarki || '-'}</Text>
</TableTd> </TableTd>
<TableTd style={{ width: '20%' }}> <TableTd>
<Button <Button
variant="light" variant="light"
color="green" color="green"
@@ -112,7 +113,7 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
<IconEdit size={18} /> <IconEdit size={18} />
</Button> </Button>
</TableTd> </TableTd>
<TableTd style={{ width: '20%' }}> <TableTd>
<Button <Button
variant="light" variant="light"
color="red" color="red"
@@ -129,9 +130,11 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
)) ))
) : ( ) : (
<TableTr> <TableTr>
<TableTd colSpan={4}> <TableTd colSpan={5}>
<Center py={20}> <Center py={{ base: 'sm', md: 'md' }}>
<Text color="dimmed">Tidak ada data posisi organisasi yang cocok</Text> <Text fz="sm" c="dimmed" ta="center" lh={1.5}>
Tidak ada data posisi organisasi yang cocok
</Text>
</Center> </Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -139,7 +142,59 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </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> </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> <Center>
<Pagination <Pagination
value={page} value={page}
@@ -154,6 +209,7 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
radius="md" radius="md"
/> />
</Center> </Center>
{/* Modal Hapus */} {/* Modal Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}

View File

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

View File

@@ -7,9 +7,8 @@ import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import stateVisiMisiPPID from '../../_state/ppid/visi_misi_ppid/visimisiPPID'; import stateVisiMisiPPID from '../../_state/ppid/visi_misi_ppid/visimisiPPID';
function VisiMisiPPIDList() { function VisiMisiPPIDList() {
const router = useRouter() const router = useRouter();
const listVisiMisi = useProxy(stateVisiMisiPPID); const listVisiMisi = useProxy(stateVisiMisiPPID);
useShallowEffect(() => { useShallowEffect(() => {
listVisiMisi.findById.load('1'); listVisiMisi.findById.load('1');
@@ -42,6 +41,7 @@ function VisiMisiPPIDList() {
</GridCol> </GridCol>
<GridCol span={{ base: 12, md: 1 }}> <GridCol span={{ base: 12, md: 1 }}>
<Button <Button
w={{ base: '100%', md: "110%" }}
c="green" c="green"
variant="light" variant="light"
leftSection={<IconEdit size={18} stroke={2} />} leftSection={<IconEdit size={18} stroke={2} />}
@@ -58,14 +58,25 @@ function VisiMisiPPIDList() {
<Grid> <Grid>
<GridCol span={12}> <GridCol span={12}>
<Center> <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> </Center>
</GridCol> </GridCol>
<GridCol span={12}> <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 MOTO PPID DESA DARMASABA
</Text> </Title>
<Text ta="center" fz={{ base: '1rem', md: '1.2rem' }} mt="sm"> <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 MEMBERIKAN INFORMASI YANG CEPAT, MUDAH, TEPAT DAN TRANSPARAN
</Text> </Text>
</GridCol> </GridCol>
@@ -74,26 +85,50 @@ function VisiMisiPPIDList() {
<Divider my="xl" color={colors['blue-button']} /> <Divider my="xl" color={colors['blue-button']} />
<Box> <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 VISI PPID
</Text> </Title>
<Box <Text
className="prose max-w-none" ta={{ base: "center", md: "justify" }}
dangerouslySetInnerHTML={{ __html: listVisiMisi.findById.data.visi }} 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> </Box>
<Divider my="xl" color={colors['blue-button']} /> <Divider my="xl" color={colors['blue-button']} />
<Box mt="xl"> <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 MISI PPID
</Text> </Title>
<Box <Text
className="prose max-w-none" ta={"justify"}
dangerouslySetInnerHTML={{ __html: listVisiMisi.findById.data.misi }} 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>
</Box> </Box>

View File

@@ -1,14 +1,56 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function permohonanInformasiPublikFindMany() { export default async function permohonanInformasiPublikFindMany(
const res = await prisma.permohonanInformasiPublik.findMany({ 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: { include: {
jenisInformasiDiminta: true, jenisInformasiDiminta: true,
caraMemperolehInformasi: true, caraMemperolehInformasi: true,
caraMemperolehSalinanInformasi: true, caraMemperolehSalinanInformasi: true,
} },
}); take: limit,
orderBy: { name: "asc" }, // opsional, kalau mau urut berdasarkan waktu
}),
prisma.permohonanInformasiPublik.count({
where: { isActive: true },
}),
]);
return { return {
data: res, 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 prisma from "@/lib/prisma";
import { Context } from "elysia";
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 },
}),
]);
export default async function permohonanKeberatanInformasiPublikFindMany() {
const res = await prisma.formulirPermohonanKeberatan.findMany();
return { return {
data: res, 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

@@ -216,6 +216,8 @@ export default function ModernNewsNotification({
{/* Widget Panel */} {/* Widget Panel */}
<Transition mounted={widgetOpen} transition="slide-up" duration={300}> <Transition mounted={widgetOpen} transition="slide-up" duration={300}>
{(styles) => ( {(styles) => (
<Box>
<Box hiddenFrom="md">
<Paper <Paper
style={{ style={{
...styles, ...styles,
@@ -308,10 +310,108 @@ export default function ModernNewsNotification({
)} )}
</Box> </Box>
</Paper> </Paper>
</Box>
<Box visibleFrom="md">
<Paper
style={{
...styles,
position: "fixed",
bottom: "100px",
right: "24px",
width: "90vw",
maxWidth: 380,
maxHeight: "500px",
boxShadow: "0 8px 32px rgba(0,0,0,0.12)",
borderRadius: "16px",
overflow: "hidden",
}}
>
<Box
style={{
background: "linear-gradient(135deg, #1e5a7e 0%, #2c7da0 100%)",
padding: "16px 20px",
color: "white",
}}
>
<Group justify="space-between">
<Group gap="xs">
<IconBell size={20} />
<Text c="white" fw={600} size="md">
Berita & Pengumuman
</Text>
</Group>
<CloseButton
onClick={() => {
setWidgetOpen(false);
onSeen?.();
}}
variant="transparent"
c="white"
/>
</Group>
</Box>
<Box style={{ maxHeight: "400px", overflowY: "auto", padding: "12px" }}>
{news.length === 0 ? (
<Box p="xl" style={{ textAlign: "center" }}>
<Text c="dimmed" size="sm">
Tidak ada berita terbaru
</Text>
</Box>
) : (
<Stack gap="xs">
{news.map((item) => (
<Paper
key={item.id}
p="md"
radius="md"
style={{
border: "1px solid #e9ecef",
cursor: "pointer",
transition: "all 0.2s ease",
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = "#1e5a7e";
e.currentTarget.style.transform = "translateY(-2px)";
e.currentTarget.style.boxShadow = "0 4px 12px rgba(0,0,0,0.08)";
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = "#e9ecef";
e.currentTarget.style.transform = "translateY(0)";
e.currentTarget.style.boxShadow = "none";
}}
onClick={() => handleNotificationClick(item)}
>
<Group justify="space-between" mb="xs">
<Badge
size="sm"
color={item.type === "berita" ? "blue" : "orange"}
variant="light"
>
{item.type === "berita" ? "Berita" : "Pengumuman"}
</Badge>
<IconChevronRight size={16} color="#adb5bd" />
</Group>
<Text fw={600} size="sm" mb={4} lineClamp={2}>
{item.title || "Tanpa Judul"}
</Text>
<Text size="xs" c="dimmed" lineClamp={2}>
{stripHtml(item.content).substring(0, 100)}...
</Text>
</Paper>
))}
</Stack>
)}
</Box>
</Paper>
</Box>
</Box>
)} )}
</Transition> </Transition>
{/* Toast Notification */} {/* Toast Notification */}
<Box>
<Box hiddenFrom="md">
<Transition <Transition
mounted={toastVisible && !!currentNews} mounted={toastVisible && !!currentNews}
transition="slide-left" transition="slide-left"
@@ -387,6 +487,85 @@ export default function ModernNewsNotification({
</Paper> </Paper>
)} )}
</Transition> </Transition>
</Box>
<Box visibleFrom="md">
<Transition
mounted={toastVisible && !!currentNews}
transition="slide-right"
duration={300}
>
{(styles) => (
<Paper
style={{
...styles,
position: "fixed",
bottom: "100px",
right: "24px",
width: "90vw",
maxWidth: 380,
boxShadow: "0 8px 32px rgba(0,0,0,0.15)",
borderRadius: "12px",
overflow: "hidden",
border: "1px solid #e9ecef",
}}
onClick={handleLihatSemua}
>
<Box
style={{
height: "3px",
background: "#1e5a7e",
animation: "shrink 8s linear forwards",
}}
/>
<style>{`
@keyframes shrink {
from { width: 100%; }
to { width: 0%; }
}
`}</style>
<Box p="md">
<Group justify="space-between" mb="xs">
<Badge
size="md"
color={currentNews?.type === "berita" ? "blue" : "orange"}
variant="light"
>
{currentNews?.type === "berita"
? "Berita Terbaru"
: "Pengumuman"}
</Badge>
<CloseButton onClick={handleDismissToast} size="sm" />
</Group>
<Text fw={600} size="sm" mb={6}>
{currentNews?.title || "Informasi Terbaru"}
</Text>
<Text size="xs" c="dimmed" lineClamp={3}>
{stripHtml(currentNews?.content || "")}
</Text>
<Group justify="space-between" mt="md">
<Text size="xs" c="dimmed">
{news.length > 1 ? `${news.length} berita tersedia` : "1 berita"}
</Text>
<Text
size="xs"
fw={500}
c="#1e5a7e"
style={{ cursor: "pointer" }}
onClick={handleLihatSemua}
>
Lihat Semua
</Text>
</Group>
</Box>
</Paper>
)}
</Transition>
</Box>
</Box>
</> </>
); );
} }

View File

@@ -3,18 +3,17 @@
import colors from "@/con/colors" import colors from "@/con/colors"
import stateNav from "@/state/state-nav" import stateNav from "@/state/state-nav"
import { ActionIcon, Button, Container, Flex, Image, Menu, MenuTarget, Stack, Tooltip } from "@mantine/core" import { ActionIcon, Button, Container, Flex, Image, Menu, MenuTarget, Stack, Tooltip } from "@mantine/core"
import { IconSearch, IconUser } from "@tabler/icons-react" import { IconSearch, IconUserCog } from "@tabler/icons-react"
import { useTransitionRouter } from 'next-view-transitions' import { useTransitionRouter } from 'next-view-transitions'
import { usePathname, useRouter } from "next/navigation" import { usePathname, useRouter } from "next/navigation"
import { useSnapshot } from "valtio" import { useSnapshot } from "valtio"
import { MenuItem } from "../../../../types/menu-item" import { MenuItem } from "../../../../types/menu-item"
import { NavbarSearch } from "./NavBarSearch" import { NavbarSearch } from "./NavBarSearch"
import { NavbarSubMenu } from "./NavbarSubMenu" import { NavbarSubMenu } from "./NavbarSubMenu"
import { authStore } from "@/store/authStore";
// contoh state auth (dummy aja dulu, bisa diganti sesuai sistem auth kamu) // contoh state auth (dummy aja dulu, bisa diganti sesuai sistem auth kamu)
const stateAuth = { const isAdmin = authStore.user?.roleId === 0 || authStore.user?.roleId === 1 || authStore.user?.roleId === 2 || authStore.user?.roleId === 3 || authStore.user?.roleId === 4;
role: "admin", // coba ubah ke "user" buat test
}
export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) { export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
const { item, isSearch } = useSnapshot(stateNav) const { item, isSearch } = useSnapshot(stateNav)
@@ -70,8 +69,8 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
</Tooltip> </Tooltip>
{/* hanya tampil kalau role = admin */} {/* hanya tampil kalau role = admin */}
{stateAuth.role === "admin" && ( {isAdmin && (
<Tooltip label="Profil Saya" position="bottom" withArrow> <Tooltip label="Kembali ke Admin" position="bottom" withArrow>
<ActionIcon <ActionIcon
onClick={() => { onClick={() => {
next.push("/admin/landing-page/profil/program-inovasi") next.push("/admin/landing-page/profil/program-inovasi")
@@ -80,7 +79,7 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
radius="xl" radius="xl"
variant="light" variant="light"
> >
<IconUser size={22} /> <IconUserCog size={22} />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
)} )}