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 */
'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation';
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 }) {
const router = useRouter()
const pathname = usePathname()
const router = useRouter();
const pathname = usePathname();
const tabs = [
{
label: "List Desa Anti Korupsi",
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",
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 tab = tabs.find(t => t.value === value)
const tab = tabs.find(t => t.value === value);
if (tab) {
router.push(tab.href)
router.push(tab.href);
}
setActiveTab(value)
}
useEffect(() => {
const match = tabs.find(tab => tab.href === pathname)
const match = tabs.find(tab => tab.href === pathname);
if (match) {
setActiveTab(match.value)
setActiveTab(match.value);
}
}, [pathname])
}, [pathname]);
return (
<Stack>
<Title order={3}>Desa Anti Korupsi</Title>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
{tabs.map((e, i) => (
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
<Stack gap="lg">
<Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
Desa Anti Korupsi
</Title>
<Tabs
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>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value}>
{/* Konten dummy, bisa diganti tergantung routing */}
<></>
{tabs.map((tab, i) => (
<TabsPanel
key={i}
value={tab.value}
style={{
padding: "1.5rem",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
borderRadius: "1rem",
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
}}
>
{children}
</TabsPanel>
))}
</Tabs>
{children}
</Stack>
);
}

View File

@@ -2,14 +2,14 @@
'use client'
import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi';
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 { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditKategoriDesaAntiKorupsi() {
export default function EditKategoriDesaAntiKorupsi() {
const router = useRouter();
const params = useParams();
const id = params?.id as string;
@@ -18,16 +18,17 @@ function EditKategoriDesaAntiKorupsi() {
const [formData, setFormData] = useState({
name: "",
});
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const loadKategorikegiatan = async () => {
const loadKategori = async () => {
if (!id) return;
setIsLoading(true);
try {
const data = await stateKategori.edit.load(id);
if (data) {
// pastikan id-nya masuk ke state edit
stateKategori.edit.id = id;
setFormData({
name: data.name || '',
@@ -36,63 +37,88 @@ function EditKategoriDesaAntiKorupsi() {
} catch (error) {
console.error("Error loading kategori desa anti korupsi:", error);
toast.error("Gagal memuat data kategori desa anti korupsi");
} finally {
setIsLoading(false);
}
};
loadKategorikegiatan();
loadKategori();
}, [id]);
const handleSubmit = async () => {
try {
if (!formData.name.trim()) {
toast.error('Nama kategori desa anti korupsi tidak boleh kosong');
return;
}
if (!formData.name.trim()) {
return toast.error('Nama kategori tidak boleh kosong');
}
try {
setIsLoading(true);
stateKategori.edit.form = {
name: formData.name.trim(),
};
// Safety check tambahan: pastikan ID tidak kosong
if (!stateKategori.edit.id) {
stateKategori.edit.id = id; // fallback
stateKategori.edit.id = id;
}
const success = await stateKategori.edit.update();
if (success) {
router.push("/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi");
}
await stateKategori.edit.update();
toast.success('Kategori berhasil diperbarui');
router.push("/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi");
} catch (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 (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<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">
Edit Kategori Desa Anti Korupsi
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Kategori Desa Anti Korupsi</Title>
<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
label="Nama Kategori"
placeholder="Masukkan nama kategori"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Desa Anti Korupsi</Text>}
placeholder='Masukkan nama kategori desa anti korupsi'
required
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>
</Stack>
</Paper>
</Box>
);
}
export default EditKategoriDesaAntiKorupsi;

View File

@@ -1,16 +1,16 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
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 { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
import korupsiState from '../../../../_state/landing-page/desa-anti-korupsi';
function CreateKategoriDesaAntiKorupsi() {
export default function CreateKategoriDesaAntiKorupsi() {
const router = useRouter();
const stateKategori = useProxy(korupsiState.kategoriDesaAntiKorupsi)
const stateKategori = useProxy(korupsiState.kategoriDesaAntiKorupsi);
useEffect(() => {
stateKategori.findMany.load();
@@ -20,42 +20,64 @@ function CreateKategoriDesaAntiKorupsi() {
stateKategori.create.form = {
name: "",
};
}
};
const handleSubmit = async () => {
if (!stateKategori.create.form.name) {
return alert('Nama kategori harus diisi');
}
await stateKategori.create.create();
resetForm();
router.push("/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi")
}
router.push("/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi");
};
return (
<Box>
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<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>
</Box>
</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={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Kategori Desa Anti Korupsi</Title>
<TextInput
value={stateKategori.create.form.name}
onChange={(val) => {
stateKategori.create.form.name = val.target.value;
<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
label="Nama Kategori"
placeholder="Masukkan nama kategori"
value={stateKategori.create.form.name || ''}
onChange={(e) => (stateKategori.create.form.name = e.target.value)}
required
/>
<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)',
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Desa Anti Korupsi</Text>}
placeholder='Masukkan nama kategori desa anti korupsi'
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateKategoriDesaAntiKorupsi;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,67 +1,110 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation';
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 }) {
const router = useRouter()
const pathname = usePathname()
const router = useRouter();
const pathname = usePathname();
const tabs = [
{
label: "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",
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",
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 tab = tabs.find(t => t.value === value)
const tab = tabs.find(t => t.value === value);
if (tab) {
router.push(tab.href)
router.push(tab.href);
}
setActiveTab(value)
}
setActiveTab(value);
};
useEffect(() => {
const match = tabs.find(tab => tab.href === pathname)
const match = tabs.find(tab => tab.href === pathname);
if (match) {
setActiveTab(match.value)
setActiveTab(match.value);
}
}, [pathname])
}, [pathname]);
return (
<Stack>
<Title order={3}>Profile</Title>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
{tabs.map((e, i) => (
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
<Stack gap="lg">
<Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
Profil Desa
</Title>
<Tabs
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>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value}>
{/* Konten dummy, bisa diganti tergantung routing */}
<></>
{tabs.map((tab, i) => (
<TabsPanel
key={i}
value={tab.value}
style={{
padding: "1.5rem",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
borderRadius: "1rem",
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
}}
>
{children}
</TabsPanel>
))}
</Tabs>
{children}
</Stack>
);
}
export default LayoutTabs;
export default LayoutTabs;

View File

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

View File

@@ -2,103 +2,132 @@
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
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 { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function DetailMediaSosial() {
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const params = useParams();
const router = useRouter();
useShallowEffect(() => {
stateMediaSosial.findUnique.load(params?.id as string)
}, [])
stateMediaSosial.findUnique.load(params?.id as string);
}, []);
const handleHapus = () => {
if (selectedId) {
stateMediaSosial.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/landing-page/profile/media-sosial")
stateMediaSosial.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
router.push("/admin/landing-page/profile/media-sosial");
}
}
};
if (!stateMediaSosial.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
<Skeleton height={500} radius="md" />
</Stack>
)
);
}
const data = stateMediaSosial.findUnique.data;
return (
<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 Media Sosial</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box py={10}>
<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 / Nama Kontak</Text>
<Text fz={"lg"}>{stateMediaSosial.findUnique.data?.name}</Text>
<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 URL / No Telephone</Text>
<Text fz={"lg"}>{stateMediaSosial.findUnique.data?.iconUrl}</Text>
<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>
<Box w={100} h={100}>
<Image src={stateMediaSosial.findUnique.data?.image?.link} alt="gambar" />
</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>
<Box>
<Flex gap={"xs"}>
<Group gap="sm">
<Tooltip label="Hapus Media Sosial" withArrow position="top">
<Button
color="red"
onClick={() => {
if (stateMediaSosial.findUnique.data) {
setSelectedId(stateMediaSosial.findUnique.data.id);
setModalHapus(true);
}
setSelectedId(data.id);
setModalHapus(true);
}}
disabled={!stateMediaSosial.findUnique.data}
color="red">
<IconX size={20} />
variant="light"
radius="md"
size="md"
>
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Media Sosial" withArrow position="top">
<Button
onClick={() => {
if (stateMediaSosial.findUnique.data) {
router.push(`/admin/landing-page/profile/media-sosial/${stateMediaSosial.findUnique.data.id}/edit`);
}
}}
disabled={!stateMediaSosial.findUnique.data}
color="green">
color="green"
onClick={() => router.push(`/admin/landing-page/profile/media-sosial/${data.id}/edit`)}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Tooltip>
</Group>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus media sosial ini?"
text="Apakah Anda yakin ingin menghapus media sosial ini?"
/>
</Box>
);

View File

@@ -1,8 +1,19 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
'use client';
import colors from '@/con/colors';
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 { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
@@ -11,9 +22,9 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import profileLandingPageState from '../../../../_state/landing-page/profile';
function CreateMediaSosial() {
export default function CreateMediaSosial() {
const router = useRouter();
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial)
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial);
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
@@ -23,27 +34,28 @@ function CreateMediaSosial() {
const resetForm = () => {
stateMediaSosial.create.form = {
name: "",
imageId: "",
iconUrl: "",
name: '',
imageId: '',
iconUrl: '',
};
setPreviewImage(null);
setFile(null);
};
const handleSubmit = async () => {
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({
file,
name: file.name,
})
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal mengupload file");
return toast.error('Gagal mengunggah gambar, silakan coba lagi');
}
stateMediaSosial.create.form.imageId = uploaded.id;
@@ -51,98 +63,108 @@ function CreateMediaSosial() {
await stateMediaSosial.create.create();
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>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<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 Media Sosial
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Media Sosial</Title>
<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>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<Text fw="bold" fz="sm" mb={6}>
Gambar Media Sosial
</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }}
radius="md"
p="xl"
>
<Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept>
<IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
</Group>
<Text ta="center" mt="sm" size="sm" color="dimmed">
Seret gambar atau klik untuk memilih file (maks 5MB)
</Text>
</Dropzone>
<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>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
{previewImage && (
<Box mt="sm" style={{ textAlign: 'center' }}>
<Image
src={previewImage}
alt="Preview Gambar"
radius="md"
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
/>
</Box>
)}
</Box>
<TextInput
label="Nama Media Sosial / Kontak"
placeholder="Masukkan nama media sosial atau kontak"
value={stateMediaSosial.create.form.name || ''}
onChange={(val) => {
stateMediaSosial.create.form.name = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Media Sosial / Nama Kontak</Text>}
placeholder='Masukkan nama media sosial / nama kontak'
onChange={(e) => (stateMediaSosial.create.form.name = e.target.value)}
required
/>
<TextInput
label="Link Media Sosial / Nomor Telepon"
placeholder="Masukkan link media sosial atau nomor telepon"
value={stateMediaSosial.create.form.iconUrl || ''}
onChange={(val) => {
stateMediaSosial.create.form.iconUrl = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Link Media Sosial / No Telephone</Text>}
placeholder='Masukkan link media sosial / no telephone'
onChange={(e) => (stateMediaSosial.create.form.iconUrl = e.target.value)}
required
/>
<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>
</Stack>
</Paper>
</Box>
);
}
export default CreateMediaSosial;

View File

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

View File

@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
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 { useProxy } from 'valtio/utils';
@@ -144,124 +144,134 @@ function EditPejabatDesa() {
return (
<Box>
<Stack gap="xs">
<Box>
<Button variant="subtle" onClick={handleBack}>
<IconArrowBack color={colors['blue-button']} size={20} />
</Button>
</Box>
<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">
Edit Pejabat Desa
</Title>
</Group>
<Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p="md" radius={10}>
<Stack gap="xs">
<Title order={3}>Edit Profile Pejabat Desa</Title>
<Paper
w={{ base: "100%", md: "50%" }}
bg={colors['white-1']}
p="md"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="xs">
<Title order={3}>Edit Profile Pejabat Desa</Title>
{/* Nama Field */}
<TextInput
label={<Text fw="bold">Nama Perbekel</Text>}
placeholder="Masukkan nama perbekel"
value={allState.edit.form.name}
onChange={(e) => handleFieldChange('name', e.currentTarget.value)}
error={!allState.edit.form.name && "Nama wajib diisi"}
/>
{/* Nama Field */}
<TextInput
label={<Text fw="bold">Nama Perbekel</Text>}
placeholder="Masukkan nama perbekel"
value={allState.edit.form.name}
onChange={(e) => handleFieldChange('name', e.currentTarget.value)}
error={!allState.edit.form.name && "Nama wajib diisi"}
/>
{/* Posisi Field */}
<TextInput
label={<Text fw="bold">Posisi</Text>}
placeholder="Masukkan posisi"
value={allState.edit.form.position}
onChange={(e) => handleFieldChange('position', e.currentTarget.value)}
error={!allState.edit.form.position && "Posisi wajib diisi"}
/>
{/* Posisi Field */}
<TextInput
label={<Text fw="bold">Posisi</Text>}
placeholder="Masukkan posisi"
value={allState.edit.form.position}
onChange={(e) => handleFieldChange('position', e.currentTarget.value)}
error={!allState.edit.form.position && "Posisi wajib diisi"}
/>
{/* File Upload */}
{/* File Upload */}
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => handleFileChange(files[0])}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<Dropzone
onDrop={(files) => handleFileChange(files[0])}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</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>
</Dropzone>
<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>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box>
{/* Preview Gambar */}
<Box>
<Text fz="sm" fw="bold" mb="xs">Preview Gambar</Text>
{previewImage ? (
<Image alt="Profile preview" src={previewImage} w={200} h={200} fit="cover" radius="md" />
) : (
<Center w={200} h={200} bg="gray.2">
<Stack align="center" gap="xs">
<IconImageInPicture size={48} color="gray" />
<Text size="sm" c="gray">Tidak ada gambar</Text>
</Stack>
</Center>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box>
{/* Submit Button */}
<Group>
<Button
bg={colors['blue-button']}
onClick={handleSubmit}
loading={isSubmitting || allState.edit.loading}
disabled={!allState.edit.form.name}
>
{isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
</Button>
{/* Preview Gambar */}
<Box>
<Text fz="sm" fw="bold" mb="xs">Preview Gambar</Text>
{previewImage ? (
<Image alt="Profile preview" src={previewImage} w={200} h={200} fit="cover" radius="md" />
) : (
<Center w={200} h={200} bg="gray.2">
<Stack align="center" gap="xs">
<IconImageInPicture size={48} color="gray" />
<Text size="sm" c="gray">Tidak ada gambar</Text>
</Stack>
</Center>
)}
</Box>
<Button
variant="outline"
onClick={handleBack}
disabled={isSubmitting || allState.edit.loading}
>
Batal
</Button>
</Group>
</Stack>
</Paper>
</Box>
{/* Submit Button */}
<Group>
<Button
bg={colors['blue-button']}
onClick={handleSubmit}
loading={isSubmitting || allState.edit.loading}
disabled={!allState.edit.form.name}
>
{isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
</Button>
<Button
variant="outline"
onClick={handleBack}
disabled={isSubmitting || allState.edit.loading}
>
Batal
</Button>
</Group>
</Stack>
</Paper>
</Stack>
</Box>
);

View File

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

View File

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

View File

@@ -2,9 +2,9 @@
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
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 { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
@@ -31,91 +31,116 @@ function DetailProgramInovasi() {
if (!stateProgramInovasi.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
<Stack py={12}>
<Skeleton height={520} radius="md" />
</Stack>
)
}
const data = stateProgramInovasi.findUnique.data
return (
<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 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>
<Text fz={"lg"} fw={"bold"}>Nama Program Inovasi</Text>
<Text fz={"lg"}>{stateProgramInovasi.findUnique.data?.name}</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={"lg"}
>{stateProgramInovasi.findUnique.data?.description}</Text>
<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>
<a
href={stateProgramInovasi.findUnique.data?.link || "#"}
target="_blank"
rel="noopener noreferrer"
style={{
wordWrap: 'break-word',
whiteSpace: 'pre-wrap',
overflowWrap: 'break-word',
width: '100%'
}}
>
{stateProgramInovasi.findUnique.data?.link || "Tidak ada link"}
</a>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
<Image src={stateProgramInovasi.findUnique.data?.image?.link} alt="gambar" />
</Box>
<Box>
<Flex gap={"xs"}>
<Button
onClick={() => {
if (stateProgramInovasi.findUnique.data) {
setSelectedId(stateProgramInovasi.findUnique.data.id);
setModalHapus(true);
}
<Text fz="lg" fw="bold">Link</Text>
{data.link ? (
<a
href={data.link}
target="_blank"
rel="noopener noreferrer"
style={{
color: colors['blue-button'],
textDecoration: 'underline',
wordBreak: 'break-word',
}}
disabled={!stateProgramInovasi.findUnique.data}
color="red">
<IconX size={20} />
>
{data.link}
</a>
) : (
<Text fz="md" c="dimmed">-</Text>
)}
</Box>
<Box>
<Text fz="lg" fw="bold">Gambar</Text>
{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>
<Group gap="sm">
<Tooltip label="Hapus Program Inovasi" withArrow position="top">
<Button
color="red"
onClick={() => {
setSelectedId(data.id);
setModalHapus(true);
}}
variant="light"
radius="md"
size="md"
>
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Program Inovasi" withArrow position="top">
<Button
onClick={() => {
if (stateProgramInovasi.findUnique.data) {
router.push(`/admin/landing-page/profile/program-inovasi/${stateProgramInovasi.findUnique.data.id}/edit`);
}
}}
disabled={!stateProgramInovasi.findUnique.data}
color="green">
color="green"
onClick={() => router.push(`/admin/landing-page/profile/program-inovasi/${data.id}/edit`)}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Tooltip>
</Group>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus program inovasi ini?"
text="Apakah Anda yakin ingin menghapus program inovasi ini?"
/>
</Box>
);

View File

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

View File

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

View File

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