Fix UI User Menu Ekonomi & Fix UI Submenu Profile, Desa Anti Korupsi

This commit is contained in:
2025-08-28 11:44:03 +08:00
parent f9530c32eb
commit a8fd715822
27 changed files with 2404 additions and 1885 deletions

View File

@@ -1,60 +1,101 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { IconList, IconCategory } from '@tabler/icons-react';
function LayoutTabs({ children }: { children: React.ReactNode }) { function LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter() const router = useRouter();
const pathname = usePathname() const pathname = usePathname();
const tabs = [ const tabs = [
{ {
label: "List Desa Anti Korupsi", label: "List Desa Anti Korupsi",
value: "listDesaAntiKorupsi", value: "listDesaAntiKorupsi",
href: "/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi" href: "/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi",
icon: <IconList size={18} stroke={1.8} />,
tooltip: "Kelola daftar program desa anti korupsi",
}, },
{ {
label: "Kategori Desa Anti Korupsi", label: "Kategori Desa Anti Korupsi",
value: "kategoriDesaAntiKorupsi", value: "kategoriDesaAntiKorupsi",
href: "/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi" href: "/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi",
icon: <IconCategory size={18} stroke={1.8} />,
tooltip: "Kelola kategori desa anti korupsi",
}, },
]; ];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value); const currentTab = tabs.find(tab => tab.href === pathname);
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => { const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value) const tab = tabs.find(t => t.value === value);
if (tab) { if (tab) {
router.push(tab.href) router.push(tab.href);
} }
setActiveTab(value) setActiveTab(value)
} }
useEffect(() => { useEffect(() => {
const match = tabs.find(tab => tab.href === pathname) const match = tabs.find(tab => tab.href === pathname);
if (match) { if (match) {
setActiveTab(match.value) setActiveTab(match.value);
} }
}, [pathname]) }, [pathname]);
return ( return (
<Stack> <Stack gap="lg">
<Title order={3}>Desa Anti Korupsi</Title> <Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}> Desa Anti Korupsi
<TabsList p={"xs"} bg={"#BBC8E7FF"}> </Title>
{tabs.map((e, i) => ( <Tabs
<TabsTab key={i} value={e.value}>{e.label}</TabsTab> variant="pills"
value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
}}
>
{tabs.map((tab, i) => (
<Tooltip key={i} label={tab.tooltip} position="bottom" withArrow>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))} ))}
</TabsList> </TabsList>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value}> {tabs.map((tab, i) => (
{/* Konten dummy, bisa diganti tergantung routing */} <TabsPanel
<></> key={i}
value={tab.value}
style={{
padding: "1.5rem",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
borderRadius: "1rem",
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
}}
>
{children}
</TabsPanel> </TabsPanel>
))} ))}
</Tabs> </Tabs>
{children}
</Stack> </Stack>
); );
} }

View File

@@ -2,14 +2,14 @@
'use client' 'use client'
import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi'; import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Paper, Stack, TextInput, Title, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function EditKategoriDesaAntiKorupsi() { export default function EditKategoriDesaAntiKorupsi() {
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
const id = params?.id as string; const id = params?.id as string;
@@ -18,16 +18,17 @@ function EditKategoriDesaAntiKorupsi() {
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: "", name: "",
}); });
const [isLoading, setIsLoading] = useState(false);
useEffect(() => { useEffect(() => {
const loadKategorikegiatan = async () => { const loadKategori = async () => {
if (!id) return; if (!id) return;
setIsLoading(true);
try { try {
const data = await stateKategori.edit.load(id); const data = await stateKategori.edit.load(id);
if (data) { if (data) {
// pastikan id-nya masuk ke state edit
stateKategori.edit.id = id; stateKategori.edit.id = id;
setFormData({ setFormData({
name: data.name || '', name: data.name || '',
@@ -36,63 +37,88 @@ function EditKategoriDesaAntiKorupsi() {
} catch (error) { } catch (error) {
console.error("Error loading kategori desa anti korupsi:", error); console.error("Error loading kategori desa anti korupsi:", error);
toast.error("Gagal memuat data kategori desa anti korupsi"); toast.error("Gagal memuat data kategori desa anti korupsi");
} finally {
setIsLoading(false);
} }
}; };
loadKategorikegiatan(); loadKategori();
}, [id]); }, [id]);
const handleSubmit = async () => { const handleSubmit = async () => {
try {
if (!formData.name.trim()) { if (!formData.name.trim()) {
toast.error('Nama kategori desa anti korupsi tidak boleh kosong'); return toast.error('Nama kategori tidak boleh kosong');
return;
} }
try {
setIsLoading(true);
stateKategori.edit.form = { stateKategori.edit.form = {
name: formData.name.trim(), name: formData.name.trim(),
}; };
// Safety check tambahan: pastikan ID tidak kosong
if (!stateKategori.edit.id) { if (!stateKategori.edit.id) {
stateKategori.edit.id = id; // fallback stateKategori.edit.id = id;
} }
const success = await stateKategori.edit.update(); await stateKategori.edit.update();
toast.success('Kategori berhasil diperbarui');
if (success) {
router.push("/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi"); router.push("/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi");
}
} catch (error) { } catch (error) {
console.error("Error updating kategori desa anti korupsi:", error); console.error("Error updating kategori desa anti korupsi:", error);
// toast akan ditampilkan dari fungsi update toast.error(error instanceof Error ? error.message : 'Gagal memperbarui kategori');
} finally {
setIsLoading(false);
} }
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> <Group mb="md">
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Tooltip label="Kembali ke halaman sebelumnya" color="blue" withArrow>
<IconArrowBack color={colors['blue-button']} size={25} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
</Box> </Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Kategori Desa Anti Korupsi
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Paper
<Stack gap={"xs"}> w={{ base: '100%', md: '50%' }}
<Title order={4}>Edit Kategori Desa Anti Korupsi</Title> bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label="Nama Kategori"
placeholder="Masukkan nama kategori"
value={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Desa Anti Korupsi</Text>} required
placeholder='Masukkan nama kategori desa anti korupsi' disabled={isLoading}
/> />
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button> <Group justify="right" mt="md">
<Button
onClick={handleSubmit}
radius="md"
size="md"
loading={isLoading}
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>
); );
} }
export default EditKategoriDesaAntiKorupsi;

View File

@@ -1,16 +1,16 @@
/* 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 { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Paper, Stack, TextInput, Title, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import korupsiState from '../../../../_state/landing-page/desa-anti-korupsi'; import korupsiState from '../../../../_state/landing-page/desa-anti-korupsi';
function CreateKategoriDesaAntiKorupsi() { export default function CreateKategoriDesaAntiKorupsi() {
const router = useRouter(); const router = useRouter();
const stateKategori = useProxy(korupsiState.kategoriDesaAntiKorupsi) const stateKategori = useProxy(korupsiState.kategoriDesaAntiKorupsi);
useEffect(() => { useEffect(() => {
stateKategori.findMany.load(); stateKategori.findMany.load();
@@ -20,42 +20,64 @@ function CreateKategoriDesaAntiKorupsi() {
stateKategori.create.form = { stateKategori.create.form = {
name: "", name: "",
}; };
} };
const handleSubmit = async () => { const handleSubmit = async () => {
await stateKategori.create.create(); if (!stateKategori.create.form.name) {
resetForm(); return alert('Nama kategori harus diisi');
router.push("/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi")
} }
return ( await stateKategori.create.create();
<Box> resetForm();
<Box> router.push("/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi");
<Box mb={10}> };
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> return (
<Stack gap={"xs"}> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Title order={4}>Create Kategori Desa Anti Korupsi</Title> <Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" color="blue" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Kategori Desa Anti Korupsi
</Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
value={stateKategori.create.form.name} label="Nama Kategori"
onChange={(val) => { placeholder="Masukkan nama kategori"
stateKategori.create.form.name = val.target.value; value={stateKategori.create.form.name || ''}
}} onChange={(e) => (stateKategori.create.form.name = e.target.value)}
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Desa Anti Korupsi</Text>} required
placeholder='Masukkan nama kategori desa anti korupsi'
/> />
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button> <Group justify="right" mt="md">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>
</Box>
); );
} }
export default CreateKategoriDesaAntiKorupsi;

View File

@@ -1,13 +1,12 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } 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 { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconX } 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';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import korupsiState from '../../../_state/landing-page/desa-anti-korupsi'; import korupsiState from '../../../_state/landing-page/desa-anti-korupsi';
@@ -56,74 +55,84 @@ function ListKategoriKegiatan({ search }: { search: string }) {
const filteredData = data || [] const filteredData = data || []
// Handle loading state
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton height={550} /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
); );
} }
if (data.length === 0) {
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<JudulList <Group justify="space-between" mb="md">
title='List Kategori Kegiatan' <Title order={4}>Daftar Kategori Kegiatan</Title>
href='/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/create' <Tooltip label="Tambah Kategori" withArrow>
/> <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>
</Tooltip>
</Group>
<Box style={{ overflowX: "auto" }}> <Box style={{ overflowX: "auto" }}>
<Table striped withTableBorder withRowBorders> <Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
</Table>
</Box>
</Paper>
</Box>
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Kategori Kegiatan'
href='/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/create'
/>
<Box style={{ overflowY: "auto" }}>
<Table striped withTableBorder withRowBorders>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Nama Kategori</TableTh> <TableTh>Nama Kategori</TableTh>
<TableTh>Edit</TableTh> <TableTh>Edit</TableTh>
<TableTh>Delete</TableTh> <TableTh>Hapus</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item) => ( {filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd> <TableTd>
<Button color="green" onClick={() => router.push(`/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/${item.id}`)}> <Text fw={500}>{item.name}</Text>
<IconEdit size={20} />
</Button>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button color="red" onClick={() => { <Tooltip label="Edit" withArrow>
setSelectedId(item.id) <Button
setModalHapus(true) variant="light"
}}> color="blue"
<IconX size={20} /> size="sm"
onClick={() => router.push(`/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/${item.id}`)}
>
<IconEdit size={18} />
</Button> </Button>
</Tooltip>
</TableTd>
<TableTd>
<Tooltip label="Hapus" withArrow>
<Button
variant="light"
color="red"
size="sm"
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={18} />
</Button>
</Tooltip>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} ))
) : (
<TableTr>
<TableTd colSpan={2}>
<Center py={20}>
<Text c="dimmed">Tidak ada data kategori yang ditemukan</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
@@ -133,11 +142,13 @@ function ListKategoriKegiatan({ search }: { search: string }) {
value={page} value={page}
onChange={(newPage) => { onChange={(newPage) => {
load(newPage, 10); load(newPage, 10);
window.scrollTo(0, 0); window.scrollTo({ top: 0, behavior: 'smooth' });
}} }}
total={totalPages} total={totalPages}
mt="md" mt="md"
mb="md" mb="md"
color="blue"
radius="md"
/> />
</Center> </Center>
{/* Modal Konfirmasi Hapus */} {/* Modal Konfirmasi Hapus */}

View File

@@ -1,18 +1,16 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client';
import { useProxy } from 'valtio/utils'; import { Box, Button, Group, Paper, Select, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { Box, Button, Group, Paper, Select, Stack, Text, TextInput } from '@mantine/core';
import { IconArrowBack, IconFile, IconUpload, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconFile, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import colors from '@/con/colors'; import colors from '@/con/colors';
import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi'; import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import { toast } from 'react-toastify';
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
interface FormDesaAntiKorupsi { interface FormDesaAntiKorupsi {
@@ -22,18 +20,20 @@ interface FormDesaAntiKorupsi {
fileId: string; fileId: string;
} }
function EditDesaAntiKorupsi() { export default function EditDesaAntiKorupsi() {
const desaAntiKorupsiState = useProxy(korupsiState.desaAntikorupsi) const desaAntiKorupsiState = useProxy(korupsiState.desaAntikorupsi);
const [previewFile, setPreviewFile] = useState<string | null>(null); const [previewFile, setPreviewFile] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const params = useParams() const [isLoading, setIsLoading] = useState(false);
const router = useRouter() const params = useParams();
const router = useRouter();
const [formData, setFormData] = useState<FormDesaAntiKorupsi>({ const [formData, setFormData] = useState<FormDesaAntiKorupsi>({
name: '', name: '',
deskripsi: '', deskripsi: '',
kategoriId: '', kategoriId: '',
fileId: '', fileId: '',
}) });
useEffect(() => { useEffect(() => {
const loadDesaAntiKorupsi = async () => { const loadDesaAntiKorupsi = async () => {
@@ -43,7 +43,6 @@ function EditDesaAntiKorupsi() {
try { try {
const data = await desaAntiKorupsiState.edit.load(id); const data = await desaAntiKorupsiState.edit.load(id);
if (data) { if (data) {
// ⬇️ FIX PENTING: tambahkan ini
desaAntiKorupsiState.edit.id = id; desaAntiKorupsiState.edit.id = id;
desaAntiKorupsiState.edit.form = { desaAntiKorupsiState.edit.form = {
@@ -61,169 +60,198 @@ function EditDesaAntiKorupsi() {
}); });
if (data?.file?.link) { if (data?.file?.link) {
setPreviewFile(data.file.link) setPreviewFile(data.file.link);
} }
} }
} catch (error) { } catch (error) {
console.error("Error loading program penghijauan:", error); console.error('Error loading data:', error);
toast.error("Gagal memuat data program penghijauan"); toast.error('Gagal memuat data Desa Anti Korupsi');
}
} }
};
loadDesaAntiKorupsi(); loadDesaAntiKorupsi();
}, [params?.id]); }, [params?.id]);
const handleSubmit = async () => { const handleSubmit = async () => {
if (!formData.name) {
return toast.warn('Masukkan judul dokumen');
}
if (!formData.kategoriId) {
return toast.warn('Pilih kategori dokumen');
}
setIsLoading(true);
try { try {
// Update global state with form data // Update global state with form data
desaAntiKorupsiState.edit.form = { desaAntiKorupsiState.edit.form = {
...desaAntiKorupsiState.edit.form, ...desaAntiKorupsiState.edit.form,
name: formData.name, ...formData,
deskripsi: formData.deskripsi,
kategoriId: formData.kategoriId || '', kategoriId: formData.kategoriId || '',
fileId: formData.fileId // Keep existing imageId if not changed
}; };
// Jika ada file baru, upload // Upload new file if exists
if (file) { if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name }); const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name
});
const uploaded = res.data?.data; const uploaded = res.data?.data;
if (!uploaded?.id) { if (!uploaded?.id) {
return toast.error("Gagal upload gambar"); throw new Error('Gagal mengunggah dokumen');
} }
// Update imageId in global state
desaAntiKorupsiState.edit.form.fileId = uploaded.id; desaAntiKorupsiState.edit.form.fileId = uploaded.id;
} }
await desaAntiKorupsiState.edit.update(); await desaAntiKorupsiState.edit.update();
toast.success("desa anti korupsi berhasil diperbarui!"); toast.success('Data berhasil diperbarui');
router.push("/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi"); router.push('/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi');
} catch (error) { } catch (error) {
console.error("Error updating desa anti korupsi:", error); console.error('Error updating data:', error);
toast.error("Terjadi kesalahan saat memperbarui desa anti korupsi"); toast.error('Terjadi kesalahan saat memperbarui data');
} finally {
setIsLoading(false);
} }
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()}> <Tooltip label="Kembali ke halaman sebelumnya" color="blue" withArrow>
<IconArrowBack color={colors['blue-button']} size={25} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
</Box> </Tooltip>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Title order={4} ml="sm" c="dark">
<Stack> Edit Desa Anti Korupsi
<Text fz={"xl"} fw={"bold"}>Edit List Desa Anti Korupsi</Text> </Title>
{desaAntiKorupsiState.findUnique.data ? ( </Group>
<Paper key={desaAntiKorupsiState.findUnique.data.id}>
<Stack gap={"xs"}> <Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label="Judul Dokumen"
placeholder="Masukkan judul dokumen"
value={formData.name} value={formData.name}
onChange={(val) => { onChange={(e) => setFormData({ ...formData, name: e.target.value })}
setFormData({ required
...formData,
name: val.target.value
})
}}
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>}
placeholder='Masukkan judul'
/> />
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text> <Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<EditEditor <EditEditor
value={formData.deskripsi} value={formData.deskripsi}
onChange={(val) => { onChange={(val) => setFormData({ ...formData, deskripsi: val })}
setFormData({
...formData,
deskripsi: val
})
}}
/> />
</Box> </Box>
<Select <Select
value={formData.kategoriId} label="Kategori"
onChange={(val) => {
setFormData({
...formData,
kategoriId: val ?? ""
})
}}
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>}
placeholder="Pilih kategori" placeholder="Pilih kategori"
value={formData.kategoriId}
onChange={(val) => setFormData({ ...formData, kategoriId: val || '' })}
data={ data={
korupsiState.kategoriDesaAntiKorupsi.findMany.data?.map((v) => ({ korupsiState.kategoriDesaAntiKorupsi.findMany.data?.map((v) => ({
value: v.id, value: v.id,
label: v.name, label: v.name,
})) || [] })) || []
} }
required
searchable
clearable
/> />
<Box> <Box>
<Text fz={"md"} fw={"bold"}>File Document</Text> <Text fw="bold" fz="sm" mb={6}>
<Box> Dokumen
</Text>
<Dropzone <Dropzone
onDrop={(files) => { onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama const selectedFile = files[0];
if (selectedFile) { if (selectedFile) {
setFile(selectedFile); setFile(selectedFile);
setPreviewFile(URL.createObjectURL(selectedFile)); // Buat preview setPreviewFile(URL.createObjectURL(selectedFile));
} }
}} }}
onReject={() => toast.error('File tidak valid.')} onReject={() => toast.error('File tidak valid, gunakan format dokumen')}
maxSize={5 * 1024 ** 2} // Maks 5MB maxSize={5 * 1024 ** 2}
accept={{ accept={{
'application/*': ['.pdf', '.doc', '.docx'], 'application/*': ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx'],
}} }}
radius="md"
> >
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}> <Group justify="center" gap="xl" mih={180} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept> <Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /> <IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept> </Dropzone.Accept>
<Dropzone.Reject> <Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} /> <IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject> </Dropzone.Reject>
<Dropzone.Idle> <Dropzone.Idle>
<IconFile size={52} color="var(--mantine-color-dimmed)" stroke={1.5} /> <IconFile size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle> </Dropzone.Idle>
<div> <div>
<Text size="xl" inline> <Text size="lg" inline>
Drag file ke sini atau klik untuk pilih file Seret dokumen ke sini atau klik untuk memilih file
</Text> </Text>
<Text size="sm" c="dimmed" inline mt={7}> <Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format document Maksimal 5MB (PDF, DOC, DOCX, XLS, XLSX, PPT, PPTX)
</Text> </Text>
</div> </div>
</Group> </Group>
</Dropzone> </Dropzone>
<Box>
<Text fw={"bold"} fz={"lg"}>Dokumen</Text> {previewFile && (
{previewFile ? ( <Box mt="md">
<Text fw="bold" fz="sm" mb={6}>
Pratinjau Dokumen
</Text>
<Box
style={{
border: '1px solid #e0e0e0',
borderRadius: '8px',
overflow: 'hidden',
height: '500px',
width: '100%',
}}
>
<iframe <iframe
src={previewFile} src={previewFile}
width="100%" width="100%"
height="500px" height="100%"
style={{ border: "1px solid #ccc", borderRadius: "8px" }} style={{ border: 'none' }}
/> />
) : ( </Box>
<Text>Tidak ada dokumen tersedia</Text> </Box>
)} )}
</Box> </Box>
</Box>
</Box> <Group justify="right" mt="xl">
<Group> <Button
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button> onClick={handleSubmit}
radius="md"
size="md"
loading={isLoading}
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>
) : null}
</Stack>
</Paper>
</Box> </Box>
); );
} }
export default EditDesaAntiKorupsi;

