Fix QC Kak Ayu Tgl 12

Fix QC Kak Ino Tgl 12
Fix UI Mobile Menu Keamanan
Fix UI Mobile Admin Menu Landing Page
This commit is contained in:
2025-12-16 10:19:15 +08:00
parent f6f77d9e35
commit 342e9bbc65
43 changed files with 1715 additions and 1344 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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