Fix UI User Menu Ekonomi & Fix UI Submenu Profile, Desa Anti Korupsi
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user