View File

@@ -2,15 +2,15 @@
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi'; import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } 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';
function DetailKegiatanDesa() { export default function DetailKegiatanDesa() {
const detailState = useProxy(korupsiState.desaAntikorupsi) const detailState = useProxy(korupsiState.desaAntikorupsi)
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null)
@@ -34,89 +34,122 @@ function DetailKegiatanDesa() {
if (!detailState.findUnique.data) { if (!detailState.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={40} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) );
} }
const data = detailState.findUnique.data;
return ( return (
<Box> <Box py={10}>
<Box mb={10}> <Button
<Button variant="subtle" onClick={() => router.back()}> variant="subtle"
<IconArrowBack color={colors['blue-button']} size={25} /> onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15}
>
Kembali
</Button> </Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}> <Paper
<Stack> w={{ base: "100%", md: "50%" }}
<Text fz={"xl"} fw={"bold"}>Detail List Desa Anti Korupsi</Text> bg={colors['white-1']}
{detailState.findUnique.data ? ( p="lg"
<Paper key={detailState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}> radius="md"
<Stack gap={"xs"}> shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Desa Anti Korupsi
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="md">
<Box> <Box>
<Text fw={"bold"} fz={"lg"}>Judul</Text> <Text fz="lg" fw="bold">Judul</Text>
<Text fz={"lg"}>{detailState.findUnique.data?.name}</Text> <Text fz="md" c="dimmed">{data.name || '-'}</Text>
</Box> </Box>
<Box> <Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text> <Text fz="lg" fw="bold">Kategori</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: detailState.findUnique.data?.deskripsi }} /> <Text fz="md" c="dimmed">{data.kategori?.name || '-'}</Text>
</Box> </Box>
<Box> <Box>
<Text fw={"bold"} fz={"lg"}>Kategori</Text> <Text fz="lg" fw="bold" mb="xs">Deskripsi</Text>
<Text fz={"lg"}>{detailState.findUnique.data?.kategori?.name}</Text> <Box
</Box> fz="md"
<Box> c="dimmed"
<Text fw={"bold"} fz={"lg"}>Dokumen</Text> dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
{detailState.findUnique.data?.file?.link ? ( style={{ lineHeight: 1.6 }}
<iframe
src={detailState.findUnique.data.file.link}
width="100%"
height="500px"
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
/> />
</Box>
<Box>
<Text fz="lg" fw="bold" mb="xs">Dokumen</Text>
{data.file?.link ? (
<Box
style={{
border: '1px solid #e0e0e0',
borderRadius: '8px',
overflow: 'hidden',
height: '500px',
width: '100%'
}}
>
<iframe
src={data.file.link}
width="100%"
height="100%"
style={{ border: 'none' }}
/>
</Box>
) : ( ) : (
<Text>Tidak ada dokumen tersedia</Text> <Text fz="sm" c="dimmed">Tidak ada dokumen tersedia</Text>
)} )}
</Box> </Box>
<Flex gap={"xs"} mt={10}>
<Group gap="sm" mt="md">
<Tooltip label="Hapus Data" withArrow position="top">
<Button <Button
color="red"
onClick={() => { onClick={() => {
if (detailState.findUnique.data) { setSelectedId(data.id);
setSelectedId(detailState.findUnique.data.id);
setModalHapus(true); setModalHapus(true);
}
}} }}
disabled={detailState.delete.loading || !detailState.findUnique.data} variant="light"
color={"red"} radius="md"
size="md"
disabled={detailState.delete.loading}
> >
<IconX size={20} /> <IconTrash size={20} />
</Button> </Button>
</Tooltip>
<Tooltip label="Edit Data" withArrow position="top">
<Button <Button
onClick={() => { color="green"
if (detailState.findUnique.data) { onClick={() => router.push(`/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/${data.id}/edit`)}
router.push(`/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/${detailState.findUnique.data.id}/edit`); variant="light"
} radius="md"
}} size="md"
disabled={!detailState.findUnique.data}
color={"green"}
> >
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Group>
</Stack> </Stack>
</Paper> </Paper>
) : null}
</Stack> </Stack>
</Paper> </Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus desa anti korupsi ini?' text="Apakah Anda yakin ingin menghapus data Desa Anti Korupsi ini?"
/> />
</Box> </Box>
); );
} }
export default DetailKegiatanDesa;

View File

@@ -1,10 +1,21 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client';
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi'; import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Select,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconFile, IconUpload, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconFile, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@@ -13,12 +24,12 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function CreateDesaAntiKorupsi() { export default function CreateDesaAntiKorupsi() {
const router = useRouter(); const router = useRouter();
const stateKorupsi = useProxy(korupsiState.desaAntikorupsi) const stateKorupsi = useProxy(korupsiState.desaAntikorupsi);
const [previewFile, setPreviewFile] = useState<string | null>(null); const [previewFile, setPreviewFile] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => { useEffect(() => {
stateKorupsi.findMany.load(); stateKorupsi.findMany.load();
@@ -27,140 +38,181 @@ function CreateDesaAntiKorupsi() {
const resetForm = () => { const resetForm = () => {
stateKorupsi.create.form = { stateKorupsi.create.form = {
name: "", name: '',
deskripsi: "", deskripsi: '',
kategoriId: "", kategoriId: '',
fileId: "", fileId: '',
}; };
setFile(null); setFile(null);
setPreviewFile(null); setPreviewFile(null);
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
if (!file) { if (!file) {
return toast.warn("Pilih file pdf terlebih dahulu"); return toast.warn('Pilih file dokumen terlebih dahulu');
}
if (!stateKorupsi.create.form.name) {
return toast.warn('Masukkan judul dokumen');
}
if (!stateKorupsi.create.form.kategoriId) {
return toast.warn('Pilih kategori dokumen');
} }
setIsLoading(true);
try {
const res = await ApiFetch.api.fileStorage.create.post({ const res = await ApiFetch.api.fileStorage.create.post({
file, file,
name: file.name, name: file.name,
}) });
const uploaded = res.data?.data; const uploaded = res.data?.data;
if (!uploaded?.id) { if (!uploaded?.id) {
return toast.error("Gagal mengupload file"); throw new Error('Gagal mengunggah dokumen');
} }
stateKorupsi.create.form.fileId = uploaded.id; stateKorupsi.create.form.fileId = uploaded.id;
await stateKorupsi.create.create(); await stateKorupsi.create.create();
toast.success('Data berhasil disimpan');
resetForm(); resetForm();
router.push("/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi") router.push('/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi');
} catch (error) {
console.error('Error:', error);
toast.error('Terjadi kesalahan saat menyimpan data');
} finally {
setIsLoading(false);
} }
};
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> <Group mb="md">
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Tooltip label="Kembali ke halaman sebelumnya" color="blue" withArrow>
<IconArrowBack color={colors['blue-button']} size={25} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
</Box> </Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Dokumen Desa Anti Korupsi
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Paper
<Stack gap={"xs"}> w={{ base: '100%', md: '50%' }}
<Title order={4}>Create Kegiatan Desa</Title> bg={colors['white-1']}
<Box> p="lg"
<Text fz={"md"} fw={"bold"}>File Document</Text> radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<Box> <Box>
<Text fw="bold" fz="sm" mb={6}>
Dokumen
</Text>
<Dropzone <Dropzone
onDrop={(files) => { onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama const selectedFile = files[0];
if (selectedFile) { if (selectedFile) {
setFile(selectedFile); setFile(selectedFile);
setPreviewFile(URL.createObjectURL(selectedFile)); // Buat preview setPreviewFile(URL.createObjectURL(selectedFile));
} }
}} }}
onReject={() => toast.error('File tidak valid.')} onReject={() => toast.error('File tidak valid, gunakan format dokumen')}
maxSize={5 * 1024 ** 2} // Maks 5MB maxSize={5 * 1024 ** 2}
accept={{ accept={{
'application/*': ['.pdf', '.doc', '.docx'], 'application/*': ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx'],
}} }}
radius="md"
> >
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}> <Group justify="center" gap="xl" mih={180} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept> <Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /> <IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept> </Dropzone.Accept>
<Dropzone.Reject> <Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} /> <IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject> </Dropzone.Reject>
<Dropzone.Idle> <Dropzone.Idle>
<IconFile size={52} color="var(--mantine-color-dimmed)" stroke={1.5} /> <IconFile size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle> </Dropzone.Idle>
<div> <div>
<Text size="xl" inline> <Text size="lg" inline>
Drag file ke sini atau klik untuk pilih file Seret dokumen ke sini atau klik untuk memilih file
</Text> </Text>
<Text size="sm" c="dimmed" inline mt={7}> <Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format document Maksimal 5MB (PDF, DOC, DOCX, XLS, XLSX, PPT, PPTX)
</Text> </Text>
</div> </div>
</Group> </Group>
</Dropzone> </Dropzone>
<Box>
<Text fw={"bold"} fz={"lg"}>Dokumen</Text> {previewFile && (
{previewFile ? ( <Box mt="md" style={{ textAlign: 'center' }}>
<iframe <iframe
src={previewFile} src={previewFile}
width="100%" width="100%"
height="500px" height="500px"
style={{ border: "1px solid #ccc", borderRadius: "8px" }} style={{
border: '1px solid #e0e0e0',
borderRadius: '8px',
maxWidth: '100%',
}}
/> />
) : ( </Box>
<Text>Tidak ada dokumen tersedia</Text>
)} )}
</Box> </Box>
</Box>
</Box>
<TextInput <TextInput
value={stateKorupsi.create.form.name} label="Judul Dokumen"
onChange={(val) => { placeholder="Masukkan judul dokumen"
stateKorupsi.create.form.name = val.target.value; value={stateKorupsi.create.form.name || ''}
}} onChange={(e) => (stateKorupsi.create.form.name = e.target.value)}
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>} required
placeholder='Masukkan judul'
/> />
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text> <Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<CreateEditor <CreateEditor
value={stateKorupsi.create.form.deskripsi} value={stateKorupsi.create.form.deskripsi || ''}
onChange={(val) => { onChange={(val) => (stateKorupsi.create.form.deskripsi = val)}
stateKorupsi.create.form.deskripsi = val;
}}
/> />
</Box> </Box>
<Select <Select
value={stateKorupsi.create.form.kategoriId} label="Kategori"
onChange={(val) => {
stateKorupsi.create.form.kategoriId = val ?? "";
}}
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>}
placeholder="Pilih kategori" placeholder="Pilih kategori"
value={stateKorupsi.create.form.kategoriId || ''}
onChange={(val) => (stateKorupsi.create.form.kategoriId = val || '')}
data={ data={
korupsiState.kategoriDesaAntiKorupsi.findMany.data?.map((v) => ({ korupsiState.kategoriDesaAntiKorupsi.findMany.data?.map((v) => ({
value: v.id, value: v.id,
label: v.name, label: v.name,
})) || [] })) || []
} }
required
searchable
clearable
/> />
<Group> <Group justify="right" mt="xl">
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button> <Button
onClick={handleSubmit}
radius="md"
size="md"
loading={isLoading}
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>
); );
} }
export default CreateDesaAntiKorupsi;

View File

@@ -1,13 +1,12 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } 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 { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, 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';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import korupsiState from '../../../_state/landing-page/desa-anti-korupsi'; import korupsiState from '../../../_state/landing-page/desa-anti-korupsi';
function DesaAntiKorupsi() { function DesaAntiKorupsi() {
@@ -16,7 +15,7 @@ function DesaAntiKorupsi() {
<Box> <Box>
<HeaderSearch <HeaderSearch
title='List Desa Anti Korupsi' title='List Desa Anti Korupsi'
placeholder='pencarian' placeholder='Cari nama program atau kategori...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -27,8 +26,8 @@ function DesaAntiKorupsi() {
} }
function ListDesaAntiKorupsi({ search }: { search: string }) { function ListDesaAntiKorupsi({ search }: { search: string }) {
const listState = useProxy(korupsiState.desaAntikorupsi)
const router = useRouter(); const router = useRouter();
const listState = useProxy(korupsiState.desaAntikorupsi);
const { const {
data, data,
@@ -42,99 +41,96 @@ function ListDesaAntiKorupsi({ search }: { search: string }) {
load(page, 10, search); load(page, 10, search);
}, [page, search]); }, [page, search]);
const filteredData = data || [] const filteredData = data || [];
// Handle loading state
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton height={550} /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
); );
} }
if (data.length === 0) {
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<JudulList <Group justify="space-between" mb="md">
title='List Desa Anti Korupsi' <Title order={4}>Daftar Program Desa Anti Korupsi</Title>
href='/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/create' <Tooltip label="Tambah Program Desa Anti Korupsi" withArrow>
/> <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>
</Tooltip>
</Group>
<Box style={{ overflowX: "auto" }}> <Box style={{ overflowX: "auto" }}>
<Table striped withTableBorder withRowBorders> <Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Nama</TableTh> <TableTh>Nama Program</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Kategori</TableTh> <TableTh>Kategori</TableTh>
<TableTh>Detail</TableTh> <TableTh>Aksi</TableTh>
</TableTr>
</TableThead>
</Table>
</Box>
</Paper>
</Box>
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<JudulList
title='List Desa Anti Korupsi'
href='/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/create'
/>
<Box style={{ overflowX: 'auto' }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<TableThead>
<TableTr>
<TableTh>Nama Desa Anti Korupsi</TableTh>
<TableTh>Deskripsi Desa Anti Korupsi</TableTh>
<TableTh>Kategori Desa Anti Korupsi</TableTh>
<TableTh>Detail</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item) => ( {filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Box w={100}> <Box w={350}>
<Text truncate="end" fz={"sm"}>{item.name}</Text> <Text lineClamp={1} fw={500}>{item.name || '-'}</Text>
</Box> </Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={200}> <Text fz="sm" c="dimmed">
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} /> {item.kategori?.name || '-'}
</Box> </Text>
</TableTd> </TableTd>
<TableTd>{item.kategori?.name}</TableTd>
<TableTd> <TableTd>
<Button onClick={() => router.push(`/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/${item.id}`)}> <Button
<IconDeviceImacCog size={25} /> variant="light"
color="blue"
onClick={() => router.push(`/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/${item.id}`)}
>
<IconDeviceImacCog size={20} />
<Text ml={5}>Detail</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} ))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text c="dimmed">Tidak ada data program yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
</Stack>
</Paper> </Paper>
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => { onChange={(newPage) => {
load(newPage, 10); load(newPage, 10);
window.scrollTo(0, 0); window.scrollTo({ top: 0, behavior: 'smooth' });
}} }}
total={totalPages} total={totalPages}
mt="md" mt="md"
mb="md" mb="md"
color="blue"
radius="md"
/> />
</Center> </Center>
</Box> </Box>
) );
} }
export default DesaAntiKorupsi; export default DesaAntiKorupsi;

View File

@@ -1,65 +1,108 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { IconBulb, IconUsers, IconBrandFacebook } from '@tabler/icons-react';
function LayoutTabs({ children }: { children: React.ReactNode }) { function LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter() const router = useRouter();
const pathname = usePathname() const pathname = usePathname();
const tabs = [ const tabs = [
{ {
label: "Program Inovasi", label: "Program Inovasi",
value: "program-inovasi", value: "program-inovasi",
href: "/admin/landing-page/profile/program-inovasi" href: "/admin/landing-page/profile/program-inovasi",
icon: <IconBulb size={18} stroke={1.8} />,
tooltip: "Lihat dan kelola program inovasi desa",
}, },
{ {
label: "Pejabat Desa", label: "Pejabat Desa",
value: "pejabat-desa", value: "pejabat-desa",
href: "/admin/landing-page/profile/pejabat-desa" href: "/admin/landing-page/profile/pejabat-desa",
icon: <IconUsers size={18} stroke={1.8} />,
tooltip: "Kelola data pejabat desa",
}, },
{ {
label: "Media Sosial", label: "Media Sosial",
value: "media-sosial", value: "media-sosial",
href: "/admin/landing-page/profile/media-sosial" href: "/admin/landing-page/profile/media-sosial",
icon: <IconBrandFacebook size={18} stroke={1.8} />,
tooltip: "Atur tautan media sosial desa",
}, },
]; ];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value); const currentTab = tabs.find(tab => tab.href === pathname);
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => { const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value) const tab = tabs.find(t => t.value === value);
if (tab) { if (tab) {
router.push(tab.href) router.push(tab.href);
}
setActiveTab(value)
} }
setActiveTab(value);
};
useEffect(() => { useEffect(() => {
const match = tabs.find(tab => tab.href === pathname) const match = tabs.find(tab => tab.href === pathname);
if (match) { if (match) {
setActiveTab(match.value) setActiveTab(match.value);
} }
}, [pathname]) }, [pathname]);
return ( return (
<Stack> <Stack gap="lg">
<Title order={3}>Profile</Title> <Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}> Profil Desa
<TabsList p={"xs"} bg={"#BBC8E7FF"}> </Title>
{tabs.map((e, i) => ( <Tabs
<TabsTab key={i} value={e.value}>{e.label}</TabsTab> variant="pills"
value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
}}
>
{tabs.map((tab, i) => (
<Tooltip key={i} label={tab.tooltip} position="bottom" withArrow>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))} ))}
</TabsList> </TabsList>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value}> {tabs.map((tab, i) => (
{/* Konten dummy, bisa diganti tergantung routing */} <TabsPanel
<></> key={i}
value={tab.value}
style={{
padding: "1.5rem",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
borderRadius: "1rem",
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
}}
>
{children}
</TabsPanel> </TabsPanel>
))} ))}
</Tabs> </Tabs>
{children}
</Stack> </Stack>
); );
} }

View File

@@ -1,9 +1,20 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client';
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile'; import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Image,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
@@ -12,17 +23,17 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function EditMediaSosial() { function EditMediaSosial() {
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial) const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial);
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
const [previewImage, setPreviewImage] = useState<string | null>(null); const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: stateMediaSosial.update.form.name || "", name: stateMediaSosial.update.form.name || '',
iconUrl: stateMediaSosial.update.form.iconUrl || "", iconUrl: stateMediaSosial.update.form.iconUrl || '',
imageId: stateMediaSosial.update.form.imageId || "" imageId: stateMediaSosial.update.form.imageId || '',
}) });
useEffect(() => { useEffect(() => {
const id = params?.id as string; const id = params?.id as string;
@@ -34,136 +45,147 @@ function EditMediaSosial() {
if (data) { if (data) {
setFormData({ setFormData({
name: data.name || "", name: data.name || '',
iconUrl: data.iconUrl || "", iconUrl: data.iconUrl || '',
imageId: data.imageId || "", imageId: data.imageId || '',
}); });
// Tampilkan preview gambar if (data.image?.link) setPreviewImage(data.image.link);
if (data.image?.link) {
setPreviewImage(data.image.link);
}
} }
} catch (error) { } catch (error) {
console.error("Error loading program inovasi:", error); console.error('Error loading media sosial:', error);
toast.error( toast.error(
error instanceof Error ? error.message : "Gagal mengambil data program inovasi" error instanceof Error ? error.message : 'Gagal mengambil data media sosial'
); );
} }
} };
loadMediaSosial(); loadMediaSosial();
}, [params?.id]); }, [params?.id]);
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
stateMediaSosial.update.form = { stateMediaSosial.update.form = { ...stateMediaSosial.update.form, ...formData };
...stateMediaSosial.update.form,
name: formData.name,
iconUrl: formData.iconUrl,
imageId: formData.imageId ?? "",
}
if (file) { if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name }); const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data; const uploaded = res.data?.data;
if (!uploaded?.id) { if (!uploaded?.id) return toast.error('Gagal upload gambar');
return toast.error("Gagal upload gambar");
}
// Update imageId in global state
stateMediaSosial.update.form.imageId = uploaded.id; stateMediaSosial.update.form.imageId = uploaded.id;
} }
await stateMediaSosial.update.update(); await stateMediaSosial.update.update();
toast.success("Media Sosial berhasil diperbarui!"); toast.success('Media sosial berhasil diperbarui!');
router.push("/admin/landing-page/profile/media-sosial"); router.push('/admin/landing-page/profile/media-sosial');
} catch (error) { } catch (error) {
console.error("Error updating media sosial:", error); console.error('Error updating media sosial:', error);
toast.error("Terjadi kesalahan saat memperbarui media sosial"); toast.error('Terjadi kesalahan saat memperbarui media sosial');
} }
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> <Group mb="md">
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Tooltip label="Kembali ke halaman sebelumnya" color="blue" withArrow>
<IconArrowBack color={colors['blue-button']} size={25} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
</Box> </Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Media Sosial
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Paper
<Stack gap={"xs"}> w={{ base: '100%', md: '50%' }}
<Title order={4}>Edit Media Sosial</Title> bg={colors['white-1']}
<Box> p="lg"
<Text fz={"md"} fw={"bold"}>Gambar</Text> radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<Box> <Box>
<Text fw="bold" fz="sm" mb={6}>
Gambar Media Sosial
</Text>
<Dropzone <Dropzone
onDrop={(files) => { onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama const selectedFile = files[0];
if (selectedFile) { if (selectedFile) {
setFile(selectedFile); setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview setPreviewImage(URL.createObjectURL(selectedFile));
} }
}} }}
onReject={() => toast.error('File tidak valid.')} onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2} // Maks 5MB maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }} accept={{ 'image/*': [] }}
radius="md"
p="xl"
> >
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}> <Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept> <Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /> <IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
</Dropzone.Accept> </Dropzone.Accept>
<Dropzone.Reject> <Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} /> <IconX size={48} color="red" stroke={1.5} />
</Dropzone.Reject> </Dropzone.Reject>
<Dropzone.Idle> <Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} /> <IconPhoto size={48} color="#868e96" stroke={1.5} />
</Dropzone.Idle> </Dropzone.Idle>
<Stack gap="xs" align="center">
<div> <Text size="md" fw={500}>
<Text size="xl" inline> Seret gambar atau klik untuk memilih file
Drag gambar ke sini atau klik untuk pilih file
</Text> </Text>
<Text size="sm" c="dimmed" inline mt={7}> <Text size="sm" c="dimmed">
Maksimal 5MB dan harus format gambar Maksimal 5MB, format gambar wajib
</Text> </Text>
</div> </Stack>
</Group> </Group>
</Dropzone> </Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && ( {previewImage && (
<Box mt="sm"> <Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
<Image <Image
src={previewImage} src={previewImage}
alt="Preview" alt="Preview Gambar"
style={{ radius="md"
maxWidth: '100%', style={{ maxHeight: 220, objectFit: 'contain', border: `1px solid ${colors['blue-button']}` }}
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/> />
</Box> </Box>
)} )}
</Box>
</Box>
</Box>
<TextInput <TextInput
label="Nama Media Sosial / Kontak"
placeholder="Masukkan nama media sosial atau kontak"
value={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Nama Media Sosial / Nama Kontak</Text>} required
placeholder='Masukkan nama media sosial'
/> />
<TextInput <TextInput
label="Link Media Sosial / Nomor Telepon"
placeholder="Masukkan link media sosial atau nomor telepon"
value={formData.iconUrl} value={formData.iconUrl}
onChange={(e) => setFormData({ ...formData, iconUrl: e.target.value })} onChange={(e) => setFormData({ ...formData, iconUrl: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Icon URL / No Telephone</Text>} required
placeholder='Masukkan icon url'
/> />
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button> <Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -2,103 +2,132 @@
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile'; import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } 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';
function DetailMediaSosial() { function DetailMediaSosial() {
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial) const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial);
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 params = useParams() const params = useParams();
const router = useRouter(); const router = useRouter();
useShallowEffect(() => { useShallowEffect(() => {
stateMediaSosial.findUnique.load(params?.id as string) stateMediaSosial.findUnique.load(params?.id as string);
}, []) }, []);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
stateMediaSosial.delete.byId(selectedId) stateMediaSosial.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/landing-page/profile/media-sosial") router.push("/admin/landing-page/profile/media-sosial");
}
} }
};
if (!stateMediaSosial.findUnique.data) { if (!stateMediaSosial.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) );
} }
const data = stateMediaSosial.findUnique.data;
return ( return (
<Box> <Box py={10}>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Media Sosial</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Media Sosial / Nama Kontak</Text>
<Text fz={"lg"}>{stateMediaSosial.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Icon URL / No Telephone</Text>
<Text fz={"lg"}>{stateMediaSosial.findUnique.data?.iconUrl}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
<Box w={100} h={100}>
<Image src={stateMediaSosial.findUnique.data?.image?.link} alt="gambar" />
</Box>
</Box>
<Box>
<Flex gap={"xs"}>
<Button <Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15}
>
Kembali
</Button>
<Paper
w={{ base: "100%", md: "60%" }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Media Sosial
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz="lg" fw="bold">Nama Media Sosial / Kontak</Text>
<Text fz="md" c="dimmed">{data.name || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Icon / Nomor Telepon</Text>
<Text fz="md" c="dimmed">{data.iconUrl || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Gambar</Text>
{data.image?.link ? (
<Image
src={data.image.link}
alt={data.name || 'Gambar Media Sosial'}
w={120}
h={120}
radius="md"
fit="cover"
/>
) : (
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
)}
</Box>
<Group gap="sm">
<Tooltip label="Hapus Media Sosial" withArrow position="top">
<Button
color="red"
onClick={() => { onClick={() => {
if (stateMediaSosial.findUnique.data) { setSelectedId(data.id);
setSelectedId(stateMediaSosial.findUnique.data.id);
setModalHapus(true); setModalHapus(true);
}
}} }}
disabled={!stateMediaSosial.findUnique.data} variant="light"
color="red"> radius="md"
<IconX size={20} /> size="md"
>
<IconTrash size={20} />
</Button> </Button>
</Tooltip>
<Tooltip label="Edit Media Sosial" withArrow position="top">
<Button <Button
onClick={() => { color="green"
if (stateMediaSosial.findUnique.data) { onClick={() => router.push(`/admin/landing-page/profile/media-sosial/${data.id}/edit`)}
router.push(`/admin/landing-page/profile/media-sosial/${stateMediaSosial.findUnique.data.id}/edit`); variant="light"
} radius="md"
}} size="md"
disabled={!stateMediaSosial.findUnique.data} >
color="green">
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Box> </Group>
</Stack> </Stack>
</Paper> </Paper>
</Stack> </Stack>
</Paper> </Paper>
{/* Modal Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus media sosial ini?" text="Apakah Anda yakin ingin menghapus media sosial ini?"
/> />
</Box> </Box>
); );

View File

@@ -1,8 +1,19 @@
/* 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 ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Image,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@@ -11,9 +22,9 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import profileLandingPageState from '../../../../_state/landing-page/profile'; import profileLandingPageState from '../../../../_state/landing-page/profile';
function CreateMediaSosial() { export default function CreateMediaSosial() {
const router = useRouter(); const router = useRouter();
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial) const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial);
const [previewImage, setPreviewImage] = useState<string | null>(null); const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
@@ -23,27 +34,28 @@ function CreateMediaSosial() {
const resetForm = () => { const resetForm = () => {
stateMediaSosial.create.form = { stateMediaSosial.create.form = {
name: "", name: '',
imageId: "", imageId: '',
iconUrl: "", iconUrl: '',
}; };
setPreviewImage(null); setPreviewImage(null);
setFile(null); setFile(null);
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
if (!file) { if (!file) {
return toast.warn("Pilih file gambar terlebih dahulu"); return toast.warn('Silakan pilih file gambar terlebih dahulu');
} }
const res = await ApiFetch.api.fileStorage.create.post({ const res = await ApiFetch.api.fileStorage.create.post({
file, file,
name: file.name, name: file.name,
}) });
const uploaded = res.data?.data; const uploaded = res.data?.data;
if (!uploaded?.id) { if (!uploaded?.id) {
return toast.error("Gagal mengupload file"); return toast.error('Gagal mengunggah gambar, silakan coba lagi');
} }
stateMediaSosial.create.form.imageId = uploaded.id; stateMediaSosial.create.form.imageId = uploaded.id;
@@ -51,98 +63,108 @@ function CreateMediaSosial() {
await stateMediaSosial.create.create(); await stateMediaSosial.create.create();
resetForm(); resetForm();
router.push("/admin/landing-page/profile/media-sosial") router.push('/admin/landing-page/profile/media-sosial');
} };
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> return (
<Stack gap={"xs"}> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Title order={4}>Create Media Sosial</Title> <Group mb="md">
<Box> <Tooltip label="Kembali ke halaman sebelumnya" color="blue" withArrow>
<Text fz={"md"} fw={"bold"}>Gambar</Text> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Media Sosial
</Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<Box> <Box>
<Text fw="bold" fz="sm" mb={6}>
Gambar Media Sosial
</Text>
<Dropzone <Dropzone
onDrop={(files) => { onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama const selectedFile = files[0];
if (selectedFile) { if (selectedFile) {
setFile(selectedFile); setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview setPreviewImage(URL.createObjectURL(selectedFile));
} }
}} }}
onReject={() => toast.error('File tidak valid.')} onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2} // Maks 5MB maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }} accept={{ 'image/*': [] }}
radius="md"
p="xl"
> >
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}> <Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept> <Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /> <IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept> </Dropzone.Accept>
<Dropzone.Reject> <Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} /> <IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject> </Dropzone.Reject>
<Dropzone.Idle> <Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} /> <IconPhoto size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle> </Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group> </Group>
<Text ta="center" mt="sm" size="sm" color="dimmed">
Seret gambar atau klik untuk memilih file (maks 5MB)
</Text>
</Dropzone> </Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && ( {previewImage && (
<Box mt="sm"> <Box mt="sm" style={{ textAlign: 'center' }}>
<Image <Image
src={previewImage} src={previewImage}
alt="Preview" alt="Preview Gambar"
style={{ radius="md"
maxWidth: '100%', style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/> />
</Box> </Box>
)} )}
</Box>
</Box>
</Box>
<TextInput <TextInput
label="Nama Media Sosial / Kontak"
placeholder="Masukkan nama media sosial atau kontak"
value={stateMediaSosial.create.form.name || ''} value={stateMediaSosial.create.form.name || ''}
onChange={(val) => { onChange={(e) => (stateMediaSosial.create.form.name = e.target.value)}
stateMediaSosial.create.form.name = val.target.value; required
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Media Sosial / Nama Kontak</Text>}
placeholder='Masukkan nama media sosial / nama kontak'
/> />
<TextInput <TextInput
label="Link Media Sosial / Nomor Telepon"
placeholder="Masukkan link media sosial atau nomor telepon"
value={stateMediaSosial.create.form.iconUrl || ''} value={stateMediaSosial.create.form.iconUrl || ''}
onChange={(val) => { onChange={(e) => (stateMediaSosial.create.form.iconUrl = e.target.value)}
stateMediaSosial.create.form.iconUrl = val.target.value; required
}}
label={<Text fw={"bold"} fz={"sm"}>Link Media Sosial / No Telephone</Text>}
placeholder='Masukkan link media sosial / no telephone'
/> />
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button> <Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>
); );
} }
export default CreateMediaSosial;

View File

@@ -1,13 +1,12 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Image, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import { Box, Button, Center, Group, Image, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import profileLandingPageState from '../../../_state/landing-page/profile'; import profileLandingPageState from '../../../_state/landing-page/profile';
function MediaSosial() { function MediaSosial() {
@@ -16,7 +15,7 @@ function MediaSosial() {
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Media Sosial' title='Media Sosial'
placeholder='pencarian' placeholder='Cari nama media sosial atau kontak...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -44,80 +43,77 @@ function ListMediaSosial({ search }: { search: string }) {
const filteredData = data || [] const filteredData = data || []
// Handle loading state
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton height={550} /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
); );
} }
if (data.length === 0) {
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<JudulList <Group justify="space-between" mb="md">
title='List Media Sosial' <Title order={4}>Daftar Media Sosial</Title>
href='/admin/landing-page/profile/media-sosial/create' <Tooltip label="Tambah Media Sosial" withArrow>
/> <Button leftSection={<IconPlus size={18} />} color="blue" variant="light" onClick={() => router.push('/admin/landing-page/profile/media-sosial/create')}>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: "auto" }}> <Box style={{ overflowX: "auto" }}>
<Table striped withTableBorder withRowBorders> <Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Nama Media Sosial / Nama Kontak</TableTh> <TableTh>Nama Media Sosial / Kontak</TableTh>
<TableTh>Image</TableTh> <TableTh>Gambar</TableTh>
<TableTh>Icon URL / No Telephone</TableTh> <TableTh>Icon / No. Telepon</TableTh>
<TableTh>Detail</TableTh> <TableTh>Aksi</TableTh>
</TableTr>
</TableThead>
</Table>
</Box>
</Paper>
</Box>
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Media Sosial'
href='/admin/landing-page/profile/media-sosial/create'
/>
<Box style={{ overflowY: "auto" }}>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Media Sosial / Nama Kontak</TableTh>
<TableTh>Image</TableTh>
<TableTh>Icon URL / No Telephone</TableTh>
<TableTh>Detail</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item) => ( {filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd> <TableTd>
<Box w={50} h={50}> <Text fw={500}>{item.name}</Text>
<Image src={item.image?.link} alt={item.name} /> </TableTd>
<TableTd>
<Box w={50} h={50} style={{ borderRadius: 8, overflow: 'hidden' }}>
{item.image?.link ? (
<Image src={item.image.link} alt={item.name} fit="cover" />
) : (
<Box bg={colors['blue-button']} w="100%" h="100%" />
)}
</Box> </Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={250}> <Text truncate fz="sm" color="dimmed">
<a style={{color: "black"}} href={item.iconUrl} target="_blank" rel="noopener noreferrer"> {item.iconUrl || item.noTelp || '-'}
<Text truncate fz={'sm'}>{item.iconUrl}</Text> </Text>
</a>
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button onClick={() => router.push(`/admin/landing-page/profile/media-sosial/${item.id}`)}> <Button
variant="light"
color="blue"
onClick={() => router.push(`/admin/landing-page/profile/media-sosial/${item.id}`)}
>
<IconDeviceImac size={20} /> <IconDeviceImac size={20} />
<Text ml={5}>Detail</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} ))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">Tidak ada data media sosial yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
@@ -127,11 +123,13 @@ function ListMediaSosial({ search }: { search: string }) {
value={page} value={page}
onChange={(newPage) => { onChange={(newPage) => {
load(newPage, 10); load(newPage, 10);
window.scrollTo(0, 0); window.scrollTo({ top: 0, behavior: 'smooth' });
}} }}
total={totalPages} total={totalPages}
mt="md" mt="md"
mb="md" mb="md"
color="blue"
radius="md"
/> />
</Center> </Center>
</Box> </Box>

View File

@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Alert, Box, Button, Center, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Alert, Box, Button, Center, Group, Image, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
@@ -144,14 +144,25 @@ function EditPejabatDesa() {
return ( return (
<Box> <Box>
<Stack gap="xs"> <Stack gap="xs">
<Box> <Group mb="md">
<Button variant="subtle" onClick={handleBack}> <Tooltip label="Kembali ke halaman sebelumnya" color="blue" withArrow>
<IconArrowBack color={colors['blue-button']} size={20} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
</Box> </Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Pejabat Desa
</Title>
</Group>
<Box> <Paper
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p="md" radius={10}> w={{ base: "100%", md: "50%" }}
bg={colors['white-1']}
p="md"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="xs"> <Stack gap="xs">
<Title order={3}>Edit Profile Pejabat Desa</Title> <Title order={3}>Edit Profile Pejabat Desa</Title>
@@ -261,7 +272,6 @@ function EditPejabatDesa() {
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>
</Box>
</Stack> </Stack>
</Box> </Box>
); );

View File

@@ -1,24 +1,26 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Divider, Grid, GridCol, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core'; import { Box, Button, Center, Divider, Grid, GridCol, Image, Paper, Skeleton, Stack, Text, Title, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconEdit } from '@tabler/icons-react'; import { IconEdit } 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 profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile'; import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
function Page() { function Page() {
const router = useRouter() const router = useRouter();
const allList = useProxy(profileLandingPageState.pejabatDesa) const allList = useProxy(profileLandingPageState.pejabatDesa);
useShallowEffect(() => { useShallowEffect(() => {
allList.findUnique.load("edit") // Assuming "1" is your default ID, adjust as needed allList.findUnique.load("edit");
}, []) }, []);
if (!allList.findUnique.data) { if (!allList.findUnique.data) {
return <Stack> return (
<Skeleton radius={10} h={800} /> <Stack align="center" justify="center" py="xl">
<Skeleton radius="md" height={800} />
</Stack> </Stack>
);
} }
const dataArray = Array.isArray(allList.findUnique.data) const dataArray = Array.isArray(allList.findUnique.data)
@@ -26,79 +28,82 @@ function Page() {
: [allList.findUnique.data]; : [allList.findUnique.data];
return ( return (
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
<Stack gap={"xs"}> <Stack gap="md">
<Grid> <Grid align="center">
<GridCol span={{ base: 12, md: 11 }}> <GridCol span={{ base: 12, md: 11 }}>
<Title order={3}>Preview Pejabat Desa</Title> <Title order={3} c={colors['blue-button']}>Preview Pejabat Desa</Title>
</GridCol> </GridCol>
<GridCol span={{ base: 12, md: 1 }}> <GridCol span={{ base: 12, md: 1 }}>
<Button bg={colors['blue-button']} onClick={() => router.push(`/admin/landing-page/profile/pejabat-desa/${allList.findUnique.data?.id}`)}> <Tooltip label="Edit Profil Pejabat" withArrow>
<IconEdit size={16} /> <Button
c="blue"
variant="light"
leftSection={<IconEdit size={18} stroke={2} />}
radius="md"
onClick={() => router.push(`/admin/landing-page/profile/pejabat-desa/${allList.findUnique.data?.id}`)}
>
Edit
</Button> </Button>
</Tooltip>
</GridCol> </GridCol>
</Grid> </Grid>
{dataArray.map((item) => ( {dataArray.map((item) => (
<Box key={item.id} > <Paper key={item.id} p="xl" bg={colors['BG-trans']} radius="md" shadow="xs">
<Paper p={"xl"} bg={colors['BG-trans']}> <Box px={{ base: "sm", md: 100 }}>
<Box px={{ base: "md", md: 100 }}>
<Grid> <Grid>
<GridCol span={{ base: 12, md: 12 }}> <GridCol span={12}>
<Center> <Center>
<Image src={"/darmasaba-icon.png"} w={{ base: 100, md: 150 }} alt='' /> <Image src="/darmasaba-icon.png" w={{ base: 100, md: 150 }} alt="Logo Desa" />
</Center> </Center>
</GridCol> </GridCol>
<GridCol span={{ base: 12, md: 12 }}> <GridCol span={12}>
<Text ta={"center"} fz={{ base: "1.2rem", md: "1.8rem" }} fw={'bold'}>PROFIL PIMPINAN BADAN PUBLIK DESA DARMASABA </Text> <Text ta="center" fz={{ base: "1.2rem", md: "1.8rem" }} fw="bold" c={colors['blue-button']}>
Profil Pimpinan Badan Publik Desa Darmasaba
</Text>
</GridCol> </GridCol>
</Grid> </Grid>
</Box> </Box>
<Divider my={"md"} color={colors['blue-button']} /> <Divider my="md" color={colors['blue-button']} />
{/* biodata perbekel */} <Box px={{ base: 0, md: 50 }} pb="xl">
<Box px={{ base: 0, md: 50 }} pb={30}> <Paper bg={colors['BG-trans']} radius="md" shadow="xs" p="lg">
<Box pb={20} px={{ base: 0, md: 50 }}>
<Paper bg={colors['BG-trans']} w={{ base: "100%", md: "100%" }}>
<Stack gap={0}> <Stack gap={0}>
<Center> <Center>
<Image <Image
pt={{ base: 0, md: 90 }} pt={{ base: 0, md: 60 }}
src={item.image?.link || "/perbekel.png"} src={item.image?.link || "/perbekel.png"}
w={{ base: 250, md: 350 }} w={{ base: 250, md: 350 }}
alt='Foto Profil PPID' alt="Foto Profil Pejabat"
onError={(e) => { radius="md"
e.currentTarget.src = "/perbekel.png"; onError={(e) => { e.currentTarget.src = "/perbekel.png"; }}
}}
/> />
</Center> </Center>
<Paper <Paper
bg={colors['blue-button']} bg={colors['blue-button']}
py={20} py="md"
px="sm"
radius="md"
className="glass3" className="glass3"
px={{ base: 10, md: 10 }} 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" }}> <Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "1.2rem", md: "1.6rem" }}>
{item.name} {item.name}
</Text> </Text>
</Paper> </Paper>
</Stack> </Stack>
</Paper> </Paper>
</Box> <Box mt="lg">
<Box pt={10}> <Text fz={{ base: "1.125rem", md: "1.5rem" }} fw="bold" mb={4}>Jabatan</Text>
<Box> <Text fz={{ base: "1rem", md: "1.4rem" }} ta="justify" c={colors['blue-button']}>
<Text fz={{ base: "1.125rem", md: "1.6rem" }} fw={'bold'}>Position</Text> {item.position}
<Text fz={{ base: "1rem", md: "1.5rem" }} ta={"justify"}>{item.position}</Text> </Text>
</Box>
</Box> </Box>
</Box> </Box>
</Paper> </Paper>
</Box>
))} ))}
</Stack> </Stack>
</Paper> </Paper>
) );
} }
export default Page; export default Page;

View File

@@ -3,7 +3,18 @@
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile'; import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Image,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
@@ -86,92 +97,113 @@ function EditProgramInovasi() {
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> <Group mb="md">
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Tooltip label="Kembali ke halaman sebelumnya" color="blue" withArrow>
<IconArrowBack color={colors['blue-button']} size={25} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
</Box> </Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Program Inovasi
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Paper
<Stack gap={"xs"}> w={{ base: '100%', md: '50%' }}
<Title order={4}>Edit Program Inovasi</Title> bg={colors['white-1']}
<Box> p="lg"
<Text fz={"md"} fw={"bold"}>Gambar</Text> radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<Box> <Box>
<Text fw="bold" fz="sm" mb={6}>
Gambar Program Inovasi
</Text>
<Dropzone <Dropzone
onDrop={(files) => { onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama const selectedFile = files[0];
if (selectedFile) { if (selectedFile) {
setFile(selectedFile); setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview setPreviewImage(URL.createObjectURL(selectedFile));
} }
}} }}
onReject={() => toast.error('File tidak valid.')} onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2} // Maks 5MB maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }} accept={{ 'image/*': [] }}
radius="md"
p="xl"
> >
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}> <Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept> <Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /> <IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
</Dropzone.Accept> </Dropzone.Accept>
<Dropzone.Reject> <Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} /> <IconX size={48} color="red" stroke={1.5} />
</Dropzone.Reject> </Dropzone.Reject>
<Dropzone.Idle> <Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} /> <IconPhoto size={48} color="#868e96" stroke={1.5} />
</Dropzone.Idle> </Dropzone.Idle>
<Stack gap="xs" align="center">
<div> <Text size="md" fw={500}>
<Text size="xl" inline> Seret gambar atau klik untuk memilih file
Drag gambar ke sini atau klik untuk pilih file
</Text> </Text>
<Text size="sm" c="dimmed" inline mt={7}> <Text size="sm" c="dimmed">
Maksimal 5MB dan harus format gambar Maksimal 5MB, format gambar wajib
</Text> </Text>
</div> </Stack>
</Group> </Group>
</Dropzone> </Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && ( {previewImage && (
<Box mt="sm"> <Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
<Image <Image
src={previewImage} src={previewImage}
alt="Preview" alt="Preview Gambar"
style={{ radius="md"
maxWidth: '100%', style={{ maxHeight: 220, objectFit: 'contain', border: `1px solid ${colors['blue-button']}` }}
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/> />
</Box> </Box>
)} )}
</Box>
</Box> </Box>
<TextInput <TextInput
label="Nama Program Inovasi"
placeholder="Masukkan nama program inovasi"
value={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Nama Produk</Text>} required
placeholder='Masukkan nama produk'
/> />
<TextInput <TextInput
label="Deskripsi"
placeholder="Masukkan deskripsi program inovasi"
value={formData.description} value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })} onChange={(e) => setFormData({ ...formData, description: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Deskripsi</Text>} required
placeholder='Masukkan deskripsi'
/> />
<TextInput <TextInput
label="Link Program Inovasi"
placeholder="Masukkan link program inovasi (opsional)"
value={formData.link} value={formData.link}
onChange={(e) => setFormData({ ...formData, link: e.target.value })} onChange={(e) => setFormData({ ...formData, link: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Link</Text>}
placeholder='Masukkan link'
/> />
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button> <Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -2,9 +2,9 @@
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile'; import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } 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';
@@ -31,91 +31,116 @@ function DetailProgramInovasi() {
if (!stateProgramInovasi.findUnique.data) { if (!stateProgramInovasi.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={12}>
<Skeleton h={500} /> <Skeleton height={520} radius="md" />
</Stack> </Stack>
) )
} }
return ( const data = stateProgramInovasi.findUnique.data
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Program Inovasi</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Program Inovasi</Text>
<Text fz={"lg"}>{stateProgramInovasi.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text>
<Text
fz={"lg"}
>{stateProgramInovasi.findUnique.data?.description}</Text> return (
</Box> <Box px={{ base: 'md', md: 'xl' }} py="lg">
<Button variant="subtle" onClick={() => router.back()} leftSection={<IconArrowBack size={22} color={colors['blue-button']} />}>
Kembali
</Button>
<Paper
w={{ base: "100%", md: "60%" }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Program Inovasi
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Link</Text> <Text fz="lg" fw="bold">Nama Program Inovasi</Text>
<Text fz="md" c="dimmed">{data.name || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Deskripsi</Text>
<Text fz="md" c="dimmed" style={{ whiteSpace: 'pre-wrap' }}>{data.description || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Link</Text>
{data.link ? (
<a <a
href={stateProgramInovasi.findUnique.data?.link || "#"} href={data.link}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
style={{ style={{
wordWrap: 'break-word', color: colors['blue-button'],
whiteSpace: 'pre-wrap', textDecoration: 'underline',
overflowWrap: 'break-word', wordBreak: 'break-word',
width: '100%'
}} }}
> >
{stateProgramInovasi.findUnique.data?.link || "Tidak ada link"} {data.link}
</a> </a>
) : (
<Text fz="md" c="dimmed">-</Text>
)}
</Box> </Box>
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text> <Text fz="lg" fw="bold">Gambar</Text>
<Image src={stateProgramInovasi.findUnique.data?.image?.link} alt="gambar" /> {data.image?.link ? (
<Image
src={data.image.link}
alt="Gambar Program"
radius="md"
style={{ maxWidth: '100%', maxHeight: 300, objectFit: 'contain' }}
/>
) : (
<Text fz="md" c="dimmed">-</Text>
)}
</Box> </Box>
<Box>
<Flex gap={"xs"}> <Group gap="sm">
<Tooltip label="Hapus Program Inovasi" withArrow position="top">
<Button <Button
color="red"
onClick={() => { onClick={() => {
if (stateProgramInovasi.findUnique.data) { setSelectedId(data.id);
setSelectedId(stateProgramInovasi.findUnique.data.id);
setModalHapus(true); setModalHapus(true);
}
}} }}
disabled={!stateProgramInovasi.findUnique.data} variant="light"
color="red"> radius="md"
<IconX size={20} /> size="md"
>
<IconTrash size={20} />
</Button> </Button>
</Tooltip>
<Tooltip label="Edit Program Inovasi" withArrow position="top">
<Button <Button
onClick={() => { color="green"
if (stateProgramInovasi.findUnique.data) { onClick={() => router.push(`/admin/landing-page/profile/program-inovasi/${data.id}/edit`)}
router.push(`/admin/landing-page/profile/program-inovasi/${stateProgramInovasi.findUnique.data.id}/edit`); variant="light"
} radius="md"
}} size="md"
disabled={!stateProgramInovasi.findUnique.data} >
color="green">
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Box> </Group>
</Stack> </Stack>
</Paper> </Paper>
</Stack> </Stack>
</Paper> </Paper>
{/* Modal Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus program inovasi ini?" text="Apakah Anda yakin ingin menghapus program inovasi ini?"
/> />
</Box> </Box>
); );

View File

@@ -1,8 +1,20 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client';
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Image,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@@ -13,7 +25,7 @@ import profileLandingPageState from '../../../../_state/landing-page/profile';
function CreateProgramInovasi() { function CreateProgramInovasi() {
const router = useRouter(); const router = useRouter();
const stateProgramInovasi = useProxy(profileLandingPageState.programInovasi) const stateProgramInovasi = useProxy(profileLandingPageState.programInovasi);
const [previewImage, setPreviewImage] = useState<string | null>(null); const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
@@ -31,20 +43,21 @@ function CreateProgramInovasi() {
setPreviewImage(null); setPreviewImage(null);
setFile(null); setFile(null);
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
if (!file) { if (!file) {
return toast.warn("Pilih file gambar terlebih dahulu"); return toast.warn("Silakan pilih file gambar terlebih dahulu");
} }
const res = await ApiFetch.api.fileStorage.create.post({ const res = await ApiFetch.api.fileStorage.create.post({
file, file,
name: file.name, name: file.name,
}) });
const uploaded = res.data?.data; const uploaded = res.data?.data;
if (!uploaded?.id) { if (!uploaded?.id) {
return toast.error("Gagal mengupload file"); return toast.error("Gagal mengunggah gambar, silakan coba lagi");
} }
stateProgramInovasi.create.form.imageId = uploaded.id; stateProgramInovasi.create.form.imageId = uploaded.id;
@@ -55,99 +68,116 @@ function CreateProgramInovasi() {
router.push("/admin/landing-page/profile/program-inovasi") router.push("/admin/landing-page/profile/program-inovasi")
} }
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> <Group mb="md">
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Tooltip label="Kembali ke halaman sebelumnya" color="blue" withArrow>
<IconArrowBack color={colors['blue-button']} size={25} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
</Box> </Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Program Inovasi
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Paper
<Stack gap={"xs"}> w={{ base: '100%', md: '50%' }}
<Title order={4}>Create Program Inovasi</Title> bg={colors['white-1']}
<Box> p="lg"
<Text fz={"md"} fw={"bold"}>Gambar</Text> radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<Box> <Box>
<Text fw="bold" fz="sm" mb={6}>
Gambar Program Inovasi
</Text>
<Dropzone <Dropzone
onDrop={(files) => { onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama const selectedFile = files[0];
if (selectedFile) { if (selectedFile) {
setFile(selectedFile); setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview setPreviewImage(URL.createObjectURL(selectedFile));
} }
}} }}
onReject={() => toast.error('File tidak valid.')} onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2} // Maks 5MB maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }} accept={{ 'image/*': [] }}
radius="md"
p="xl"
> >
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}> <Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept> <Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /> <IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
</Dropzone.Accept> </Dropzone.Accept>
<Dropzone.Reject> <Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} /> <IconX size={48} color="red" stroke={1.5} />
</Dropzone.Reject> </Dropzone.Reject>
<Dropzone.Idle> <Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} /> <IconPhoto size={48} color="#868e96" stroke={1.5} />
</Dropzone.Idle> </Dropzone.Idle>
<Stack gap="xs" align="center">
<div> <Text size="md" fw={500}>
<Text size="xl" inline> Seret gambar atau klik untuk memilih file
Drag gambar ke sini atau klik untuk pilih file
</Text> </Text>
<Text size="sm" c="dimmed" inline mt={7}> <Text size="sm" c="dimmed">
Maksimal 5MB dan harus format gambar Maksimal 5MB, format gambar wajib
</Text> </Text>
</div> </Stack>
</Group> </Group>
</Dropzone> </Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && ( {previewImage && (
<Box mt="sm"> <Box mt="sm" style={{ textAlign: 'center' }}>
<Image <Image
src={previewImage} src={previewImage}
alt="Preview" alt="Preview Gambar"
style={{ radius="md"
maxWidth: '100%', style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/> />
</Box> </Box>
)} )}
</Box>
</Box>
</Box>
<TextInput <TextInput
value={stateProgramInovasi.create.form.name || ''} label="Nama Program Inovasi"
onChange={(val) => { placeholder="Masukkan nama program inovasi"
stateProgramInovasi.create.form.name = val.target.value; value={stateProgramInovasi.create.form.name}
}} onChange={(e) => (stateProgramInovasi.create.form.name = e.target.value)}
label={<Text fw={"bold"} fz={"sm"}>Nama Program Inovasi</Text>} required
placeholder='Masukkan nama program inovasi'
/> />
<TextInput
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<CreateEditor
value={stateProgramInovasi.create.form.description || ''} value={stateProgramInovasi.create.form.description || ''}
onChange={(val) => { onChange={(htmlContent: string) => {
stateProgramInovasi.create.form.description = val.target.value; stateProgramInovasi.create.form.description = htmlContent;
}} }}
label={<Text fw={"bold"} fz={"sm"}>Deskripsi</Text>}
placeholder='Masukkan deskripsi'
/> />
</Box>
<TextInput <TextInput
label="Link Program Inovasi"
placeholder="Masukkan link program inovasi (opsional)"
value={stateProgramInovasi.create.form.link || ''} value={stateProgramInovasi.create.form.link || ''}
onChange={(val) => { onChange={(e) => (stateProgramInovasi.create.form.link = e.target.value)}
stateProgramInovasi.create.form.link = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Link</Text>}
placeholder='Masukkan link'
/> />
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button> <Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -1,22 +1,22 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import profileLandingPageState from '../../../_state/landing-page/profile'; import profileLandingPageState from '../../../_state/landing-page/profile';
function ProgramInovasi() { function ProgramInovasi() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
return ( return (
<Box> <Box px="md" py="lg">
<HeaderSearch <HeaderSearch
title='Program Inovasi' title="Program Inovasi"
placeholder='pencarian' placeholder="Cari program inovasi..."
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -27,107 +27,118 @@ 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 { const { data, page, totalPages, loading, load } = stateProgramInovasi.findMany;
data,
page,
totalPages,
loading,
load,
} = stateProgramInovasi.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search); load(page, 10, search);
}, [page, search]); }, [page, search]);
const filteredData = data || [] const filteredData = data || [];
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={20}>
<Skeleton height={550} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
); );
} }
if (data.length === 0) {
return ( return (
<Box py={10}> <Box py={15}>
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
<JudulList <Box mb="md" display="flex"
title='List Program Inovasi' style={{ justifyContent: 'space-between', alignItems: 'center' }}
href='/admin/landing-page/profile/program-inovasi/create' >
/> <Title order={4}>Daftar Program Inovasi</Title>
<Box style={{ overflowX: "auto" }}> <Tooltip label="Tambah Program Inovasi" withArrow>
<Table striped withTableBorder withRowBorders> <Button
color="blue"
leftSection={<IconPlus size={18} />}
variant="light"
radius="md"
onClick={() => router.push('/admin/landing-page/profile/program-inovasi/create')}
>
Tambah Program
</Button>
</Tooltip>
</Box>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover striped verticalSpacing="sm">
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Nama Program</TableTh> <TableTh>Nama Program</TableTh>
<TableTh>Deskripsi</TableTh> <TableTh>Deskripsi</TableTh>
<TableTh>Link</TableTh> <TableTh>Link</TableTh>
<TableTh>Detail</TableTh> <TableTh>Aksi</TableTh>
</TableTr>
</TableThead>
</Table>
</Box>
</Paper>
</Box>
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Program Inovasi'
href='/admin/landing-page/profile/program-inovasi/create'
/>
<Box style={{ overflowY: "auto" }}>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Program</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Link</TableTh>
<TableTh>Detail</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item) => ( {filteredData.length === 0 ? (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">Belum ada data program inovasi</Text>
</Center>
</TableTd>
</TableTr>
) : (
filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd w={200}>{item.description}</TableTd>
<TableTd> <TableTd>
<Box w={250}> <Text fw={500}>{item.name}</Text>
<a style={{ color: "black" }} href={item.link} target="_blank" rel="noopener noreferrer"> </TableTd>
<Text truncate fz={'sm'}>{item.link}</Text> <TableTd style={{ maxWidth: 250 }}>
<Text fz="sm" lineClamp={2}>
{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> </a>
</Box> </Tooltip>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button onClick={() => router.push(`/admin/landing-page/profile/program-inovasi/${item.id}`)}> <Button
variant="light"
color="blue"
radius="md"
onClick={() =>
router.push(`/admin/landing-page/profile/program-inovasi/${item.id}`)
}
>
<IconDeviceImac size={20} /> <IconDeviceImac size={20} />
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} ))
)}
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
</Paper> {filteredData.length > 0 && (
<Center> <Center mt="md">
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => { onChange={(newPage) => {
load(newPage, 10); load(newPage, 10);
window.scrollTo(0, 0); window.scrollTo({ top: 0, behavior: 'smooth' });
}} }}
total={totalPages} total={totalPages}
mt="md" color="blue"
mb="md"
/> />
</Center> </Center>
)}
</Paper>
</Box> </Box>
); );
} }

View File

@@ -7,18 +7,23 @@ import {
AppShellHeader, AppShellHeader,
AppShellMain, AppShellMain,
AppShellNavbar, AppShellNavbar,
Box,
Burger, Burger,
Flex, Flex,
Group, Group,
Image, Image,
NavLink, NavLink,
ScrollArea, ScrollArea,
Text Text,
Tooltip,
rem
} from "@mantine/core"; } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks"; import { useDisclosure } from "@mantine/hooks";
import { IconChevronLeft, IconChevronRight, IconDoorExit } from "@tabler/icons-react"; import {
import _ from 'lodash'; IconChevronLeft,
IconChevronRight,
IconDoorExit,
} from "@tabler/icons-react";
import _ from "lodash";
import Link from "next/link"; import Link from "next/link";
import { useRouter, useSelectedLayoutSegments } from "next/navigation"; import { useRouter, useSelectedLayoutSegments } from "next/navigation";
import { navBar } from "./_com/list_PageAdmin"; import { navBar } from "./_com/list_PageAdmin";
@@ -26,69 +31,97 @@ import { navBar } from "./_com/list_PageAdmin";
export default function Layout({ children }: { children: React.ReactNode }) { export default function Layout({ children }: { children: React.ReactNode }) {
const [opened, { toggle }] = useDisclosure(); const [opened, { toggle }] = useDisclosure();
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true); const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
const router = useRouter() const router = useRouter();
// Normalisasi semua segmen jadi lowercase const segments = useSelectedLayoutSegments().map((s) => _.lowerCase(s));
const segments = useSelectedLayoutSegments().map(s => _.lowerCase(s));
return ( return (
<AppShell <AppShell
suppressHydrationWarning suppressHydrationWarning
header={{ height: 60 }} header={{ height: 64 }}
navbar={{ navbar={{
width: 300, width: 300,
breakpoint: 'sm', breakpoint: "sm",
collapsed: { collapsed: {
mobile: !opened, mobile: !opened,
desktop: !desktopOpened, desktop: !desktopOpened,
}, },
}} }}
padding={'md'} padding="md"
> >
<AppShellHeader bg={colors["white-1"]}> <AppShellHeader
<Group px={10} align="center"> style={{
<Flex align="center" gap={'xs'}> background: "linear-gradient(90deg, #ffffff, #f9fbff)",
borderBottom: `1px solid ${colors["blue-button"]}20`,
}}
>
<Group px="md" h="100%" justify="space-between">
<Flex align="center" gap="sm">
<Image <Image
py={5} src="/assets/images/darmasaba-icon.png"
src={'/assets/images/darmasaba-icon.png'} alt="Logo Darmasaba"
alt="" width={46}
width={50} height={46}
height={50} radius="md"
/> />
<Text fw={'bold'} c={colors["blue-button"]} fz={'lg'}> <Text
Dashboard Admin fw={700}
c={colors["blue-button"]}
fz="lg"
style={{ letterSpacing: rem(0.3) }}
>
Admin Darmasaba
</Text> </Text>
</Flex> </Flex>
<Group gap="xs">
{!desktopOpened && ( {!desktopOpened && (
<ActionIcon variant="light" onClick={toggleDesktop}> <Tooltip label="Buka Navigasi" position="bottom" withArrow>
<ActionIcon
variant="light"
radius="xl"
size="lg"
onClick={toggleDesktop}
color={colors["blue-button"]}
>
<IconChevronRight /> <IconChevronRight />
</ActionIcon> </ActionIcon>
</Tooltip>
)} )}
<Burger <Burger
opened={opened} opened={opened}
onClick={toggle} onClick={toggle}
hiddenFrom="sm" hiddenFrom="sm"
size={'sm'} size="sm"
color={colors["blue-button"]}
/> />
<Box>
<ActionIcon onClick={() => { <Tooltip label="Kembali ke Website Desa" position="bottom" withArrow>
router.push("/darmasaba")
}} color={colors["blue-button"]} radius={'xl'}>
<IconDoorExit size={24} />
</ActionIcon>
</Box>
<ActionIcon <ActionIcon
w={50} onClick={() => {
h={50} router.push("/darmasaba");
variant="transparent" }}
component={Link} color={colors["blue-button"]}
href="/admin" radius="xl"
size="lg"
variant="gradient"
gradient={{ from: colors["blue-button"], to: "#228be6" }}
> >
<IconDoorExit size={22} />
</ActionIcon> </ActionIcon>
</Tooltip>
</Group>
</Group> </Group>
</AppShellHeader> </AppShellHeader>
<AppShellNavbar c={colors["blue-button"]} component={ScrollArea}> <AppShellNavbar
<AppShell.Section> component={ScrollArea}
style={{
background: "#ffffff",
borderRight: `1px solid ${colors["blue-button"]}20`,
}}
>
<AppShell.Section p="sm">
{navBar.map((v, k) => { {navBar.map((v, k) => {
const isParentActive = segments.includes(_.lowerCase(v.name)); const isParentActive = segments.includes(_.lowerCase(v.name));
@@ -96,26 +129,42 @@ export default function Layout({ children }: { children: React.ReactNode }) {
<NavLink <NavLink
key={k} key={k}
defaultOpened={isParentActive} defaultOpened={isParentActive}
c={isParentActive ? colors["blue-button"] : "grey"} c={isParentActive ? colors["blue-button"] : "gray"}
label={ label={
<Text style={{ fontWeight: isParentActive ? "bold" : "normal" }}> <Text fw={isParentActive ? 600 : 400} fz="sm">
{v.name} {v.name}
</Text> </Text>
} }
style={{
borderRadius: rem(10),
marginBottom: rem(4),
transition: "background 150ms ease",
}}
variant="light"
active={isParentActive}
> >
{v.children.map((child, key) => { {v.children.map((child, key) => {
const isChildActive = segments.includes(_.lowerCase(child.name)); const isChildActive = segments.includes(
_.lowerCase(child.name)
);
return ( return (
<NavLink <NavLink
key={key} key={key}
href={child.path} href={child.path}
c={isChildActive ? colors["blue-button"] : "grey"} c={isChildActive ? colors["blue-button"] : "gray"}
label={ label={
<Text style={{ fontWeight: isChildActive ? "bold" : "normal" }}> <Text fw={isChildActive ? 600 : 400} fz="sm">
{child.name} {child.name}
</Text> </Text>
} }
style={{
borderRadius: rem(8),
marginBottom: rem(2),
transition: "background 150ms ease",
}}
active={isChildActive}
component={Link}
/> />
); );
})} })}
@@ -124,16 +173,35 @@ export default function Layout({ children }: { children: React.ReactNode }) {
})} })}
</AppShell.Section> </AppShell.Section>
<AppShell.Section py={20}> <AppShell.Section py="md">
<Group justify="end"> <Group justify="end" pr="sm">
<ActionIcon variant="light" onClick={toggleDesktop}> <Tooltip
label={desktopOpened ? "Tutup Navigasi" : "Buka Navigasi"}
position="top"
withArrow
>
<ActionIcon
variant="light"
radius="xl"
size="lg"
onClick={toggleDesktop}
color={colors["blue-button"]}
>
<IconChevronLeft /> <IconChevronLeft />
</ActionIcon> </ActionIcon>
</Tooltip>
</Group> </Group>
</AppShell.Section> </AppShell.Section>
</AppShellNavbar> </AppShellNavbar>
<AppShellMain bg={colors.Bg}>{children}</AppShellMain> <AppShellMain
style={{
background: "linear-gradient(180deg, #fdfdfd, #f6f9fc)",
minHeight: "100vh",
}}
>
{children}
</AppShellMain>
</AppShell> </AppShell>
); );
} }

View File

@@ -1,46 +1,25 @@
'use client' 'use client'
import beasiswaDesaState from '@/app/admin/(dashboard)/_state/pendidikan/beasiswa-desa';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Group, Image, Modal, Paper, Select, SimpleGrid, Stack, Stepper, StepperStep, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Center, Group, Image, Modal, Paper, Select, SimpleGrid, Stack, Stepper, StepperStep, Text, TextInput, Title } from '@mantine/core';
import { useState } from 'react';
import BackButton from '../../desa/layanan/_com/BackButto';
import { useDisclosure } from '@mantine/hooks'; import { useDisclosure } from '@mantine/hooks';
import { IconArrowRight, IconCoin, IconInfoCircle, IconSchool, IconUsers } from '@tabler/icons-react';
import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import beasiswaDesaState from '@/app/admin/(dashboard)/_state/pendidikan/beasiswa-desa'; import BackButton from '../../desa/layanan/_com/BackButto';
const dataBeasiswa = [ const dataBeasiswa = [
{ { id: 1, nama: 'Penerima Beasiswa', jumlah: '250+', icon: IconUsers },
id: 1, { id: 2, nama: 'Peluang Kelulusan', jumlah: '90%', icon: IconSchool },
nama: 'Penerima Beasiswa', { id: 3, nama: 'Dana Tersalurkan', jumlah: '1.5M', icon: IconCoin },
jumlah: '250+' ];
},
{
id: 2,
nama: 'Peluang Kelulusan',
jumlah: '90%'
},
{
id: 3,
nama: 'Dana Tersalurkan',
jumlah: '1.5M'
},
]
const dataProgram = [ const dataProgram = [
{ { id: 1, judul: "Pelatihan SoftSkill", deskripsi: "Pengembangan diri untuk mempersiapkan karir masa depan." },
id: 1, { id: 2, judul: "Peningkatan Akses Pendidikan", deskripsi: "Memberi kesempatan bagi masyarakat kurang mampu untuk tetap sekolah." },
judul: "Pelatihan SoftSkill", { id: 3, judul: "Pendampingan Intensif", deskripsi: "Bimbingan dari mentor berpengalaman untuk mendukung akademik." },
deskripsi: "Program pengembangan diri untuk mempersiapkan karir masa depan", ];
},
{
id: 2,
judul: "Peningkatan Akses Pendidikan ",
deskripsi: "Program yang menjangkau masyarakat kurang mampu secara finansial, mengurangi angka putus sekolah",
},
{
id: 3,
judul: "Pendampingan Intensif",
deskripsi: "Program dengan mentor berpengalaman yang membimbing dalam perjalanan akademik",
}
]
function Page() { function Page() {
const beasiswaDesa = useProxy(beasiswaDesaState.beasiswaPendaftar) const beasiswaDesa = useProxy(beasiswaDesaState.beasiswaPendaftar)
const [opened, { open, close }] = useDisclosure(false); const [opened, { open, close }] = useDisclosure(false);
@@ -60,267 +39,173 @@ function Page() {
statusPernikahan: "", statusPernikahan: "",
ukuranBaju: "", ukuranBaju: "",
}; };
} };
const handleSubmit = async () => { const handleSubmit = async () => {
await beasiswaDesa.create.create(); await beasiswaDesa.create.create();
resetForm(); resetForm();
close(); close();
} };
const [active, setActive] = useState(1); const [active, setActive] = useState(1);
const nextStep = () => setActive((current) => (current < 5 ? current + 1 : current)); const nextStep = () => setActive((current) => (current < 5 ? current + 1 : current));
const prevStep = () => setActive((current) => (current > 0 ? current - 1 : current)); const prevStep = () => setActive((current) => (current > 0 ? current - 1 : current));
return ( return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}> <Stack pos="relative" bg={colors.Bg} py="xl" gap={40}>
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
<BackButton /> <BackButton />
</Box> </Box>
{/* Page 1 */}
<Box px={{ base: 'md', md: 100 }} pb={50}> <Box px={{ base: 'md', md: 100 }} pb={50}>
<SimpleGrid <SimpleGrid cols={{ base: 1, md: 2 }} spacing="xl">
cols={{
base: 1,
md: 2
}}
>
<Box> <Box>
<Title fz={55} fw={'bold'} c={colors['blue-button']}> <Title fz={55} fw={900} c={colors['blue-button']}>
Wujudkan Mimpi Pendidikanmu di Desa Darmasaba Wujudkan Mimpi Pendidikanmu di Desa Darmasaba
</Title> </Title>
<Text fz={'xl'} > <Text fz="lg" mt="md" c="dimmed">
Program beasiswa komprehensif untuk mendukung pendidikan berkualitas bagi putra-putri Desa Darmasaba. Program beasiswa untuk mendukung pendidikan berkualitas bagi generasi muda Desa Darmasaba.
</Text> </Text>
<SimpleGrid <Group mt="xl">
mt={10} <Button size="lg" radius="xl" bg={colors['blue-button']} rightSection={<IconArrowRight size={20} />} onClick={open}>
cols={{ Daftar Sekarang
base: 1, </Button>
md: 2 <Button size="lg" radius="xl" variant="light" color={colors['blue-button']} rightSection={<IconInfoCircle size={20} />}>
}} Pelajari Lebih Lanjut
> </Button>
<Button bg={colors['blue-button']} fz={'lg'} onClick={open}>Daftar Sekarang</Button> </Group>
<Button bg={colors['blue-button-trans']} fz={'lg'}>Pelajari Lebih Lanjut</Button>
</SimpleGrid>
</Box> </Box>
<Box> <Box>
<Image alt='' src={'/api/img/beasiswa-siswa.png'} /> <Image alt="Beasiswa Desa" src="/api/img/beasiswa-siswa.png" radius="lg" />
</Box> </Box>
</SimpleGrid> </SimpleGrid>
<SimpleGrid mt={30}
cols={{ <SimpleGrid mt={50} cols={{ base: 1, md: 3 }} spacing="lg">
base: 1,
md: 3
}}
>
{dataBeasiswa.map((v, k) => { {dataBeasiswa.map((v, k) => {
const IconComp = v.icon;
return ( return (
<Box key={k}> <Paper key={k} p="xl" radius="xl" shadow="md" bg={colors['white-trans-1']} withBorder>
<Paper p={'xl'} bg={colors['white-trans-1']}> <Stack align="center" gap="sm">
<Title ta={'center'} fz={55} fw={'bold'} c={colors['blue-button']}> <IconComp size={45} color={colors['blue-button']} />
{v.jumlah} <Title fz={42} fw={900} c={colors['blue-button']}>{v.jumlah}</Title>
</Title> <Text fz="sm" ta="center">{v.nama}</Text>
<Text ta={'center'}> </Stack>
{v.nama}
</Text>
</Paper> </Paper>
</Box> );
)
})} })}
</SimpleGrid> </SimpleGrid>
</Box> </Box>
{/* ---- */}
<Box px={{ base: 'md', md: 100 }} pb={20}> <Box px={{ base: 'md', md: 100 }} pb={20}>
<Title pb={20} ta={'center'} order={1} fw={'bold'} c={colors['blue-button']}> <Title pb={20} ta="center" order={1} fw={900} c={colors['blue-button']}>
Keunggulan Program Keunggulan Program
</Title> </Title>
<Paper p={'xl'} bg={colors['white-trans-1']}> <SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg">
<SimpleGrid {dataProgram.map((v, k) => (
cols={{ <Paper key={k} p="xl" radius="xl" shadow="sm" bg={colors['white-trans-1']}>
base: 1, <Title order={3} fw={700} c={colors['blue-button']} mb="xs">{v.judul}</Title>
md: 3 <Text fz="sm" c="dimmed">{v.deskripsi}</Text>
}}
>
{dataProgram.map((v, k) => {
return (
<Box key={k}>
<Title order={2} fw={'bold'} c={colors['blue-button']}>
{v.judul}
</Title>
<Text>
{v.deskripsi}
</Text>
{/* <Divider orientation="vertical" size="md" h="auto" /> */}
</Box>
)
})}
</SimpleGrid>
</Paper> </Paper>
<Title py={20} ta={'center'} order={1} fw={'bold'} c={colors['blue-button']}> ))}
</SimpleGrid>
<Title py={40} ta="center" order={1} fw={900} c={colors['blue-button']}>
Timeline Pendaftaran Timeline Pendaftaran
</Title> </Title>
<Center> <Center>
<Stepper mt={20} active={active} onStepClick={setActive} orientation="vertical" allowNextStepsSelect={false}> <Stepper mt={20} active={active} onStepClick={setActive} orientation="vertical" allowNextStepsSelect={false}>
<StepperStep label="Pembukaan Pendaftaran 1 Maret 2025" description="" /> <StepperStep label="1 Maret 2025" description="Pembukaan Pendaftaran" />
<StepperStep label="Seleksi Administrasi 15 Maret 2025" description="" /> <StepperStep label="15 Maret 2025" description="Seleksi Administrasi" />
<StepperStep label="Tes Potensi Akademik 1 April 2025" description="" /> <StepperStep label="1 April 2025" description="Tes Potensi Akademik" />
<StepperStep label="Wawancara 15 April 2025" description="" /> <StepperStep label="15 April 2025" description="Wawancara" />
<StepperStep label="Pengumuman 1 Mei 2025" description="" /> <StepperStep label="1 Mei 2025" description="Pengumuman Hasil" />
</Stepper> </Stepper>
</Center> </Center>
<Group justify="center" mt="xl"> <Group justify="center" mt="xl">
<Button variant="default" onClick={prevStep}>Back</Button> <Button variant="default" radius="xl" onClick={prevStep}>Kembali</Button>
<Button onClick={nextStep}>Next step</Button> <Button radius="xl" bg={colors['blue-button']} onClick={nextStep}>Lanjut</Button>
</Group> </Group>
</Box> </Box>
<Modal <Modal
opened={opened} opened={opened}
onClose={close} onClose={close}
radius={0} radius="xl"
size="lg"
transitionProps={{ transition: 'fade', duration: 200 }} transitionProps={{ transition: 'fade', duration: 200 }}
title={
<Text fz="xl" fw={800} c={colors['blue-button']}>
Formulir Beasiswa
</Text>
}
> >
<Paper p={"md"} withBorder> <Paper p="lg" radius="xl" withBorder shadow="sm">
<Stack gap={"xs"}> <Stack gap="sm">
<Title order={3}>Ajukan Beasiswa</Title>
<TextInput <TextInput
label={<Text fz={"sm"} fw={"bold"}>Nama</Text>} label="Nama Lengkap"
placeholder="masukkan nama" placeholder="Masukkan nama lengkap"
onChange={(val) => { onChange={(val) => { beasiswaDesa.create.form.namaLengkap = val.target.value }} />
beasiswaDesa.create.form.namaLengkap = val.target.value
}}
/>
<TextInput <TextInput
type='number' type="number"
label={<Text fz={"sm"} fw={"bold"}>NIK</Text>} label="NIK"
placeholder="masukkan nik" placeholder="Masukkan NIK"
onChange={(val) => { onChange={(val) => { beasiswaDesa.create.form.nik = val.target.value }} />
beasiswaDesa.create.form.nik = val.target.value
}}
/>
<TextInput <TextInput
label={<Text fz={"sm"} fw={"bold"}>Tempat Lahir</Text>} label="Tempat Lahir"
placeholder="masukkan tempat lahir" placeholder="Masukkan tempat lahir"
onChange={(val) => { onChange={(val) => { beasiswaDesa.create.form.tempatLahir = val.target.value }} />
beasiswaDesa.create.form.tempatLahir = val.target.value
}}
/>
<TextInput <TextInput
type='date' type="date"
label={<Text fz={"sm"} fw={"bold"}>Tanggal Lahir</Text>} label="Tanggal Lahir"
placeholder="masukkan tanggal lahir" placeholder="Pilih tanggal lahir"
onChange={(val) => { onChange={(val) => { beasiswaDesa.create.form.tanggalLahir = val.target.value }} />
beasiswaDesa.create.form.tanggalLahir = val.target.value
}}
/>
<Select <Select
label={<Text fz={"sm"} fw={"bold"}>Jenis Kelamin</Text>} label="Jenis Kelamin"
placeholder="Pilih jenis kelamin" placeholder="Pilih jenis kelamin"
data={[ data={[{ value: "LAKI_LAKI", label: "Laki-laki" }, { value: "PEREMPUAN", label: "Perempuan" }]}
{ value: "LAKI_LAKI", label: "Laki-laki" }, onChange={(val) => { if (val) beasiswaDesa.create.form.jenisKelamin = val }} />
{ value: "PEREMPUAN", label: "Perempuan" },
]}
onChange={(val) => {
if (val) beasiswaDesa.create.form.jenisKelamin = val as "LAKI_LAKI" | "PEREMPUAN";
}}
/>
<TextInput <TextInput
label={<Text fz={"sm"} fw={"bold"}>Kewarganegaraan</Text>} label="Kewarganegaraan"
placeholder="masukkan kewarganegaraan" placeholder="Masukkan kewarganegaraan"
onChange={(val) => { onChange={(val) => { beasiswaDesa.create.form.kewarganegaraan = val.target.value }} />
beasiswaDesa.create.form.kewarganegaraan = val.target.value
}}
/>
<Select <Select
label={<Text fz={"sm"} fw={"bold"}>Agama</Text>} label="Agama"
placeholder="Pilih agama" placeholder="Pilih agama"
data={[ data={[{ value: "ISLAM", label: "Islam" }, { value: "KRISTEN_PROTESTAN", label: "Kristen Protestan" }, { value: "KRISTEN_KATOLIK", label: "Kristen Katolik" }, { value: "HINDU", label: "Hindu" }, { value: "BUDDHA", label: "Buddha" }, { value: "KONGHUCU", label: "Konghucu" }, { value: "LAINNYA", label: "Lainnya" }]}
{ value: "ISLAM", label: "Islam" }, onChange={(val) => { if (val) beasiswaDesa.create.form.agama = val }} />
{ value: "KRISTEN_PROTESTAN", label: "Kristen Protestan" },
{ value: "KRISTEN_KATOLIK", label: "Kristen Katolik" },
{ value: "HINDU", label: "Hindu" },
{ value: "BUDDHA", label: "Buddha" },
{ value: "KONGHUCU", label: "Konghucu" },
{ value: "LAINNYA", label: "Lainnya" },
]}
onChange={(val) => {
if (val) beasiswaDesa.create.form.agama = val as
"ISLAM"
| "KRISTEN_PROTESTAN"
| "KRISTEN_KATOLIK"
| "HINDU"
| "BUDDHA"
| "KONGHUCU"
| "LAINNYA";
}}
/>
<TextInput <TextInput
label={<Text fz={"sm"} fw={"bold"}>Alamat KTP</Text>} label="Alamat KTP"
placeholder="masukkan alamat ktp" placeholder="Masukkan alamat sesuai KTP"
onChange={(val) => { onChange={(val) => { beasiswaDesa.create.form.alamatKTP = val.target.value }} />
beasiswaDesa.create.form.alamatKTP = val.target.value
}}
/>
<TextInput <TextInput
label={<Text fz={"sm"} fw={"bold"}>Alamat Domisili</Text>} label="Alamat Domisili"
placeholder="masukkan alamat domisili" placeholder="Masukkan alamat domisili"
onChange={(val) => { onChange={(val) => { beasiswaDesa.create.form.alamatDomisili = val.target.value }} />
beasiswaDesa.create.form.alamatDomisili = val.target.value
}}
/>
<TextInput <TextInput
type='number' type="number"
label={<Text fz={"sm"} fw={"bold"}>No Hp</Text>} label="Nomor HP"
placeholder="masukkan no hp" placeholder="Masukkan nomor HP"
onChange={(val) => { onChange={(val) => { beasiswaDesa.create.form.noHp = val.target.value }} />
beasiswaDesa.create.form.noHp = val.target.value
}}
/>
<TextInput <TextInput
type='email' type="email"
label={<Text fz={"sm"} fw={"bold"}>Email</Text>} label="Email"
placeholder="masukkan email" placeholder="Masukkan alamat email"
onChange={(val) => { onChange={(val) => { beasiswaDesa.create.form.email = val.target.value }} />
beasiswaDesa.create.form.email = val.target.value
}}
/>
<Select <Select
label={<Text fz={"sm"} fw={"bold"}>Status Pernikahan</Text>} label="Status Pernikahan"
placeholder="Pilih status pernikahan" placeholder="Pilih status pernikahan"
data={[ data={[{ value: "BELUM_MENIKAH", label: "Belum Menikah" }, { value: "MENIKAH", label: "Menikah" }, { value: "JANDA_DUDA", label: "Janda/Duda" }]}
{ value: "BELUM_MENIKAH", label: "Belum Menikah" }, onChange={(val) => { if (val) beasiswaDesa.create.form.statusPernikahan = val }} />
{ value: "MENIKAH", label: "Menikah" },
{ value: "JANDA_DUDA", label: "Janda/Duda" },
]}
onChange={(val) => {
if (val) beasiswaDesa.create.form.statusPernikahan = val as
"BELUM_MENIKAH"
| "MENIKAH"
| "JANDA_DUDA";
}}
/>
<Select <Select
label={<Text fz={"sm"} fw={"bold"}>Ukuran Baju</Text>} label="Ukuran Baju"
placeholder="Pilih ukuran baju" placeholder="Pilih ukuran baju"
data={[ data={[{ value: "S", label: "S" }, { value: "M", label: "M" }, { value: "L", label: "L" }, { value: "XL", label: "XL" }, { value: "XXL", label: "XXL" }, { value: "LAINNYA", label: "Lainnya" }]}
{ value: "S", label: "S" }, onChange={(val) => { if (val) beasiswaDesa.create.form.ukuranBaju = val }} />
{ value: "M", label: "M" }, <Group justify="flex-end" mt="md">
{ value: "L", label: "L" }, <Button variant="default" radius="xl" onClick={close}>Batal</Button>
{ value: "XL", label: "XL" }, <Button radius="xl" bg={colors['blue-button']} onClick={handleSubmit}>Kirim</Button>
{ value: "XXL", label: "XXL" }, </Group>
{ value: "LAINNYA", label: "Lainnya" },
]}
onChange={(val) => {
if (val) beasiswaDesa.create.form.ukuranBaju = val as
"S"
| "M"
| "L"
| "XL"
| "XXL"
| "LAINNYA";
}}
/>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
</Stack> </Stack>
</Paper> </Paper>
</Modal> </Modal>

View File

@@ -1,67 +1,97 @@
'use client'
import stateBimbinganBelajarDesa from '@/app/admin/(dashboard)/_state/pendidikan/bimbingan-belajar-desa';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Stack, Box, Title, Text, SimpleGrid, Paper, List, ListItem } from '@mantine/core'; import { Box, Paper, SimpleGrid, Skeleton, Stack, Text, Title, Tooltip, Divider, Badge } from '@mantine/core';
import React from 'react'; import { useShallowEffect } from '@mantine/hooks';
import { useProxy } from 'valtio/utils';
import { IconMapPin, IconCalendarTime, IconBook2 } from '@tabler/icons-react';
import BackButton from '../../desa/layanan/_com/BackButto'; import BackButton from '../../desa/layanan/_com/BackButto';
function Page() { function Page() {
const stateTujuanProgram = useProxy(stateBimbinganBelajarDesa.stateTujuanProgram);
const stateLokasiDanJadwal = useProxy(stateBimbinganBelajarDesa.lokasiDanJadwalState);
const stateFasilitas = useProxy(stateBimbinganBelajarDesa.fasilitasYangDisediakanState);
useShallowEffect(() => {
stateTujuanProgram.findById.load('edit');
stateLokasiDanJadwal.findById.load('edit');
stateFasilitas.findById.load('edit');
}, []);
if (!stateTujuanProgram.findById.data || !stateLokasiDanJadwal.findById.data || !stateFasilitas.findById.data)
return ( return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}> <Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
<BackButton /> <BackButton />
</Box> </Box>
<Box px={{ base: 'md', md: 100 }} pb={50}> <Box px={{ base: 'md', md: 100 }} pb={50}>
<Box> <Skeleton h={60} radius="xl" />
<Title ta={'center'} order={1} fw={'bold'} c={colors['blue-button']}> <Skeleton h={200} mt="lg" radius="md" />
Bimbingan Belajar Desa </Box>
</Stack>
);
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="xl">
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 120 }} pb={80}>
<Box mb="lg">
<Title ta="center" order={1} fw="bold" c={colors['blue-button']} fz={{ base: 28, md: 38 }}>
Program Bimbingan Belajar Desa
</Title> </Title>
<Text pb={20} ta={'justify'} fz={'xl'} px={{ base: 'md', md: 100 }}> <Divider size="sm" my="md" mx="auto" w="60%" color={colors['blue-button']} />
Bimbingan Belajar Desa merupakan program unggulan untuk membantu siswa-siswi di Desa Darmasaba dalam memahami pelajaran sekolah, meningkatkan prestasi akademik, serta membangun semangat belajar yang tinggi sejak dini. <Text ta="center" fz="lg" c="dimmed" px={{ base: 'sm', md: 120 }}>
Program unggulan untuk mendukung siswa Desa Darmasaba memahami pelajaran sekolah, meningkatkan prestasi akademik, dan menumbuhkan semangat belajar sejak dini.
</Text> </Text>
</Box> </Box>
<SimpleGrid <SimpleGrid cols={{ base: 1, md: 3 }} spacing="xl">
cols={{ <Paper p="xl" radius="lg" shadow="md" withBorder bg={colors['white-trans-1']}>
base: 1, <Stack gap="sm">
md: 3
}}
>
<Box> <Box>
<Paper p={'xl'} radius={'md'} bg={colors['white-trans-1']}> <Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
<Title order={2} fw={'bold'} c={colors['blue-button']}>
Tujuan Program Tujuan Program
</Title> </Badge>
<List> <Tooltip label="Gambaran manfaat utama program" position="top-start" withArrow>
<ListItem fz={'h4'}>Memberikan pendampingan belajar secara gratis bagi siswa SD hingga SMP</ListItem>
<ListItem fz={'h4'}>Membantu siswa dalam menghadapi ujian dan menyelesaikan tugas sekolah</ListItem>
<ListItem fz={'h4'}>Menumbuhkan kepercayaan diri dan kemandirian dalam belajar</ListItem>
<ListItem fz={'h4'}>Meningkatkan kesetaraan pendidikan untuk seluruh anak desa</ListItem>
</List>
</Paper>
</Box>
<Box> <Box>
<Paper h={{base: 0, md: 324}} p={'xl'} radius={'md'} bg={colors['white-trans-1']}> <IconBook2 size={36} stroke={1.5} color={colors['blue-button']} />
<Title order={2} fw={'bold'} c={colors['blue-button']}>
Lokasi dan Jadwal
</Title>
<List>
<ListItem fz={'h4'}>Lokasi: Balai Banjar / Balai Desa Darmasaba / Perpustakaan Desa</ListItem>
<ListItem fz={'h4'}>Jadwal: Setiap hari Senin, Rabu, dan Jumat pukul 16.0018.00 WITA</ListItem>
<ListItem fz={'h4'}>Peserta: Terbuka untuk semua siswa SDSMP di wilayah desa</ListItem>
</List>
</Paper>
</Box> </Box>
</Tooltip>
</Box>
<Text fz="md" lh={1.6} dangerouslySetInnerHTML={{ __html: stateTujuanProgram.findById.data?.deskripsi }} />
</Stack>
</Paper>
<Paper p="xl" radius="lg" shadow="md" withBorder bg={colors['white-trans-1']}>
<Stack gap="sm">
<Box> <Box>
<Paper h={{base: 0, md: 324}} p={'xl'} radius={'md'} bg={colors['white-trans-1']}> <Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
<Title order={2} fw={'bold'} c={colors['blue-button']}> Lokasi & Jadwal
Fasilitas yang Disediakan </Badge>
</Title> <Tooltip label="Tempat dan waktu pelaksanaan" position="top-start" withArrow>
<List> <Box>
<ListItem fz={'h4'}>Buku-buku pelajaran dan alat tulis</ListItem> <IconMapPin size={36} stroke={1.5} color={colors['blue-button']} />
<ListItem fz={'h4'}>Ruang belajar nyaman dan kondusif</ListItem>
<ListItem fz={'h4'}>Modul latihan dan pendampingan tugas</ListItem>
<ListItem fz={'h4'}>Minuman ringan dan dukungan motivasi belajar</ListItem>
</List>
</Paper>
</Box> </Box>
</Tooltip>
</Box>
<Text fz="md" lh={1.6} dangerouslySetInnerHTML={{ __html: stateLokasiDanJadwal.findById.data?.deskripsi }} />
</Stack>
</Paper>
<Paper p="xl" radius="lg" shadow="md" withBorder bg={colors['white-trans-1']}>
<Stack gap="sm">
<Box>
<Badge variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="lg" radius="sm" mb="sm">
Fasilitas
</Badge>
<Tooltip label="Sarana yang disediakan untuk peserta" position="top-start" withArrow>
<Box>
<IconCalendarTime size={36} stroke={1.5} color={colors['blue-button']} />
</Box>
</Tooltip>
</Box>
<Text fz="md" lh={1.6} dangerouslySetInnerHTML={{ __html: stateFasilitas.findById.data?.deskripsi }} />
</Stack>
</Paper>
</SimpleGrid> </SimpleGrid>
</Box> </Box>
</Stack> </Stack>

View File

@@ -1,71 +1,102 @@
import colors from '@/con/colors'; 'use client'
import { Stack, Box, Title, Paper } from '@mantine/core'; import dataPendidikan from '@/app/admin/(dashboard)/_state/pendidikan/data-pendidikan';
import React from 'react'; import { Box, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconSchool } from '@tabler/icons-react';
import { useEffect, useState } from 'react';
import { Bar, BarChart, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto'; import BackButton from '../../desa/layanan/_com/BackButto';
import { BarChart } from '@mantine/charts'; import colors from '@/con/colors';
const data = [
{
kategori: 'Jumlah penduduk usia 15-64 th yang tidak bisa baca tulis',
jumlah: 30
},
{
kategori: 'Jumlah penduduk tidak tamat SD/sederajat',
jumlah: 25
},
{
kategori: 'Jumlah penduduk tidak tamat SLTP/Sederajat',
jumlah: 20
},
{
kategori: 'Jumlah penduduk tidak tamat SLTA/Sederajat',
jumlah: 10
},
{
kategori: 'Jumlah penduduk tamat Sarjana/S1',
jumlah: 15
},
{
kategori: 'Jumlah penduduk tamat Pascsarjana',
jumlah: 30
},
]
function Page() { function Page() {
type DPMrafik = {
id: string;
name: string;
jumlah: number;
};
const stateDPM = useProxy(dataPendidikan);
const [chartData, setChartData] = useState<DPMrafik[]>([]);
const [mounted, setMounted] = useState(false);
useShallowEffect(() => {
setMounted(true);
stateDPM.findMany.load();
}, []);
useEffect(() => {
if (stateDPM.findMany.data) {
setChartData(
stateDPM.findMany.data.map((item) => ({
id: item.id,
name: item.name,
jumlah: Number(item.jumlah),
}))
);
}
}, [stateDPM.findMany.data]);
if (!stateDPM.findMany.data) {
return ( return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}> <Stack px="md" py="xl">
<Skeleton h={400} radius="lg" />
</Stack>
);
}
return (
<Stack bg="var(--mantine-color-gray-0)" py="xl" gap="lg" pos="relative">
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
<BackButton /> <BackButton />
</Box> </Box>
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
<Box pb={20}> <Stack gap="xs" align="center" pb="lg">
<Title ta={'center'} order={1} fw={'bold'} c={colors['blue-button']}> <IconSchool size={48} stroke={1.5} color={colors['blue-button']} />
Data Pendidikan <Title order={1} fw={700} ta="center" c={colors['blue-button']}>
Statistik Data Pendidikan
</Title> </Title>
</Box> <Text c="dimmed" size="sm" ta="center">
<Paper p={'xl'} radius={'md'} bg={colors['white-trans-1']}> Visualisasi jumlah pendidikan berdasarkan kategori yang tersedia
<BarChart </Text>
p={'100'} </Stack>
h={600}
data={data} {!mounted || chartData.length === 0 ? (
dataKey="kategori" <Paper radius="lg" p="xl" withBorder shadow="sm" bg="var(--mantine-color-white)">
series={[ <Stack align="center" gap="sm" justify="center" h={350}>
{ name: 'jumlah', color: colors['blue-button'] }, <IconSchool size={40} stroke={1.5} color="var(--mantine-color-gray-5)" />
]} <Title order={4} fw={600}>
tickLine="y" Belum Ada Data
xAxisProps={{ </Title>
angle: -45, // Rotate labels by -45 degrees <Text c="dimmed" size="sm">
textAnchor: 'end', // Anchor text to the end for better alignment Data pendidikan belum tersedia. Silakan tambahkan data untuk melihat grafik.
height: 100, // Increase height for rotated labels </Text>
interval: 0, // Show all labels </Stack>
style: {
fontSize: '12px', // Adjust font size if needed
overflow: 'visible',
whiteSpace: 'nowrap'
}
}}
/>
</Paper> </Paper>
) : (
<Paper radius="lg" p="xl" withBorder shadow="sm" bg="var(--mantine-color-white)">
<Title order={4} fw={600} mb="md">
Grafik Pendidikan
</Title>
<ResponsiveContainer width="100%" height={350}>
<BarChart data={chartData}>
<XAxis dataKey="name" />
<YAxis />
<Tooltip
contentStyle={{
borderRadius: 12,
background: 'var(--mantine-color-gray-0)',
border: '1px solid var(--mantine-color-gray-3)',
}}
cursor={{ fill: 'var(--mantine-color-gray-1)' }}
/>
<Legend />
<Bar dataKey="jumlah" fill={colors['blue-button']} name="Jumlah Pendidikan" radius={[8, 8, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</Paper>
)}
</Box> </Box>
</Stack> </Stack>
); );

View File

@@ -1,92 +1,107 @@
'use client'
import pendidikanNonFormalState from '@/app/admin/(dashboard)/_state/pendidikan/pendidikan-non-formal';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Stack, Box, Title, Text, SimpleGrid, Paper, List, ListItem } from '@mantine/core'; import { Box, Paper, SimpleGrid, Skeleton, Stack, Text, Title, Tooltip } from '@mantine/core';
import React from 'react'; import { useShallowEffect } from '@mantine/hooks';
import { useProxy } from 'valtio/utils';
import { IconMapPin, IconTarget, IconBook2 } from '@tabler/icons-react';
import BackButton from '../../desa/layanan/_com/BackButto'; import BackButton from '../../desa/layanan/_com/BackButto';
function Page() { function Page() {
const stateTujuanPendidikanNonFormal = useProxy(pendidikanNonFormalState.stateTujuanPendidikanNonFormal);
const stateTempatKegiatan = useProxy(pendidikanNonFormalState.stateTempatKegiatan);
const stateJenisProgram = useProxy(pendidikanNonFormalState.stateJenisProgram);
useShallowEffect(() => {
stateTujuanPendidikanNonFormal.findById.load('edit');
stateTempatKegiatan.findById.load('edit');
stateJenisProgram.findById.load('edit');
}, []);
if (!stateTujuanPendidikanNonFormal.findById.data || !stateTempatKegiatan.findById.data || !stateJenisProgram.findById.data)
return ( return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}> <Stack pos="relative" bg={colors.Bg} py="xl" gap="lg" mih="100vh" justify="flex-start">
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }} pb={50}>
<Skeleton h={50} radius="xl" />
<Skeleton h={150} mt="lg" radius="md" />
<Skeleton h={150} mt="lg" radius="md" />
</Box>
</Stack>
);
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="lg" mih="100vh">
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
<BackButton /> <BackButton />
</Box> </Box>
<Box px={{ base: 'md', md: 100 }} pb={50}> <Box px={{ base: 'md', md: 100 }} pb={50}>
<Box> <Box>
<Title ta={'center'} order={1} fw={'bold'} c={colors['blue-button']}> <Title ta="center" order={1} fw="bold" c={colors['blue-button']} mb="sm">
Pendidikan Non Formal Pendidikan Non Formal
</Title> </Title>
<Text pb={20} ta={'justify'} fz={'xl'} px={{ base: 'md', md: 100 }}> <Text ta="center" fz="lg" lh={1.6} c="black" maw={800} mx="auto">
Pendidikan Non Formal adalah bentuk pendidikan di luar sekolah yang diselenggarakan secara terstruktur dan bertujuan memberikan keterampilan, pengetahuan, serta pengembangan karakter masyarakat dari berbagai usia dan latar belakang. Bentuk pendidikan di luar sekolah yang terstruktur, bertujuan memberikan keterampilan, pengetahuan, dan pengembangan karakter masyarakat dari berbagai usia serta latar belakang.
</Text> </Text>
</Box> </Box>
<SimpleGrid <SimpleGrid
cols={{ cols={{ base: 1, md: 2 }}
base: 1, spacing="lg"
md: 2 mt={40}
}}
> >
<Box> <Paper
<Paper p={'xl'} radius={'md'} bg={colors['white-trans-1']}> p="xl"
<Title order={2} fw={'bold'} c={colors['blue-button']}> radius="lg"
bg={colors['white-trans-1']}
shadow="md"
withBorder
>
<Stack>
<Tooltip label="Fokus utama program" withArrow>
<Title order={2} fw="bold" c={colors['blue-button']} mb="xs" flex="center">
<IconTarget size={28} style={{ marginRight: 8 }} />
Tujuan Program Tujuan Program
</Title> </Title>
<List> </Tooltip>
<ListItem fz={'h4'}>Memberikan kesempatan belajar yang fleksibel bagi warga desa</ListItem> <Text fz="md" lh={1.7} c="dark" dangerouslySetInnerHTML={{ __html: stateTujuanPendidikanNonFormal.findById.data?.deskripsi }} />
<ListItem fz={'h4'}>Meningkatkan keterampilan hidup dan kemandirian ekonomi</ListItem> </Stack>
<ListItem fz={'h4'}>Mendorong partisipasi masyarakat dalam pembangunan desa</ListItem>
<ListItem fz={'h4'}>Mengurangi angka putus sekolah dan meningkatkan kualitas SDM</ListItem>
</List>
</Paper> </Paper>
</Box> <Paper
<Box> p="xl"
<Paper h={{ base: 0, md: 210 }} p={'xl'} radius={'md'} bg={colors['white-trans-1']}> radius="lg"
<Title order={2} fw={'bold'} c={colors['blue-button']}> bg={colors['white-trans-1']}
shadow="md"
withBorder
>
<Stack>
<Tooltip label="Lokasi pelaksanaan kegiatan" withArrow>
<Title order={2} fw="bold" c={colors['blue-button']} mb="xs" flex="center">
<IconMapPin size={28} style={{ marginRight: 8 }} />
Tempat Kegiatan Tempat Kegiatan
</Title> </Title>
<List> </Tooltip>
<ListItem fz={'h4'}>Balai Desa Darmasaba</ListItem> <Text fz="md" lh={1.7} c="dark" dangerouslySetInnerHTML={{ __html: stateTempatKegiatan.findById.data?.deskripsi }} />
<ListItem fz={'h4'}>TPK, Perpustakaan Desa, atau Posyandu</ListItem> </Stack>
<ListItem fz={'h4'}>Bisa juga dilakukan secara mobile atau door to door</ListItem>
</List>
</Paper> </Paper>
</Box>
</SimpleGrid> </SimpleGrid>
<Box py={20}> <Box py={40}>
<Paper p={'xl'} radius={'md'} bg={colors['white-trans-1']}> <Paper
<Title order={2} fw={'bold'} c={colors['blue-button']}> p="xl"
radius="lg"
bg={colors['white-trans-1']}
shadow="md"
withBorder
>
<Stack>
<Tooltip label="Ragam jenis program yang tersedia" withArrow>
<Title order={2} fw="bold" c={colors['blue-button']} mb="xs" flex="center">
<IconBook2 size={28} style={{ marginRight: 8 }} />
Jenis Program yang Diselenggarakan Jenis Program yang Diselenggarakan
</Title> </Title>
<Text fz={'h4'}>Program Pendidikan Non Formal yang diselenggarakan di Desa Darmasaba meliputi:</Text> </Tooltip>
<Stack> <Text fz="md" lh={1.7} c="dark" dangerouslySetInnerHTML={{ __html: stateJenisProgram.findById.data?.deskripsi }} />
<Box>
<Text fz={'h4'}> 1) Keaksaraan Fungsional</Text>
<List>
<ListItem fz={'h4'}>Untuk warga yang belum bisa membaca dan menulis</ListItem>
</List>
</Box>
<Box>
<Text fz={'h4'}> 2) Pendidikan Kesetaraan (Paket A, B, C)</Text>
<List>
<ListItem fz={'h4'}>Setara SD, SMP, dan SMA bagi yang tidak menyelesaikan pendidikan formal</ListItem>
</List>
</Box>
<Box>
<Text fz={'h4'}> 3) Pelatihan Keterampilan</Text>
<List>
<ListItem fz={'h4'}>Menjahit, memasak, sablon, pertanian, peternakan, hingga teknologi digital</ListItem>
</List>
</Box>
<Box>
<Text fz={'h4'}> 4) Kursus & Pelatihan Soft Skill</Text>
<List>
<ListItem fz={'h4'}>Public speaking, pengelolaan keuangan, kepemimpinan pemuda</ListItem>
</List>
</Box>
<Box>
<Text fz={'h4'}> 5) Pendidikan Keluarga & Parenting</Text>
<List>
<ListItem fz={'h4'}>Untuk membekali orang tua dalam mendampingi tumbuh kembang anak</ListItem>
</List>
</Box>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

@@ -1,55 +1,96 @@
'use client'
import stateProgramPendidikanAnak from '@/app/admin/(dashboard)/_state/pendidikan/program-pendidikan-anak';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Stack, Box, Title, Text, SimpleGrid, Paper, List, ListItem } from '@mantine/core'; import { Box, Paper, SimpleGrid, Skeleton, Stack, Text, Title, Tooltip, Divider, Group } from '@mantine/core';
import React from 'react'; import { useShallowEffect } from '@mantine/hooks';
import { useProxy } from 'valtio/utils';
import { IconBook2, IconTargetArrow } from '@tabler/icons-react';
import BackButton from '../../desa/layanan/_com/BackButto'; import BackButton from '../../desa/layanan/_com/BackButto';
function Page() { function Page() {
const stateUnggulan = useProxy(stateProgramPendidikanAnak.programUnggulanState);
const stateTujuan = useProxy(stateProgramPendidikanAnak.stateTujuanProgram);
useShallowEffect(() => {
stateUnggulan.findById.load('edit');
stateTujuan.findById.load('edit');
}, []);
if (!stateUnggulan.findById.data || !stateTujuan.findById.data)
return ( return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}> <Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
<BackButton /> <BackButton />
</Box> </Box>
<Box px={{ base: 'md', md: 100 }} pb={50}> <Box px={{ base: 'md', md: 100 }} pb={50}>
<Box> <Skeleton h={50} radius="xl" />
<Title ta={'center'} order={1} fw={'bold'} c={colors['blue-button']}> <Skeleton h={150} mt="lg" radius="md" />
</Box>
</Stack>
);
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }} pb={50}>
<Box mb="xl">
<Title ta="center" order={1} fw="bold" c={colors['blue-button']} mb="sm">
Program Pendidikan Anak Program Pendidikan Anak
</Title> </Title>
<Text pb={20} ta={'justify'} fz={'xl'} px={{ base: 'md', md: 100 }}> <Text ta="center" fz="lg" c="black" mb="lg" maw={800} mx="auto">
Desa Darmasaba berkomitmen untuk menciptakan generasi muda yang cerdas, berkarakter, dan berdaya saing melalui berbagai program pendidikan yang inklusif dan berkelanjutan. Pendidikan anak menjadi pondasi utama dalam mewujudkan masa depan desa yang lebih baik. Desa Darmasaba berkomitmen mencetak generasi muda yang cerdas, berkarakter, dan siap bersaing melalui program pendidikan yang inklusif dan berkelanjutan.
</Text> </Text>
<Divider size="sm" color={colors['blue-button']} mx="auto" maw={120} />
</Box> </Box>
<SimpleGrid <SimpleGrid
cols={{ cols={{ base: 1, md: 2 }}
base: 1, spacing="xl"
md: 2
}}
> >
<Box> <Paper
<Paper p={'xl'} radius={'md'} bg={colors['white-trans-1']}> p="xl"
<Title order={2} fw={'bold'} c={colors['blue-button']}> radius="xl"
withBorder
bg="white"
shadow="md"
style={{ transition: 'transform 0.2s ease', cursor: 'default' }}
>
<Stack gap="sm">
<Group gap="sm">
<IconTargetArrow size={28} color={colors['blue-button']} />
<Title order={2} fw="bold" c={colors['blue-button']}>
Tujuan Program Tujuan Program
</Title> </Title>
<List> </Group>
<ListItem fz={'h4'}>Meningkatkan akses pendidikan yang merata dan berkualitas</ListItem> <Tooltip label="Detail tujuan program pendidikan anak" position="top-start" withArrow>
<ListItem fz={'h4'}>Menumbuhkan semangat belajar sejak dini</ListItem> <Text fz="lg" lh={1.6} c="dark" dangerouslySetInnerHTML={{ __html: stateTujuan.findById.data?.deskripsi }} />
<ListItem fz={'h4'}>Membentuk karakter anak yang berakhlak dan berwawasan lingkungan</ListItem> </Tooltip>
<ListItem fz={'h4'}>Mendukung tumbuh kembang anak melalui pendekatan pendidikan yang holistik</ListItem> </Stack>
</List>
</Paper> </Paper>
</Box>
<Box> <Paper
<Paper h={{base: 0, md: 239}} p={'xl'} radius={'md'} bg={colors['white-trans-1']}> p="xl"
<Title order={2} fw={'bold'} c={colors['blue-button']}> radius="xl"
withBorder
bg="white"
shadow="md"
style={{ transition: 'transform 0.2s ease', cursor: 'default' }}
>
<Stack gap="sm">
<Group gap="sm">
<IconBook2 size={28} color={colors['blue-button']} />
<Title order={2} fw="bold" c={colors['blue-button']}>
Program Unggulan Program Unggulan
</Title> </Title>
<List> </Group>
<ListItem fz={'h4'}>Bimbingan Belajar Gratis: Untuk siswa kurang mampu</ListItem> <Tooltip label="Detail program unggulan yang sedang berjalan" position="top-start" withArrow>
<ListItem fz={'h4'}>Gerakan Literasi Desa: Meningkatkan minat baca sejak dini</ListItem> <Text fz="lg" lh={1.6} c="dark" dangerouslySetInnerHTML={{ __html: stateUnggulan.findById.data?.deskripsi }} />
<ListItem fz={'h4'}>Pelatihan Digital untuk Anak dan Remaja</ListItem> </Tooltip>
<ListItem fz={'h4'}>Beasiswa Anak Berprestasi & Kurang Mampu</ListItem> </Stack>
</List>
</Paper> </Paper>
</Box>
</SimpleGrid> </SimpleGrid>
</Box> </Box>
</Stack> </Stack>

View File

@@ -5,7 +5,7 @@ import { IconAt, IconBrandFacebook, IconBrandInstagram, IconBrandTwitter, IconBr
function Footer() { function Footer() {
return ( return (
<Stack bg="linear-gradient(180deg, #1C6EA4, #124170)" c="white"> <Stack bg="linear-gradient(180deg, #1C6EA4, #124170)" c="white">
<Box w="100%" p="xl" h={{ base: 1800, md: 1100 }}> <Box w="100%" p="xl">
<Center> <Center>
<Paper w="100%" bg="transparent" shadow="md" radius="lg" p="xl"> <Paper w="100%" bg="transparent" shadow="md" radius="lg" p="xl">
<Box component="footer"> <Box component="footer">

View File

@@ -26,8 +26,8 @@ export function Navbar() {
> >
<NavbarMainMenu listNavbar={navbarListMenu} /> <NavbarMainMenu listNavbar={navbarListMenu} />
<Stack hiddenFrom="sm" bg={colors.grey[2]} px="md" py="sm"> <Box hiddenFrom="sm" bg={colors.grey[2]} px="md" py="sm">
<Group justify="space-between"> <Group justify="space-between" wrap="nowrap">
<ActionIcon <ActionIcon
variant="transparent" variant="transparent"
size="xl" size="xl"
@@ -51,16 +51,23 @@ export function Navbar() {
</Tooltip> </Tooltip>
</Group> </Group>
{mobileOpen && ( {mobileOpen && (
<motion.div <Paper
initial={{ x: 300 }} component={motion.div}
initial={{ x: '100%' }}
animate={{ x: 0 }} animate={{ x: 0 }}
exit={{ x: '100%' }}
transition={{ duration: 0.2 }} transition={{ duration: 0.2 }}
style={{ height: "100vh" }} pos="absolute"
left={0}
right={0}
top="100%"
m={0}
radius={0}
> >
<NavbarMobile listNavbar={navbarListMenu} /> <NavbarMobile listNavbar={navbarListMenu} />
</motion.div> </Paper>
)} )}
</Stack> </Box>
</Paper> </Paper>
{(item || isSearch) && <Box className="glass" />} {(item || isSearch) && <Box className="glass" />}
</Box> </Box>
@@ -70,28 +77,34 @@ export function Navbar() {
function NavbarMobile({ listNavbar }: { listNavbar: MenuItem[] }) { function NavbarMobile({ listNavbar }: { listNavbar: MenuItem[] }) {
const router = useRouter(); const router = useRouter();
return ( return (
<ScrollArea h="100vh" offsetScrollbars> <ScrollArea.Autosize mah="calc(100vh - 80px)" offsetScrollbars>
<Stack p="lg" gap="md" style={{ backgroundColor: "rgba(255, 255, 255, 0.25)" }}> <Stack p="md" gap="xs">
{listNavbar.map((item, k) => ( {listNavbar.map((item, k) => (
<Stack key={k} gap={4}> <Box key={k}>
<Group <Group
justify="space-between" justify="space-between"
align="center" align="center"
p="xs"
onClick={() => { onClick={() => {
router.push(item.href); router.push(item.href);
stateNav.mobileOpen = false; stateNav.mobileOpen = false;
}} }}
style={{ cursor: "pointer" }} style={{ cursor: "pointer" }}
> >
<Text c="dark.9" fw={600} fz="lg"> <Text c="dark.9" fw={600} fz="md">
{item.name} {item.name}
</Text> </Text>
<IconSquareArrowRight size={20} /> <IconSquareArrowRight size={18} />
</Group> </Group>
{item.children && <NavbarMobile listNavbar={item.children} />} {item.children && (
</Stack> <Box pl="md">
<NavbarMobile listNavbar={item.children} />
</Box>
)}
</Box>
))} ))}
</Stack> </Stack>
</ScrollArea> </ScrollArea.Autosize>
); );
} }