Fix UI Admin menu desa
This commit is contained in:
@@ -368,11 +368,37 @@ const kategoriBerita = proxy({
|
|||||||
isActive: true;
|
isActive: true;
|
||||||
};
|
};
|
||||||
}>[],
|
}>[],
|
||||||
|
page: 1,
|
||||||
|
totalPages: 1,
|
||||||
loading: false,
|
loading: false,
|
||||||
async load() {
|
search: "",
|
||||||
const res = await ApiFetch.api.desa.kategoriberita["findMany"].get();
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
if (res.status === 200) {
|
kategoriBerita.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
kategoriBerita.findMany.data = res.data?.data ?? [];
|
kategoriBerita.findMany.page = page;
|
||||||
|
kategoriBerita.findMany.search = search;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.desa.kategoriberita[
|
||||||
|
"findMany"
|
||||||
|
].get({ query });
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
kategoriBerita.findMany.data = res.data.data ?? [];
|
||||||
|
kategoriBerita.findMany.totalPages =
|
||||||
|
res.data.totalPages ?? 1;
|
||||||
|
} else {
|
||||||
|
kategoriBerita.findMany.data = [];
|
||||||
|
kategoriBerita.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal fetch kategori berita paginated:", err);
|
||||||
|
kategoriBerita.findMany.data = [];
|
||||||
|
kategoriBerita.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
kategoriBerita.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ const templateTelunjukSaktiDesaForm = z.object({
|
|||||||
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
|
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const templatePelayananPerizinanBerusaha = z.object({
|
const templatePelayananPerizinanBerusaha = z.object({
|
||||||
name: z.string().min(3, "Nama minimal 3 karakter"),
|
name: z.string().min(3, "Nama minimal 3 karakter"),
|
||||||
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
|
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
|
||||||
@@ -72,7 +71,6 @@ const pelayananPendudukNonPermanenForm = {
|
|||||||
deskripsi: "",
|
deskripsi: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const suratKeterangan = proxy({
|
const suratKeterangan = proxy({
|
||||||
create: {
|
create: {
|
||||||
form: { ...suratKeteranganForm },
|
form: { ...suratKeteranganForm },
|
||||||
@@ -113,14 +111,19 @@ const suratKeterangan = proxy({
|
|||||||
totalPages: 1,
|
totalPages: 1,
|
||||||
total: 0,
|
total: 0,
|
||||||
loading: false,
|
loading: false,
|
||||||
load: async (page = 1, limit = 10) => { // Change to arrow function
|
search: "",
|
||||||
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
|
// Change to arrow function
|
||||||
suratKeterangan.findMany.loading = true; // Use the full path to access the property
|
suratKeterangan.findMany.loading = true; // Use the full path to access the property
|
||||||
suratKeterangan.findMany.page = page;
|
suratKeterangan.findMany.page = page;
|
||||||
|
suratKeterangan.findMany.search = search;
|
||||||
try {
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
const res = await ApiFetch.api.desa.layanan.pelayanansuratketerangan[
|
const res = await ApiFetch.api.desa.layanan.pelayanansuratketerangan[
|
||||||
"find-many"
|
"find-many"
|
||||||
].get({
|
].get({
|
||||||
query: { page, limit },
|
query,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status === 200 && res.data?.success) {
|
if (res.status === 200 && res.data?.success) {
|
||||||
@@ -341,28 +344,34 @@ const pelayananTelunjukSaktiDesa = proxy({
|
|||||||
totalPages: 1,
|
totalPages: 1,
|
||||||
total: 0,
|
total: 0,
|
||||||
loading: false,
|
loading: false,
|
||||||
load: async (page = 1, limit = 10) => { // Change to arrow function
|
search: "",
|
||||||
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
|
// Change to arrow function
|
||||||
pelayananTelunjukSaktiDesa.findMany.loading = true; // Use the full path to access the property
|
pelayananTelunjukSaktiDesa.findMany.loading = true; // Use the full path to access the property
|
||||||
pelayananTelunjukSaktiDesa.findMany.page = page;
|
pelayananTelunjukSaktiDesa.findMany.page = page;
|
||||||
|
pelayananTelunjukSaktiDesa.findMany.search = search;
|
||||||
try {
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
const res = await ApiFetch.api.desa.layanan.pelayanantelunjuksaktidesa[
|
const res = await ApiFetch.api.desa.layanan.pelayanantelunjuksaktidesa[
|
||||||
"find-many"
|
"find-many"
|
||||||
].get({
|
].get({
|
||||||
query: { page, limit },
|
query,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status === 200 && res.data?.success) {
|
if (res.status === 200 && res.data?.success) {
|
||||||
pelayananTelunjukSaktiDesa.findMany.data = res.data.data || [];
|
pelayananTelunjukSaktiDesa.findMany.data = res.data.data || [];
|
||||||
pelayananTelunjukSaktiDesa.findMany.total = res.data.total || 0;
|
pelayananTelunjukSaktiDesa.findMany.total = res.data.total || 0;
|
||||||
pelayananTelunjukSaktiDesa.findMany.totalPages = res.data.totalPages || 1;
|
pelayananTelunjukSaktiDesa.findMany.totalPages =
|
||||||
|
res.data.totalPages || 1;
|
||||||
} else {
|
} else {
|
||||||
console.error("Failed to load telunjuk sakti desa:", res.data?.message);
|
console.error("Failed to load surat keterangan:", res.data?.message);
|
||||||
pelayananTelunjukSaktiDesa.findMany.data = [];
|
pelayananTelunjukSaktiDesa.findMany.data = [];
|
||||||
pelayananTelunjukSaktiDesa.findMany.total = 0;
|
suratKeterangan.findMany.total = 0;
|
||||||
pelayananTelunjukSaktiDesa.findMany.totalPages = 1;
|
suratKeterangan.findMany.totalPages = 1;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading telunjuk sakti desa:", error);
|
console.error("Error loading surat keterangan:", error);
|
||||||
pelayananTelunjukSaktiDesa.findMany.data = [];
|
pelayananTelunjukSaktiDesa.findMany.data = [];
|
||||||
pelayananTelunjukSaktiDesa.findMany.total = 0;
|
pelayananTelunjukSaktiDesa.findMany.total = 0;
|
||||||
pelayananTelunjukSaktiDesa.findMany.totalPages = 1;
|
pelayananTelunjukSaktiDesa.findMany.totalPages = 1;
|
||||||
@@ -410,7 +419,9 @@ const pelayananTelunjukSaktiDesa = proxy({
|
|||||||
);
|
);
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
toast.success(result.message || "Telunjuk Sakti Desa berhasil dihapus");
|
toast.success(
|
||||||
|
result.message || "Telunjuk Sakti Desa berhasil dihapus"
|
||||||
|
);
|
||||||
await pelayananTelunjukSaktiDesa.findMany.load(); // refresh list
|
await pelayananTelunjukSaktiDesa.findMany.load(); // refresh list
|
||||||
} else {
|
} else {
|
||||||
toast.error(result.message || "Gagal menghapus telunjuk sakti desa");
|
toast.error(result.message || "Gagal menghapus telunjuk sakti desa");
|
||||||
@@ -501,7 +512,9 @@ const pelayananTelunjukSaktiDesa = proxy({
|
|||||||
}
|
}
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
toast.success(result.message || "Telunjuk Sakti Desa berhasil diupdate");
|
toast.success(
|
||||||
|
result.message || "Telunjuk Sakti Desa berhasil diupdate"
|
||||||
|
);
|
||||||
await pelayananTelunjukSaktiDesa.findMany.load(); // refresh list
|
await pelayananTelunjukSaktiDesa.findMany.load(); // refresh list
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
@@ -522,7 +535,7 @@ const pelayananTelunjukSaktiDesa = proxy({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
const pelayananPerizinanBerusaha = proxy({
|
const pelayananPerizinanBerusaha = proxy({
|
||||||
findById: {
|
findById: {
|
||||||
@@ -596,9 +609,7 @@ const pelayananPerizinanBerusaha = proxy({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching pelayanan perizinan berusaha:", error);
|
console.error("Error fetching pelayanan perizinan berusaha:", error);
|
||||||
toast.error(
|
toast.error(
|
||||||
error instanceof Error
|
error instanceof Error ? error.message : "Gagal memuat data"
|
||||||
? error.message
|
|
||||||
: "Gagal memuat data"
|
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -713,9 +724,7 @@ const pelayananPendudukNonPermanen = proxy({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching pelayanan penduduk non permanen:", error);
|
console.error("Error fetching pelayanan penduduk non permanen:", error);
|
||||||
toast.error(
|
toast.error(
|
||||||
error instanceof Error
|
error instanceof Error ? error.message : "Gagal memuat data"
|
||||||
? error.message
|
|
||||||
: "Gagal memuat data"
|
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,14 +56,19 @@ const penghargaanState = proxy({
|
|||||||
totalPages: 1,
|
totalPages: 1,
|
||||||
total: 0,
|
total: 0,
|
||||||
loading: false,
|
loading: false,
|
||||||
load: async (page = 1, limit = 10) => { // Change to arrow function
|
search: "",
|
||||||
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
|
// Change to arrow function
|
||||||
penghargaanState.findMany.loading = true; // Use the full path to access the property
|
penghargaanState.findMany.loading = true; // Use the full path to access the property
|
||||||
penghargaanState.findMany.page = page;
|
penghargaanState.findMany.page = page;
|
||||||
|
penghargaanState.findMany.search = search;
|
||||||
try {
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
const res = await ApiFetch.api.desa.penghargaan[
|
const res = await ApiFetch.api.desa.penghargaan[
|
||||||
"find-many"
|
"find-many"
|
||||||
].get({
|
].get({
|
||||||
query: { page, limit },
|
query,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status === 200 && res.data?.success) {
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
|||||||
@@ -55,11 +55,39 @@ const category = proxy({
|
|||||||
pengumumans: number;
|
pengumumans: number;
|
||||||
};
|
};
|
||||||
})[],
|
})[],
|
||||||
|
page: 1,
|
||||||
|
totalPages: 1,
|
||||||
|
total: 0,
|
||||||
loading: false,
|
loading: false,
|
||||||
async load() {
|
search: "",
|
||||||
const res = await ApiFetch.api.desa.kategoripengumuman["findMany"].get();
|
load: async (page = 1, limit = 10, search = "") => { // Change to arrow function
|
||||||
if (res.status === 200) {
|
category.findMany.loading = true; // Use the full path to access the property
|
||||||
category.findMany.data = res.data?.data ?? [];
|
category.findMany.page = page;
|
||||||
|
category.findMany.search = search;
|
||||||
|
try {
|
||||||
|
const res = await ApiFetch.api.desa.kategoripengumuman[
|
||||||
|
"findMany"
|
||||||
|
].get({
|
||||||
|
query: { page, limit },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
category.findMany.data = res.data.data || [];
|
||||||
|
category.findMany.total = res.data.total || 0;
|
||||||
|
category.findMany.totalPages = res.data.totalPages || 1;
|
||||||
|
} else {
|
||||||
|
console.error("Failed to load potensi desa:", res.data?.message);
|
||||||
|
category.findMany.data = [];
|
||||||
|
category.findMany.total = 0;
|
||||||
|
category.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading potensi desa:", error);
|
||||||
|
category.findMany.data = [];
|
||||||
|
category.findMany.total = 0;
|
||||||
|
category.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
category.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -56,9 +56,11 @@ const potensiDesa = proxy({
|
|||||||
totalPages: 1,
|
totalPages: 1,
|
||||||
total: 0,
|
total: 0,
|
||||||
loading: false,
|
loading: false,
|
||||||
load: async (page = 1, limit = 10) => { // Change to arrow function
|
search: "",
|
||||||
|
load: async (page = 1, limit = 10, search = "") => { // Change to arrow function
|
||||||
potensiDesa.findMany.loading = true; // Use the full path to access the property
|
potensiDesa.findMany.loading = true; // Use the full path to access the property
|
||||||
potensiDesa.findMany.page = page;
|
potensiDesa.findMany.page = page;
|
||||||
|
potensiDesa.findMany.search = search;
|
||||||
try {
|
try {
|
||||||
const res = await ApiFetch.api.desa.potensi[
|
const res = await ApiFetch.api.desa.potensi[
|
||||||
"find-many"
|
"find-many"
|
||||||
@@ -298,11 +300,34 @@ const kategoriPotensi = proxy({
|
|||||||
isActive: true;
|
isActive: true;
|
||||||
};
|
};
|
||||||
}>[],
|
}>[],
|
||||||
|
page: 1,
|
||||||
|
totalPages: 1,
|
||||||
loading: false,
|
loading: false,
|
||||||
async load() {
|
search: "",
|
||||||
const res = await ApiFetch.api.desa.kategoripotensi["findMany"].get();
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
if (res.status === 200) {
|
kategoriPotensi.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
kategoriPotensi.findMany.data = res.data?.data ?? [];
|
kategoriPotensi.findMany.page = page;
|
||||||
|
kategoriPotensi.findMany.search = search;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.desa.kategoripotensi["findMany"].get({ query });
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
kategoriPotensi.findMany.data = res.data.data ?? [];
|
||||||
|
kategoriPotensi.findMany.totalPages = res.data.totalPages ?? 1;
|
||||||
|
} else {
|
||||||
|
kategoriPotensi.findMany.data = [];
|
||||||
|
kategoriPotensi.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal fetch kategori potensi paginated:", err);
|
||||||
|
kategoriPotensi.findMany.data = [];
|
||||||
|
kategoriPotensi.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
kategoriPotensi.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
|
||||||
import { usePathname, useRouter } from 'next/navigation';
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { IconFileText, IconBuildingStore, IconSparkles, IconUsers } from '@tabler/icons-react';
|
||||||
|
|
||||||
function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
|
function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -12,26 +13,35 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
|
|||||||
{
|
{
|
||||||
label: "Pelayanan Surat Keterangan",
|
label: "Pelayanan Surat Keterangan",
|
||||||
value: "pelayanansuratketerangan",
|
value: "pelayanansuratketerangan",
|
||||||
href: "/admin/desa/layanan/pelayanan_surat_keterangan"
|
href: "/admin/desa/layanan/pelayanan_surat_keterangan",
|
||||||
|
icon: <IconFileText size={18} stroke={1.8} />,
|
||||||
|
tooltip: "Layanan terkait surat keterangan resmi desa"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Pelayanan Perizinan Berusaha",
|
label: "Pelayanan Perizinan Berusaha",
|
||||||
value: "pelayananperizinanusaha",
|
value: "pelayananperizinanusaha",
|
||||||
href: "/admin/desa/layanan/pelayanan_perizinan_berusaha"
|
href: "/admin/desa/layanan/pelayanan_perizinan_berusaha",
|
||||||
|
icon: <IconBuildingStore size={18} stroke={1.8} />,
|
||||||
|
tooltip: "Layanan untuk izin usaha masyarakat"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Pelayanan Telunjuk Sakti Desa",
|
label: "Pelayanan Telunjuk Sakti Desa",
|
||||||
value: "pelayanantelunjuksaktidesa",
|
value: "pelayanantelunjuksaktidesa",
|
||||||
href: "/admin/desa/layanan/pelayanan_telunjuk_sakti_desa"
|
href: "/admin/desa/layanan/pelayanan_telunjuk_sakti_desa",
|
||||||
|
icon: <IconSparkles size={18} stroke={1.8} />,
|
||||||
|
tooltip: "Layanan inovasi khusus desa"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Pelayanan Penduduk Non-Permanent",
|
label: "Pelayanan Penduduk Non-Permanent",
|
||||||
value: "pelayanantelunjuknonpermanent",
|
value: "pelayanannonpermanent",
|
||||||
href: "/admin/desa/layanan/pelayanan_penduduk_non_permanent"
|
href: "/admin/desa/layanan/pelayanan_penduduk_non_permanent",
|
||||||
|
icon: <IconUsers size={18} stroke={1.8} />,
|
||||||
|
tooltip: "Pendataan penduduk non-permanent"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
const curentTab = tabs.find(tab => tab.href === pathname)
|
|
||||||
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
|
const currentTab = tabs.find(tab => tab.href === pathname)
|
||||||
|
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
|
||||||
|
|
||||||
const handleTabChange = (value: string | null) => {
|
const handleTabChange = (value: string | null) => {
|
||||||
const tab = tabs.find(t => t.value === value)
|
const tab = tabs.find(t => t.value === value)
|
||||||
@@ -49,22 +59,63 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
|
|||||||
}, [pathname])
|
}, [pathname])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack gap="lg">
|
||||||
<Title order={3}>Layanan</Title>
|
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Layanan</Title>
|
||||||
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
|
<Tabs
|
||||||
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
|
color={colors['blue-button']}
|
||||||
{tabs.map((e, i) => (
|
variant='pills'
|
||||||
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
|
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
|
||||||
|
transitionProps={{ transition: 'pop', duration: 200 }}
|
||||||
|
>
|
||||||
|
<TabsTab
|
||||||
|
value={tab.value}
|
||||||
|
leftSection={tab.icon}
|
||||||
|
style={{
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
transition: "all 0.2s ease",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</TabsTab>
|
||||||
|
</Tooltip>
|
||||||
))}
|
))}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
{tabs.map((e, i) => (
|
|
||||||
<TabsPanel key={i} value={e.value}>
|
{tabs.map((tab, i) => (
|
||||||
{/* Konten dummy, bisa diganti tergantung routing */}
|
<TabsPanel
|
||||||
<></>
|
key={i}
|
||||||
|
value={tab.value}
|
||||||
|
style={{
|
||||||
|
padding: "1.5rem",
|
||||||
|
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
|
||||||
|
borderRadius: "1rem",
|
||||||
|
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Konten dummy, bisa diganti sesuai routing */}
|
||||||
|
<>{children}</>
|
||||||
</TabsPanel>
|
</TabsPanel>
|
||||||
))}
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
{children}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,61 +1,108 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
|
||||||
import { usePathname, useRouter } from 'next/navigation';
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { IconNews, IconCategory } from '@tabler/icons-react';
|
||||||
|
|
||||||
function LayoutTabsBerita({ children }: { children: React.ReactNode }) {
|
function LayoutTabsBerita({ children }: { children: React.ReactNode }) {
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
const pathname = usePathname()
|
const pathname = usePathname();
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
label: "List Berita",
|
label: "List Berita",
|
||||||
value: "list_berita",
|
value: "list_berita",
|
||||||
href: "/admin/desa/berita/list-berita"
|
href: "/admin/desa/berita/list-berita",
|
||||||
|
icon: <IconNews size={18} stroke={1.8} />,
|
||||||
|
tooltip: "Lihat dan kelola semua berita desa"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Kategori Berita",
|
label: "Kategori Berita",
|
||||||
value: "kategori_berita",
|
value: "kategori_berita",
|
||||||
href: "/admin/desa/berita/kategori-berita"
|
href: "/admin/desa/berita/kategori-berita",
|
||||||
|
icon: <IconCategory size={18} stroke={1.8} />,
|
||||||
|
tooltip: "Kelola kategori berita desa"
|
||||||
},
|
},
|
||||||
|
|
||||||
];
|
];
|
||||||
const curentTab = tabs.find(tab => tab.href === pathname)
|
|
||||||
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
|
const currentTab = tabs.find(tab => tab.href === pathname);
|
||||||
|
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
|
||||||
|
|
||||||
const handleTabChange = (value: string | null) => {
|
const handleTabChange = (value: string | null) => {
|
||||||
const tab = tabs.find(t => t.value === value)
|
const tab = tabs.find(t => t.value === value);
|
||||||
if (tab) {
|
if (tab) {
|
||||||
router.push(tab.href)
|
router.push(tab.href);
|
||||||
}
|
|
||||||
setActiveTab(value)
|
|
||||||
}
|
}
|
||||||
|
setActiveTab(value);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const match = tabs.find(tab => tab.href === pathname)
|
const match = tabs.find(tab => tab.href === pathname);
|
||||||
if (match) {
|
if (match) {
|
||||||
setActiveTab(match.value)
|
setActiveTab(match.value);
|
||||||
}
|
}
|
||||||
}, [pathname])
|
}, [pathname]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack gap="lg">
|
||||||
<Title order={3}>Gallery</Title>
|
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Berita Desa</Title>
|
||||||
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
|
<Tabs
|
||||||
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
|
color={colors['blue-button']}
|
||||||
{tabs.map((e, i) => (
|
variant="pills"
|
||||||
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
|
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
|
||||||
|
transitionProps={{ transition: 'pop', duration: 200 }}
|
||||||
|
>
|
||||||
|
<TabsTab
|
||||||
|
value={tab.value}
|
||||||
|
leftSection={tab.icon}
|
||||||
|
style={{
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
transition: "all 0.2s ease",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</TabsTab>
|
||||||
|
</Tooltip>
|
||||||
))}
|
))}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
{tabs.map((e, i) => (
|
|
||||||
<TabsPanel key={i} value={e.value}>
|
{tabs.map((tab, i) => (
|
||||||
{/* Konten dummy, bisa diganti tergantung routing */}
|
<TabsPanel
|
||||||
<></>
|
key={i}
|
||||||
|
value={tab.value}
|
||||||
|
style={{
|
||||||
|
padding: "1.5rem",
|
||||||
|
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
|
||||||
|
borderRadius: "1rem",
|
||||||
|
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Konten dummy, bisa diganti sesuai routing */}
|
||||||
|
<>{children}</>
|
||||||
</TabsPanel>
|
</TabsPanel>
|
||||||
))}
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
{children}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,16 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
|
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Tooltip
|
||||||
|
} from '@mantine/core';
|
||||||
import { IconArrowBack } from '@tabler/icons-react';
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
@@ -10,9 +19,10 @@ import { toast } from 'react-toastify';
|
|||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
function EditKategoriBerita() {
|
function EditKategoriBerita() {
|
||||||
const editState = useProxy(stateDashboardBerita.kategoriBerita)
|
const editState = useProxy(stateDashboardBerita.kategoriBerita);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: editState.update.form.name || '',
|
name: editState.update.form.name || '',
|
||||||
});
|
});
|
||||||
@@ -23,15 +33,15 @@ function EditKategoriBerita() {
|
|||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await editState.update.load(id); // akses langsung, bukan dari proxy
|
const data = await editState.update.load(id);
|
||||||
if (data) {
|
if (data) {
|
||||||
setFormData({
|
setFormData({
|
||||||
name: data.name || '',
|
name: data.name || '',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading kategori Berita:", error);
|
console.error('Error loading kategori Berita:', error);
|
||||||
toast.error("Gagal memuat data kategori Berita");
|
toast.error('Gagal memuat data kategori Berita');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -44,6 +54,7 @@ function EditKategoriBerita() {
|
|||||||
...editState.update.form,
|
...editState.update.form,
|
||||||
name: formData.name,
|
name: formData.name,
|
||||||
};
|
};
|
||||||
|
|
||||||
await editState.update.update();
|
await editState.update.update();
|
||||||
toast.success('Kategori Berita berhasil diperbarui!');
|
toast.success('Kategori Berita berhasil diperbarui!');
|
||||||
router.push('/admin/desa/berita/kategori-berita');
|
router.push('/admin/desa/berita/kategori-berita');
|
||||||
@@ -54,23 +65,56 @@ function EditKategoriBerita() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Back Button + Title */}
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors["blue-button"]} size={30} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
p="xs"
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Tooltip>
|
||||||
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
|
<Title order={4} ml="sm" c="dark">
|
||||||
<Stack gap={"xs"}>
|
Edit Kategori Berita
|
||||||
<Title order={3}>Edit Kategori Berita</Title>
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{/* Form Wrapper */}
|
||||||
|
<Paper
|
||||||
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label="Nama Kategori Berita"
|
||||||
|
placeholder="Masukkan nama kategori berita"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Nama Kategori Berita</Text>}
|
required
|
||||||
placeholder="masukkan nama kategori Berita"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button onClick={handleSubmit}>Simpan</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>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,50 +1,87 @@
|
|||||||
'use client'
|
'use client';
|
||||||
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
|
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
import { IconArrowBack } from '@tabler/icons-react';
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function CreateKategoriBerita() {
|
function CreateKategoriBerita() {
|
||||||
const createState = useProxy(stateDashboardBerita.kategoriBerita)
|
const createState = useProxy(stateDashboardBerita.kategoriBerita);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
createState.create.form = {
|
createState.create.form = {
|
||||||
name: "",
|
name: '',
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
await createState.create.create();
|
await createState.create.create();
|
||||||
resetForm();
|
resetForm();
|
||||||
router.push("/admin/desa/berita/kategori-berita")
|
router.push('/admin/desa/berita/kategori-berita');
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Header dengan back button */}
|
||||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
p="xs"
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Tooltip>
|
||||||
|
<Title order={4} ml="sm" c="dark">
|
||||||
|
Tambah Kategori Berita
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
{/* Form utama */}
|
||||||
<Stack gap={"xs"}>
|
<Paper
|
||||||
<Title order={4}>Create Kategori Berita</Title>
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Berita</Text>}
|
label={<Text fw="bold" fz="sm">Nama Kategori Berita</Text>}
|
||||||
placeholder='Masukkan nama kategori Berita'
|
placeholder="Masukkan nama kategori berita"
|
||||||
value={createState.create.form.name}
|
value={createState.create.form.name || ''}
|
||||||
onChange={(val) => {
|
onChange={(e) => (createState.create.form.name = e.target.value)}
|
||||||
createState.create.form.name = val.target.value;
|
required
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<Group>
|
|
||||||
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
|
color: '#fff',
|
||||||
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -1,25 +1,40 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
import {
|
||||||
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
|
Box,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Group,
|
||||||
|
Pagination,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
TableTbody,
|
||||||
|
TableTd,
|
||||||
|
TableTh,
|
||||||
|
TableThead,
|
||||||
|
TableTr,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
Tooltip
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import HeaderSearch from '../../../_com/header';
|
import HeaderSearch from '../../../_com/header';
|
||||||
import JudulList from '../../../_com/judulList';
|
|
||||||
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||||
import stateDashboardBerita from '../../../_state/desa/berita';
|
import stateDashboardBerita from '../../../_state/desa/berita';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function KategoriBerita() {
|
function KategoriBerita() {
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
title='Kategori Berita'
|
title="Kategori Berita"
|
||||||
placeholder='pencarian'
|
placeholder="Cari nama kategori berita..."
|
||||||
searchIcon={<IconSearch size={20} />}
|
searchIcon={<IconSearch size={20} />}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
@@ -30,99 +45,155 @@ function KategoriBerita() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ListKategoriBerita({ search }: { search: string }) {
|
function ListKategoriBerita({ search }: { search: string }) {
|
||||||
const listDataState = useProxy(stateDashboardBerita.kategoriBerita)
|
const listDataState = useProxy(stateDashboardBerita.kategoriBerita);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [modalHapus, setModalHapus] = useState(false)
|
const [modalHapus, setModalHapus] = useState(false);
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
loading,
|
||||||
|
load,
|
||||||
|
page,
|
||||||
|
totalPages,
|
||||||
|
} = listDataState.findMany;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
listDataState.findMany.load()
|
load(page, 10, search);
|
||||||
}, [])
|
}, [page, search]);
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
if (selectedId) {
|
if (selectedId) {
|
||||||
listDataState.delete.delete(selectedId)
|
listDataState.delete.delete(selectedId);
|
||||||
setModalHapus(false)
|
setModalHapus(false);
|
||||||
setSelectedId(null)
|
setSelectedId(null);
|
||||||
|
load(page, 10, search);
|
||||||
listDataState.findMany.load()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const filteredData = (listDataState.findMany.data || []).filter(item => {
|
const filteredData = data || [];
|
||||||
const keyword = search.toLowerCase();
|
|
||||||
return (
|
|
||||||
item.name.toLowerCase().includes(keyword)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!listDataState.findMany.data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton h={500} />
|
<Skeleton height={600} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={10}>
|
||||||
<Paper bg={colors['white-1']} p="md">
|
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||||
<Stack>
|
<Group justify="space-between" mb="md">
|
||||||
<JudulList
|
<Title order={4}>Daftar Kategori Berita</Title>
|
||||||
title='List Kategori Berita'
|
<Tooltip label="Tambah Kategori Berita" withArrow>
|
||||||
href='/admin/desa/berita/kategori-berita/create'
|
<Button
|
||||||
/>
|
leftSection={<IconPlus size={18} />}
|
||||||
|
color="blue"
|
||||||
|
variant="light"
|
||||||
|
onClick={() =>
|
||||||
|
router.push('/admin/desa/berita/kategori-berita/create')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Tambah Baru
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
|
|
||||||
<Box style={{ overflowX: 'auto' }}>
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
|
<Table highlightOnHover>
|
||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh>No</TableTh>
|
<TableTh style={{ width: '10%' }}>No</TableTh>
|
||||||
<TableTh>Nama</TableTh>
|
<TableTh style={{ width: '50%' }}>Nama</TableTh>
|
||||||
<TableTh>Edit</TableTh>
|
<TableTh style={{ width: '20%' }}>Edit</TableTh>
|
||||||
<TableTh>Hapus</TableTh>
|
<TableTh style={{ width: '20%' }}>Hapus</TableTh>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
</TableThead>
|
</TableThead>
|
||||||
<TableTbody>
|
<TableTbody>
|
||||||
{filteredData.map((item, index) => (
|
{filteredData.length > 0 ? (
|
||||||
|
filteredData.map((item, index) => (
|
||||||
<TableTr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Box w={100}>
|
<Text fz="sm">{index + 1}</Text>
|
||||||
<Text truncate="end" fz={"sm"}>{index + 1}</Text>
|
|
||||||
</Box>
|
|
||||||
</TableTd>
|
|
||||||
<TableTd>{item.name}</TableTd>
|
|
||||||
<TableTd>
|
|
||||||
<Button color='green' onClick={() => router.push(`/admin/pendidikan/perpustakaan-digital/kategori-Berita/${item.id}`)}>
|
|
||||||
<IconEdit size={20} />
|
|
||||||
</Button>
|
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
|
<Text fw={500} truncate="end" lineClamp={1}>
|
||||||
|
{item.name}
|
||||||
|
</Text>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Tooltip label="Edit Kategori Berita" withArrow>
|
||||||
<Button
|
<Button
|
||||||
color='red'
|
variant="light"
|
||||||
|
color="green"
|
||||||
|
onClick={() =>
|
||||||
|
router.push(
|
||||||
|
`/admin/desa/berita/kategori-berita/${item.id}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<IconEdit size={18} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Tooltip label="Hapus Kategori Berita" withArrow>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
color="red"
|
||||||
disabled={listDataState.delete.loading}
|
disabled={listDataState.delete.loading}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedId(item.id)
|
setSelectedId(item.id);
|
||||||
setModalHapus(true)
|
setModalHapus(true);
|
||||||
}}>
|
}}
|
||||||
<IconTrash size={20} />
|
>
|
||||||
|
<IconTrash size={18} />
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))}
|
))
|
||||||
|
) : (
|
||||||
|
<TableTr>
|
||||||
|
<TableTd colSpan={4}>
|
||||||
|
<Center py={20}>
|
||||||
|
<Text color="dimmed">
|
||||||
|
Tidak ada data kategori berita yang cocok
|
||||||
|
</Text>
|
||||||
|
</Center>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
)}
|
||||||
</TableTbody>
|
</TableTbody>
|
||||||
</Table>
|
</Table>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
|
<Center>
|
||||||
|
<Pagination
|
||||||
|
value={page}
|
||||||
|
onChange={(newPage) => {
|
||||||
|
load(newPage, 10, search);
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
}}
|
||||||
|
total={totalPages}
|
||||||
|
mt="md"
|
||||||
|
mb="md"
|
||||||
|
color="blue"
|
||||||
|
radius="md"
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
|
||||||
{/* Modal Konfirmasi Hapus */}
|
{/* Modal Konfirmasi Hapus */}
|
||||||
<ModalKonfirmasiHapus
|
<ModalKonfirmasiHapus
|
||||||
opened={modalHapus}
|
opened={modalHapus}
|
||||||
onClose={() => setModalHapus(false)}
|
onClose={() => setModalHapus(false)}
|
||||||
onConfirm={handleDelete}
|
onConfirm={handleDelete}
|
||||||
text='Apakah anda yakin ingin menghapus kategori Berita ini?'
|
text="Apakah anda yakin ingin menghapus kategori berita ini?"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default KategoriBerita;
|
export default KategoriBerita;
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title
|
Title,
|
||||||
|
Tooltip,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { Dropzone } from "@mantine/dropzone";
|
import { Dropzone } from "@mantine/dropzone";
|
||||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from "@tabler/icons-react";
|
import { IconArrowBack, IconPhoto, IconUpload, IconX } from "@tabler/icons-react";
|
||||||
@@ -24,7 +25,6 @@ import { useEffect, useState } from "react";
|
|||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { useProxy } from "valtio/utils";
|
import { useProxy } from "valtio/utils";
|
||||||
|
|
||||||
|
|
||||||
function EditBerita() {
|
function EditBerita() {
|
||||||
const beritaState = useProxy(stateDashboardBerita);
|
const beritaState = useProxy(stateDashboardBerita);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -33,29 +33,29 @@ function EditBerita() {
|
|||||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
judul: beritaState.berita.edit.form.judul || '',
|
judul: beritaState.berita.edit.form.judul || "",
|
||||||
deskripsi: beritaState.berita.edit.form.deskripsi || '',
|
deskripsi: beritaState.berita.edit.form.deskripsi || "",
|
||||||
kategoriBeritaId: beritaState.berita.edit.form.kategoriBeritaId || '',
|
kategoriBeritaId: beritaState.berita.edit.form.kategoriBeritaId || "",
|
||||||
content: beritaState.berita.edit.form.content || '',
|
content: beritaState.berita.edit.form.content || "",
|
||||||
imageId: beritaState.berita.edit.form.imageId || ''
|
imageId: beritaState.berita.edit.form.imageId || "",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load berita by id saat pertama kali
|
// Load berita by id saat pertama kali
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
beritaState.kategoriBerita.findMany.load()
|
beritaState.kategoriBerita.findMany.load();
|
||||||
const loadBerita = async () => {
|
const loadBerita = async () => {
|
||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await stateDashboardBerita.berita.edit.load(id); // akses langsung, bukan dari proxy
|
const data = await stateDashboardBerita.berita.edit.load(id);
|
||||||
if (data) {
|
if (data) {
|
||||||
setFormData({
|
setFormData({
|
||||||
judul: data.judul || '',
|
judul: data.judul || "",
|
||||||
deskripsi: data.deskripsi || '',
|
deskripsi: data.deskripsi || "",
|
||||||
kategoriBeritaId: data.kategoriBeritaId || '',
|
kategoriBeritaId: data.kategoriBeritaId || "",
|
||||||
content: data.content || '',
|
content: data.content || "",
|
||||||
imageId: data.imageId || '',
|
imageId: data.imageId || "",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data?.image?.link) {
|
if (data?.image?.link) {
|
||||||
@@ -69,31 +69,26 @@ function EditBerita() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
loadBerita();
|
loadBerita();
|
||||||
}, [params?.id]); // ✅ hapus beritaState dari dependency
|
}, [params?.id]);
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Update global state with form data
|
|
||||||
beritaState.berita.edit.form = {
|
beritaState.berita.edit.form = {
|
||||||
...beritaState.berita.edit.form,
|
...beritaState.berita.edit.form,
|
||||||
judul: formData.judul,
|
...formData,
|
||||||
deskripsi: formData.deskripsi,
|
|
||||||
content: formData.content,
|
|
||||||
kategoriBeritaId: formData.kategoriBeritaId || '',
|
|
||||||
imageId: formData.imageId // Keep existing imageId if not changed
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Jika ada file baru, upload
|
|
||||||
if (file) {
|
if (file) {
|
||||||
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
|
const res = await ApiFetch.api.fileStorage.create.post({
|
||||||
|
file,
|
||||||
|
name: file.name,
|
||||||
|
});
|
||||||
const uploaded = res.data?.data;
|
const uploaded = res.data?.data;
|
||||||
|
|
||||||
if (!uploaded?.id) {
|
if (!uploaded?.id) {
|
||||||
return toast.error("Gagal upload gambar");
|
return toast.error("Gagal upload gambar");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update imageId in global state
|
|
||||||
beritaState.berita.edit.form.imageId = uploaded.id;
|
beritaState.berita.edit.form.imageId = uploaded.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,87 +102,111 @@ function EditBerita() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: "sm", md: "lg" }} py="md">
|
||||||
<Box mb={10}>
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
<IconArrowBack color={colors["blue-button"]} size={30} />
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
p="xs"
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
|
<IconArrowBack color={colors["blue-button"]} size={24} />
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Tooltip>
|
||||||
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
|
<Title order={4} ml="sm" c="dark">
|
||||||
<Stack gap={"xs"}>
|
Edit Berita
|
||||||
<Title order={3}>Edit Berita</Title>
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Paper
|
||||||
|
w={{ base: "100%", md: "50%" }}
|
||||||
|
bg={colors["white-1"]}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: "1px solid #e0e0e0" }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label="Judul"
|
||||||
|
placeholder="Masukkan judul"
|
||||||
value={formData.judul}
|
value={formData.judul}
|
||||||
onChange={(e) => setFormData({ ...formData, judul: e.target.value })}
|
onChange={(e) =>
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
|
setFormData({ ...formData, judul: e.target.value })
|
||||||
placeholder="masukkan judul"
|
}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label="Deskripsi"
|
||||||
|
placeholder="Masukkan deskripsi"
|
||||||
value={formData.deskripsi}
|
value={formData.deskripsi}
|
||||||
onChange={(e) => setFormData({ ...formData, deskripsi: e.target.value })}
|
onChange={(e) =>
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>}
|
setFormData({ ...formData, deskripsi: e.target.value })
|
||||||
placeholder="masukkan deskripsi"
|
}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
<Box>
|
Gambar Berita
|
||||||
|
</Text>
|
||||||
<Dropzone
|
<Dropzone
|
||||||
onDrop={(files) => {
|
onDrop={(files) => {
|
||||||
const selectedFile = files[0]; // Ambil file pertama
|
const selectedFile = files[0];
|
||||||
if (selectedFile) {
|
if (selectedFile) {
|
||||||
setFile(selectedFile);
|
setFile(selectedFile);
|
||||||
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
|
setPreviewImage(URL.createObjectURL(selectedFile));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onReject={() => toast.error('File tidak valid.')}
|
onReject={() => toast.error("File tidak valid, gunakan format gambar")}
|
||||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
maxSize={5 * 1024 ** 2}
|
||||||
accept={{ 'image/*': [] }}
|
accept={{ "image/*": [] }}
|
||||||
|
radius="md"
|
||||||
|
p="xl"
|
||||||
>
|
>
|
||||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
<Group justify="center" gap="xl" mih={180}>
|
||||||
<Dropzone.Accept>
|
<Dropzone.Accept>
|
||||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
<IconUpload size={48} color={colors["blue-button"]} stroke={1.5} />
|
||||||
</Dropzone.Accept>
|
</Dropzone.Accept>
|
||||||
<Dropzone.Reject>
|
<Dropzone.Reject>
|
||||||
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
<IconX size={48} color="red" stroke={1.5} />
|
||||||
</Dropzone.Reject>
|
</Dropzone.Reject>
|
||||||
<Dropzone.Idle>
|
<Dropzone.Idle>
|
||||||
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
<IconPhoto size={48} color="#868e96" stroke={1.5} />
|
||||||
</Dropzone.Idle>
|
</Dropzone.Idle>
|
||||||
|
<Stack gap="xs" align="center">
|
||||||
<div>
|
<Text size="md" fw={500}>
|
||||||
<Text size="xl" inline>
|
Seret gambar atau klik untuk memilih file
|
||||||
Drag gambar ke sini atau klik untuk pilih file
|
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="sm" c="dimmed" inline mt={7}>
|
<Text size="sm" c="dimmed">
|
||||||
Maksimal 5MB dan harus format gambar
|
Maksimal 5MB, format gambar wajib
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</Stack>
|
||||||
</Group>
|
</Group>
|
||||||
</Dropzone>
|
</Dropzone>
|
||||||
|
|
||||||
{/* Tampilkan preview kalau ada */}
|
|
||||||
{previewImage && (
|
{previewImage && (
|
||||||
<Box mt="sm">
|
<Box mt="sm" style={{ display: "flex", justifyContent: "center" }}>
|
||||||
<Image
|
<Image
|
||||||
src={previewImage}
|
src={previewImage}
|
||||||
alt="Preview"
|
alt="Preview Gambar"
|
||||||
|
radius="md"
|
||||||
style={{
|
style={{
|
||||||
maxWidth: '100%',
|
maxHeight: 220,
|
||||||
maxHeight: '200px',
|
objectFit: "contain",
|
||||||
objectFit: 'contain',
|
border: `1px solid ${colors["blue-button"]}`,
|
||||||
borderRadius: '8px',
|
|
||||||
border: '1px solid #ddd',
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"sm"} fw={"bold"}>Konten</Text>
|
<Text fz="sm" fw="bold">
|
||||||
|
Konten
|
||||||
|
</Text>
|
||||||
<EditEditor
|
<EditEditor
|
||||||
value={formData.content}
|
value={formData.content}
|
||||||
onChange={(htmlContent) => {
|
onChange={(htmlContent) => {
|
||||||
@@ -199,13 +218,15 @@ function EditBerita() {
|
|||||||
|
|
||||||
<Select
|
<Select
|
||||||
value={formData.kategoriBeritaId}
|
value={formData.kategoriBeritaId}
|
||||||
onChange={(val) => setFormData({ ...formData, kategoriBeritaId: val || "" })}
|
onChange={(val) =>
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>}
|
setFormData({ ...formData, kategoriBeritaId: val || "" })
|
||||||
placeholder='Pilih kategori'
|
}
|
||||||
|
label="Kategori"
|
||||||
|
placeholder="Pilih kategori"
|
||||||
data={
|
data={
|
||||||
beritaState.kategoriBerita.findMany.data?.map((v) => ({
|
beritaState.kategoriBerita.findMany.data?.map((v) => ({
|
||||||
value: v.id,
|
value: v.id,
|
||||||
label: v.name
|
label: v.name,
|
||||||
})) || []
|
})) || []
|
||||||
}
|
}
|
||||||
clearable
|
clearable
|
||||||
@@ -214,7 +235,20 @@ function EditBerita() {
|
|||||||
error={!formData.kategoriBeritaId ? "Pilih kategori" : undefined}
|
error={!formData.kategoriBeritaId ? "Pilih kategori" : undefined}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button onClick={handleSubmit}>Edit Berita</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>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
|
||||||
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
|
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
@@ -12,104 +11,143 @@ import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirma
|
|||||||
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
|
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
|
||||||
|
|
||||||
function DetailBerita() {
|
function DetailBerita() {
|
||||||
const beritaState = useProxy(stateDashboardBerita)
|
const beritaState = useProxy(stateDashboardBerita);
|
||||||
const [modalHapus, setModalHapus] = useState(false)
|
const [modalHapus, setModalHapus] = useState(false);
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
const params = useParams()
|
const params = useParams();
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
beritaState.berita.findUnique.load(params?.id as string)
|
beritaState.berita.findUnique.load(params?.id as string);
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
const handleHapus = () => {
|
const handleHapus = () => {
|
||||||
if (selectedId) {
|
if (selectedId) {
|
||||||
beritaState.berita.delete.byId(selectedId)
|
beritaState.berita.delete.byId(selectedId);
|
||||||
setModalHapus(false)
|
setModalHapus(false);
|
||||||
setSelectedId(null)
|
setSelectedId(null);
|
||||||
router.push("/admin/desa/berita/list-berita")
|
router.push("/admin/desa/berita/list-berita");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (!beritaState.berita.findUnique.data) {
|
if (!beritaState.berita.findUnique.data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton h={40} />
|
<Skeleton height={500} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = beritaState.berita.findUnique.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box py={10}>
|
||||||
<Box mb={10}>
|
{/* Tombol Back */}
|
||||||
<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 Berita</Text>
|
|
||||||
{beritaState.berita.findUnique.data ? (
|
|
||||||
<Paper key={beritaState.berita.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
|
||||||
<Stack gap={"xs"}>
|
|
||||||
<Box>
|
|
||||||
<Text fw={"bold"} fz={"lg"}>Kategori</Text>
|
|
||||||
<Text fz={"lg"}>{beritaState.berita.findUnique.data?.kategoriBerita?.name}</Text>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text fw={"bold"} fz={"lg"}>Judul</Text>
|
|
||||||
<Text fz={"lg"}>{beritaState.berita.findUnique.data?.judul}</Text>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
|
|
||||||
<Text fz={"lg"} >{beritaState.berita.findUnique.data?.deskripsi}</Text>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
|
|
||||||
<Image w={{ base: 150, md: 150, lg: 150 }} src={beritaState.berita.findUnique.data?.image?.link} alt="gambar" />
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text fw={"bold"} fz={"lg"}>Konten</Text>
|
|
||||||
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: beritaState.berita.findUnique.data?.content }} />
|
|
||||||
</Box>
|
|
||||||
<Flex gap={"xs"} mt={10}>
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
variant="subtle"
|
||||||
if (beritaState.berita.findUnique.data) {
|
onClick={() => router.back()}
|
||||||
setSelectedId(beritaState.berita.findUnique.data.id);
|
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
|
||||||
setModalHapus(true);
|
mb={15}
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={beritaState.berita.delete.loading || !beritaState.berita.findUnique.data}
|
|
||||||
color={"red"}
|
|
||||||
>
|
>
|
||||||
<IconX size={20} />
|
Kembali
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
{/* Detail Berita */}
|
||||||
|
<Paper
|
||||||
|
withBorder
|
||||||
|
w={{ base: "100%", md: "70%" }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
|
||||||
|
Detail Berita
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">Kategori</Text>
|
||||||
|
<Text fz="md" c="dimmed">{data.kategoriBerita?.name || '-'}</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">Judul</Text>
|
||||||
|
<Text fz="md" c="dimmed">{data.judul || '-'}</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">Deskripsi</Text>
|
||||||
|
<Text fz="md" c="dimmed">{data.deskripsi || '-'}</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">Gambar</Text>
|
||||||
|
{data.image?.link ? (
|
||||||
|
<Image
|
||||||
|
src={data.image.link}
|
||||||
|
alt={data.judul || 'Gambar Berita'}
|
||||||
|
w={200}
|
||||||
|
h={200}
|
||||||
|
radius="md"
|
||||||
|
fit="cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">Konten</Text>
|
||||||
|
<Text
|
||||||
|
fz="md"
|
||||||
|
c="dimmed"
|
||||||
|
dangerouslySetInnerHTML={{ __html: data.content || '-' }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Action Button */}
|
||||||
|
<Group gap="sm">
|
||||||
|
<Tooltip label="Hapus Berita" withArrow position="top">
|
||||||
<Button
|
<Button
|
||||||
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (beritaState.berita.findUnique.data) {
|
setSelectedId(data.id);
|
||||||
router.push(`/admin/desa/berita/list-berita/${beritaState.berita.findUnique.data.id}/edit`);
|
setModalHapus(true);
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
disabled={!beritaState.berita.findUnique.data}
|
variant="light"
|
||||||
color={"green"}
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
>
|
||||||
|
<IconTrash size={20} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip label="Edit Berita" withArrow position="top">
|
||||||
|
<Button
|
||||||
|
color="green"
|
||||||
|
onClick={() => router.push(`/admin/desa/berita/list-berita/${data.id}/edit`)}
|
||||||
|
variant="light"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
>
|
>
|
||||||
<IconEdit size={20} />
|
<IconEdit size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
) : null}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{/* Modal Konfirmasi Hapus */}
|
{/* Modal Hapus */}
|
||||||
<ModalKonfirmasiHapus
|
<ModalKonfirmasiHapus
|
||||||
opened={modalHapus}
|
opened={modalHapus}
|
||||||
onClose={() => setModalHapus(false)}
|
onClose={() => setModalHapus(false)}
|
||||||
onConfirm={handleHapus}
|
onConfirm={handleHapus}
|
||||||
text='Apakah anda yakin ingin menghapus berita ini?'
|
text="Apakah Anda yakin ingin menghapus berita ini?"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,7 +3,19 @@ import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
|||||||
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
|
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import ApiFetch from '@/lib/api-fetch';
|
import ApiFetch from '@/lib/api-fetch';
|
||||||
import { Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Image,
|
||||||
|
Paper,
|
||||||
|
Select,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
import { Dropzone } from '@mantine/dropzone';
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||||
@@ -12,38 +24,33 @@ import { useState } from 'react';
|
|||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
|
|
||||||
export default function CreateBerita() {
|
export default function CreateBerita() {
|
||||||
const beritaState = useProxy(stateDashboardBerita);
|
const beritaState = useProxy(stateDashboardBerita);
|
||||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
beritaState.kategoriBerita.findMany.load()
|
beritaState.kategoriBerita.findMany.load();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
// Reset state di valtio
|
|
||||||
beritaState.berita.create.form = {
|
beritaState.berita.create.form = {
|
||||||
judul: "",
|
judul: '',
|
||||||
deskripsi: "",
|
deskripsi: '',
|
||||||
kategoriBeritaId: "",
|
kategoriBeritaId: '',
|
||||||
imageId: "",
|
imageId: '',
|
||||||
content: "",
|
content: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Reset state lokal
|
|
||||||
setPreviewImage(null);
|
setPreviewImage(null);
|
||||||
setFile(null);
|
setFile(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return toast.warn("Pilih file gambar terlebih dahulu");
|
return toast.warn('Silakan pilih file gambar terlebih dahulu');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload gambar dulu
|
|
||||||
const res = await ApiFetch.api.fileStorage.create.post({
|
const res = await ApiFetch.api.fileStorage.create.post({
|
||||||
file,
|
file,
|
||||||
name: file.name,
|
name: file.name,
|
||||||
@@ -51,40 +58,55 @@ export default function CreateBerita() {
|
|||||||
|
|
||||||
const uploaded = res.data?.data;
|
const uploaded = res.data?.data;
|
||||||
if (!uploaded?.id) {
|
if (!uploaded?.id) {
|
||||||
return toast.error("Gagal upload gambar");
|
return toast.error('Gagal mengunggah gambar, silakan coba lagi');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simpan ID gambar ke form
|
|
||||||
beritaState.berita.create.form.imageId = uploaded.id;
|
beritaState.berita.create.form.imageId = uploaded.id;
|
||||||
|
|
||||||
// Submit data berita
|
|
||||||
await beritaState.berita.create.create();
|
await beritaState.berita.create.create();
|
||||||
|
|
||||||
// Reset form setelah submit
|
|
||||||
resetForm();
|
resetForm();
|
||||||
router.push("/admin/desa/berita/list-berita")
|
router.push('/admin/desa/berita/list-berita');
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Header dengan tombol kembali */}
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
p="xs"
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Tooltip>
|
||||||
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
|
<Title order={4} ml="sm" c="dark">
|
||||||
<Stack gap={"xs"}>
|
Tambah Berita
|
||||||
<Title order={3}>Create Berita</Title>
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Paper
|
||||||
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label="Judul"
|
||||||
|
placeholder="Masukkan judul berita"
|
||||||
value={beritaState.berita.create.form.judul}
|
value={beritaState.berita.create.form.judul}
|
||||||
onChange={(val) => {
|
onChange={(e) => (beritaState.berita.create.form.judul = e.target.value)}
|
||||||
beritaState.berita.create.form.judul = val.target.value;
|
required
|
||||||
}}
|
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
|
|
||||||
placeholder="masukkan judul"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Kategori</Text>}
|
label="Kategori"
|
||||||
placeholder="Pilih kategori"
|
placeholder="Pilih kategori"
|
||||||
data={beritaState.kategoriBerita.findMany.data.map((item) => ({
|
data={beritaState.kategoriBerita.findMany.data.map((item) => ({
|
||||||
label: item.name,
|
label: item.name,
|
||||||
@@ -93,85 +115,83 @@ export default function CreateBerita() {
|
|||||||
value={beritaState.berita.create.form.kategoriBeritaId || null}
|
value={beritaState.berita.create.form.kategoriBeritaId || null}
|
||||||
onChange={(val: string | null) => {
|
onChange={(val: string | null) => {
|
||||||
if (val) {
|
if (val) {
|
||||||
const selected = beritaState.kategoriBerita.findMany.data?.find((item) => item.id === val);
|
const selected = beritaState.kategoriBerita.findMany.data?.find(
|
||||||
|
(item) => item.id === val
|
||||||
|
);
|
||||||
if (selected) {
|
if (selected) {
|
||||||
beritaState.berita.create.form.kategoriBeritaId = selected.id;
|
beritaState.berita.create.form.kategoriBeritaId = selected.id;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
beritaState.berita.create.form.kategoriBeritaId = "";
|
beritaState.berita.create.form.kategoriBeritaId = '';
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
searchable
|
searchable
|
||||||
clearable
|
clearable
|
||||||
nothingFoundMessage="Tidak ditemukan"
|
nothingFoundMessage="Tidak ditemukan"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label="Deskripsi Singkat"
|
||||||
|
placeholder="Masukkan deskripsi berita"
|
||||||
value={beritaState.berita.create.form.deskripsi}
|
value={beritaState.berita.create.form.deskripsi}
|
||||||
onChange={(val) => {
|
onChange={(e) => (beritaState.berita.create.form.deskripsi = e.target.value)}
|
||||||
beritaState.berita.create.form.deskripsi = val.target.value;
|
|
||||||
}}
|
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>}
|
|
||||||
placeholder="masukkan deskripsi"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
<Box>
|
Gambar Berita
|
||||||
|
</Text>
|
||||||
<Dropzone
|
<Dropzone
|
||||||
onDrop={(files) => {
|
onDrop={(files) => {
|
||||||
const selectedFile = files[0]; // Ambil file pertama
|
const selectedFile = files[0];
|
||||||
if (selectedFile) {
|
if (selectedFile) {
|
||||||
setFile(selectedFile);
|
setFile(selectedFile);
|
||||||
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
|
setPreviewImage(URL.createObjectURL(selectedFile));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onReject={() => toast.error('File tidak valid.')}
|
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
||||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
maxSize={5 * 1024 ** 2}
|
||||||
accept={{ 'image/*': [] }}
|
accept={{ 'image/*': [] }}
|
||||||
|
radius="md"
|
||||||
|
p="xl"
|
||||||
>
|
>
|
||||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
<Group justify="center" gap="xl" mih={180}>
|
||||||
<Dropzone.Accept>
|
<Dropzone.Accept>
|
||||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
<IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||||
</Dropzone.Accept>
|
</Dropzone.Accept>
|
||||||
<Dropzone.Reject>
|
<Dropzone.Reject>
|
||||||
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
<IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||||
</Dropzone.Reject>
|
</Dropzone.Reject>
|
||||||
<Dropzone.Idle>
|
<Dropzone.Idle>
|
||||||
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
<IconPhoto size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||||
</Dropzone.Idle>
|
</Dropzone.Idle>
|
||||||
|
|
||||||
<div>
|
|
||||||
<Text size="xl" inline>
|
|
||||||
Drag gambar ke sini atau klik untuk pilih file
|
|
||||||
</Text>
|
|
||||||
<Text size="sm" c="dimmed" inline mt={7}>
|
|
||||||
Maksimal 5MB dan harus format gambar
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</Group>
|
</Group>
|
||||||
|
<Text ta="center" mt="sm" size="sm" color="dimmed">
|
||||||
|
Seret gambar atau klik untuk memilih file (maks 5MB)
|
||||||
|
</Text>
|
||||||
</Dropzone>
|
</Dropzone>
|
||||||
|
|
||||||
{/* Tampilkan preview kalau ada */}
|
|
||||||
{previewImage && (
|
{previewImage && (
|
||||||
<Box mt="sm">
|
<Box mt="sm" style={{ textAlign: 'center' }}>
|
||||||
<Image
|
<Image
|
||||||
src={previewImage}
|
src={previewImage}
|
||||||
alt="Preview"
|
alt="Preview Gambar"
|
||||||
|
radius="md"
|
||||||
style={{
|
style={{
|
||||||
maxWidth: '100%',
|
maxHeight: 200,
|
||||||
maxHeight: '200px',
|
|
||||||
objectFit: 'contain',
|
objectFit: 'contain',
|
||||||
borderRadius: '8px',
|
|
||||||
border: '1px solid #ddd',
|
border: '1px solid #ddd',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"sm"} fw={"bold"}>Konten</Text>
|
<Text fz="sm" fw="bold" mb={6}>
|
||||||
|
Konten
|
||||||
|
</Text>
|
||||||
<CreateEditor
|
<CreateEditor
|
||||||
value={beritaState.berita.create.form.content}
|
value={beritaState.berita.create.form.content}
|
||||||
onChange={(htmlContent) => {
|
onChange={(htmlContent) => {
|
||||||
@@ -179,7 +199,21 @@ export default function CreateBerita() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan Berita</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>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,6 +1,25 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Grid, GridCol, Image, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Group,
|
||||||
|
Image,
|
||||||
|
Pagination,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
TableTbody,
|
||||||
|
TableTd,
|
||||||
|
TableTh,
|
||||||
|
TableThead,
|
||||||
|
TableTr,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
|
import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
@@ -9,15 +28,13 @@ import { useProxy } from 'valtio/utils';
|
|||||||
import HeaderSearch from '../../../_com/header';
|
import HeaderSearch from '../../../_com/header';
|
||||||
import stateDashboardBerita from '../../../_state/desa/berita';
|
import stateDashboardBerita from '../../../_state/desa/berita';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function Berita() {
|
function Berita() {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
title='Berita'
|
title="Berita"
|
||||||
placeholder='pencarian'
|
placeholder="Cari judul atau kategori berita..."
|
||||||
searchIcon={<IconSearch size={20} />}
|
searchIcon={<IconSearch size={20} />}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
@@ -28,103 +45,125 @@ function Berita() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ListBerita({ search }: { search: string }) {
|
function ListBerita({ search }: { search: string }) {
|
||||||
const beritaState = useProxy(stateDashboardBerita)
|
const beritaState = useProxy(stateDashboardBerita);
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
const {
|
|
||||||
data,
|
|
||||||
page,
|
|
||||||
totalPages,
|
|
||||||
loading,
|
|
||||||
load,
|
|
||||||
} = beritaState.berita.findMany;
|
|
||||||
|
|
||||||
|
const { data, page, totalPages, loading, load } = beritaState.berita.findMany;
|
||||||
|
|
||||||
// Fetch data when page or search changes
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
load(page, 10, search);
|
load(page, 10, search);
|
||||||
}, [page, search]);
|
}, [page, search]);
|
||||||
|
|
||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
return <Skeleton h={500} />;
|
return (
|
||||||
|
<Stack py={10}>
|
||||||
|
<Skeleton height={600} radius="md" />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredData = data || [];
|
const filteredData = data || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={10}>
|
||||||
<Paper bg={colors["white-1"]} p={"md"}>
|
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||||
<Stack>
|
<Group justify="space-between" mb="md">
|
||||||
<Grid>
|
<Title order={4}>Daftar Berita</Title>
|
||||||
<GridCol span={{ base: 12, md: 11 }}>
|
<Tooltip label="Tambah Berita" withArrow>
|
||||||
<Text fz={"xl"} fw={"bold"}>
|
|
||||||
List Berita
|
|
||||||
</Text>
|
|
||||||
</GridCol>
|
|
||||||
<GridCol span={{ base: 12, md: 1 }}>
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => router.push("/admin/desa/berita/list-berita/create")}
|
leftSection={<IconCircleDashedPlus size={18} />}
|
||||||
bg={colors["blue-button"]}
|
color="blue"
|
||||||
|
variant="light"
|
||||||
|
onClick={() => router.push('/admin/desa/berita/list-berita/create')}
|
||||||
>
|
>
|
||||||
<IconCircleDashedPlus size={25} />
|
Tambah Baru
|
||||||
</Button>
|
</Button>
|
||||||
</GridCol>
|
</Tooltip>
|
||||||
</Grid>
|
</Group>
|
||||||
<Box style={{ overflowX: "auto" }}>
|
|
||||||
<Table
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
striped
|
<Table highlightOnHover>
|
||||||
withRowBorders
|
|
||||||
withTableBorder
|
|
||||||
style={{ minWidth: "700px" }}
|
|
||||||
>
|
|
||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh w={250}>Judul</TableTh>
|
<TableTh style={{ width: '30%' }}>Judul</TableTh>
|
||||||
<TableTh w={250}>Kategori</TableTh>
|
<TableTh style={{ width: '20%' }}>Kategori</TableTh>
|
||||||
<TableTh w={250}>Image</TableTh>
|
<TableTh style={{ width: '25%' }}>Gambar</TableTh>
|
||||||
<TableTh w={200}>Detail</TableTh>
|
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
</TableThead>
|
</TableThead>
|
||||||
<TableTbody>
|
<TableTbody>
|
||||||
{filteredData.map((item) => (
|
{filteredData.length > 0 ? (
|
||||||
|
filteredData.map((item) => (
|
||||||
<TableTr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
<TableTd>
|
<TableTd style={{ width: '30%' }}>
|
||||||
<Box w={100}>
|
<Text fw={500} truncate="end" lineClamp={1}>
|
||||||
<Text truncate="end" fz={"sm"}>
|
|
||||||
{item.judul}
|
{item.judul}
|
||||||
</Text>
|
</Text>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd style={{ width: '20%' }}>
|
||||||
|
<Text fz="sm" c="dimmed">
|
||||||
|
{item.kategoriBerita?.name || '-'}
|
||||||
|
</Text>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd style={{ width: '25%' }}>
|
||||||
|
<Box
|
||||||
|
w={80}
|
||||||
|
h={80}
|
||||||
|
style={{ borderRadius: 8, overflow: 'hidden' }}
|
||||||
|
>
|
||||||
|
{item.image?.link ? (
|
||||||
|
<Image src={item.image.link} alt="gambar" fit="cover" />
|
||||||
|
) : (
|
||||||
|
<Box bg={colors['blue-button']} w="100%" h="100%" />
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd>{item.kategoriBerita?.name}</TableTd>
|
<TableTd style={{ width: '15%' }}>
|
||||||
<TableTd>
|
|
||||||
<Image w={100} src={item.image?.link} alt="gambar" />
|
|
||||||
</TableTd>
|
|
||||||
<TableTd>
|
|
||||||
<Button
|
<Button
|
||||||
bg={"green"}
|
variant="light"
|
||||||
|
color="blue"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
router.push(`/admin/desa/berita/list-berita/${item.id}`)
|
router.push(`/admin/desa/berita/list-berita/${item.id}`)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<IconDeviceImacCog size={25} />
|
<IconDeviceImacCog size={20} />
|
||||||
|
<Text ml={5}>Detail</Text>
|
||||||
</Button>
|
</Button>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))}
|
))
|
||||||
|
) : (
|
||||||
|
<TableTr>
|
||||||
|
<TableTd colSpan={4}>
|
||||||
|
<Center py={20}>
|
||||||
|
<Text color="dimmed">
|
||||||
|
Tidak ada data berita yang cocok
|
||||||
|
</Text>
|
||||||
|
</Center>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
)}
|
||||||
</TableTbody>
|
</TableTbody>
|
||||||
</Table>
|
</Table>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
<Center>
|
<Center>
|
||||||
<Pagination
|
<Pagination
|
||||||
value={page}
|
value={page}
|
||||||
onChange={(newPage) => load(newPage)} // ini penting!
|
onChange={(newPage) => {
|
||||||
|
load(newPage, 10);
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
}}
|
||||||
total={totalPages}
|
total={totalPages}
|
||||||
mt="md"
|
mt="md"
|
||||||
mb="md"
|
mb="md"
|
||||||
|
color="blue"
|
||||||
|
radius="md"
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Berita;
|
export default Berita;
|
||||||
|
|||||||
@@ -1,161 +0,0 @@
|
|||||||
'use client'
|
|
||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
|
||||||
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
|
||||||
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
|
|
||||||
import colors from '@/con/colors';
|
|
||||||
import ApiFetch from '@/lib/api-fetch';
|
|
||||||
import { Box, Button, Center, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
|
||||||
import { Dropzone } from '@mantine/dropzone';
|
|
||||||
import { IconArrowBack, IconImageInPicture, IconPhoto, 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';
|
|
||||||
|
|
||||||
|
|
||||||
function EditFoto() {
|
|
||||||
const fotoState = useProxy(stateGallery.foto)
|
|
||||||
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: fotoState.update.form.name || '',
|
|
||||||
deskripsi: fotoState.update.form.deskripsi || '',
|
|
||||||
imagesId: fotoState.update.form.imagesId || ''
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const loadFoto = async () => {
|
|
||||||
const id = params?.id as string;
|
|
||||||
if (!id) return;
|
|
||||||
try {
|
|
||||||
const data = await fotoState.update.load(id);
|
|
||||||
if (data) {
|
|
||||||
setFormData({
|
|
||||||
name: data.name || '',
|
|
||||||
deskripsi: data.deskripsi || '',
|
|
||||||
imagesId: data.imageGalleryFoto?.id || ''
|
|
||||||
});
|
|
||||||
if (data?.imageGalleryFoto?.link) {
|
|
||||||
setPreviewImage(data.imageGalleryFoto.link);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading foto:', error);
|
|
||||||
toast.error('Gagal memuat data foto');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
loadFoto();
|
|
||||||
}, [params?.id]);
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
try {
|
|
||||||
fotoState.update.form = {
|
|
||||||
...fotoState.update.form,
|
|
||||||
name: formData.name,
|
|
||||||
deskripsi: formData.deskripsi,
|
|
||||||
imagesId: formData.imagesId
|
|
||||||
};
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
fotoState.update.form.imagesId = uploaded.id;
|
|
||||||
}
|
|
||||||
await fotoState.update.update();
|
|
||||||
toast.success('Foto berhasil diperbarui!');
|
|
||||||
router.push('/admin/desa/gallery/foto');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error updating foto:', error);
|
|
||||||
toast.error('Terjadi kesalahan saat memperbarui foto');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Box mb={10}>
|
|
||||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
|
||||||
<Stack gap={"xs"}>
|
|
||||||
<Title order={4}>Edit Foto</Title>
|
|
||||||
<TextInput
|
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Judul Foto</Text>}
|
|
||||||
placeholder='Masukkan judul foto'
|
|
||||||
value={formData.name}
|
|
||||||
onChange={(e) =>
|
|
||||||
(formData.name = e.target.value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Box>
|
|
||||||
<Text>Upload Foto</Text>
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
{previewImage ? (
|
|
||||||
<Image alt="" src={previewImage} w={200} h={200} />
|
|
||||||
) : (
|
|
||||||
<Center w={200} h={200} bg={"gray"}>
|
|
||||||
<IconImageInPicture />
|
|
||||||
</Center>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text fw={"bold"} fz={"sm"}>Deskripsi Foto</Text>
|
|
||||||
<EditEditor
|
|
||||||
value={fotoState.update.form.deskripsi}
|
|
||||||
onChange={(val) => {
|
|
||||||
fotoState.update.form.deskripsi = val;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Group>
|
|
||||||
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default EditFoto;
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
'use client'
|
|
||||||
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
|
|
||||||
import React from 'react';
|
|
||||||
import { useProxy } from 'valtio/utils';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
|
||||||
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
|
||||||
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
|
|
||||||
import colors from '@/con/colors';
|
|
||||||
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
|
||||||
|
|
||||||
function DetailFoto() {
|
|
||||||
const fotoState = useProxy(stateGallery.foto)
|
|
||||||
const [modalHapus, setModalHapus] = useState(false);
|
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
|
||||||
const params = useParams()
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
useShallowEffect(() => {
|
|
||||||
fotoState.findUnique.load(params?.id as string)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleHapus = () => {
|
|
||||||
if (selectedId) {
|
|
||||||
fotoState.delete.byId(selectedId)
|
|
||||||
setModalHapus(false)
|
|
||||||
setSelectedId(null)
|
|
||||||
router.push("/admin/desa/gallery/foto")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fotoState.findUnique.data) {
|
|
||||||
return (
|
|
||||||
<Stack py={10}>
|
|
||||||
<Skeleton h={500} />
|
|
||||||
</Stack>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 Foto</Text>
|
|
||||||
{fotoState.findUnique.data ? (
|
|
||||||
<Paper key={fotoState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
|
||||||
<Stack gap={"xs"}>
|
|
||||||
<Box>
|
|
||||||
<Text fw={"bold"} fz={"lg"}>Judul</Text>
|
|
||||||
<Text fz={"lg"}>{fotoState.findUnique.data?.name}</Text>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text fw={"bold"} fz={"lg"}>Tanggal Foto</Text>
|
|
||||||
<Text fz={"lg"}>{new Date(fotoState.findUnique.data?.createdAt).toDateString()}</Text>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
|
|
||||||
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: fotoState.findUnique.data?.deskripsi }} />
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
|
|
||||||
<Image w={{ base: 300, md: 350}} src={fotoState.findUnique.data?.imageGalleryFoto?.link} alt="gambar" />
|
|
||||||
</Box>
|
|
||||||
<Flex gap={"xs"} mt={10}>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
if (fotoState.findUnique.data) {
|
|
||||||
setSelectedId(fotoState.findUnique.data.id);
|
|
||||||
setModalHapus(true);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={fotoState.delete.loading || !fotoState.findUnique.data}
|
|
||||||
color={"red"}
|
|
||||||
>
|
|
||||||
<IconX size={20} />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
if (fotoState.findUnique.data) {
|
|
||||||
router.push(`/admin/desa/gallery/foto/${fotoState.findUnique.data.id}/edit`);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={!fotoState.findUnique.data}
|
|
||||||
color={"green"}
|
|
||||||
>
|
|
||||||
<IconEdit size={20} />
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
|
||||||
) : null}
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
|
||||||
|
|
||||||
{/* Modal Konfirmasi Hapus */}
|
|
||||||
<ModalKonfirmasiHapus
|
|
||||||
opened={modalHapus}
|
|
||||||
onClose={() => setModalHapus(false)}
|
|
||||||
onConfirm={handleHapus}
|
|
||||||
text='Apakah anda yakin ingin menghapus berita ini?'
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DetailFoto;
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
'use client'
|
|
||||||
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
|
||||||
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
|
|
||||||
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 { Dropzone } from '@mantine/dropzone';
|
|
||||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { toast } from 'react-toastify';
|
|
||||||
import { useProxy } from 'valtio/utils';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function CreateFoto() {
|
|
||||||
const fotoState = useProxy(stateGallery.foto)
|
|
||||||
const router = useRouter();
|
|
||||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
|
||||||
const [file, setFile] = useState<File | null>(null);
|
|
||||||
|
|
||||||
const resetForm = () => {
|
|
||||||
fotoState.create.form = {
|
|
||||||
name: "",
|
|
||||||
deskripsi: "",
|
|
||||||
imagesId: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
setPreviewImage(null)
|
|
||||||
setFile(null)
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
if (!file) {
|
|
||||||
return toast.warn("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 upload gambar");
|
|
||||||
}
|
|
||||||
|
|
||||||
fotoState.create.form.imagesId = uploaded.id;
|
|
||||||
await fotoState.create.create();
|
|
||||||
resetForm();
|
|
||||||
router.push("/admin/desa/gallery/foto")
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Box mb={10}>
|
|
||||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
|
||||||
<Stack gap={"xs"}>
|
|
||||||
<Title order={4}>Create Foto</Title>
|
|
||||||
<TextInput
|
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Judul Foto</Text>}
|
|
||||||
placeholder='Masukkan judul foto'
|
|
||||||
value={fotoState.create.form.name}
|
|
||||||
onChange={(val) => {
|
|
||||||
fotoState.create.form.name = val.target.value;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
<Box>
|
|
||||||
<Text fw={"bold"} fz={"sm"}>Deskripsi Foto</Text>
|
|
||||||
<CreateEditor
|
|
||||||
value={fotoState.create.form.deskripsi}
|
|
||||||
onChange={(val) => {
|
|
||||||
fotoState.create.form.deskripsi = val;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Group>
|
|
||||||
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CreateFoto;
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import colors from "@/con/colors";
|
|
||||||
import stateFileStorage from "@/state/state-list-image";
|
import stateFileStorage from "@/state/state-list-image";
|
||||||
import {
|
import {
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
Box,
|
Box,
|
||||||
|
Card,
|
||||||
Flex,
|
Flex,
|
||||||
Group,
|
Group,
|
||||||
Image,
|
Image,
|
||||||
@@ -13,7 +13,8 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title
|
Title,
|
||||||
|
Tooltip,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useShallowEffect } from "@mantine/hooks";
|
import { useShallowEffect } from "@mantine/hooks";
|
||||||
import { IconSearch, IconTrash, IconX } from "@tabler/icons-react";
|
import { IconSearch, IconTrash, IconX } from "@tabler/icons-react";
|
||||||
@@ -29,95 +30,128 @@ export default function ListImage() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
let timeOut: NodeJS.Timer;
|
let timeOut: NodeJS.Timer;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack p={"lg"}>
|
<Stack p="lg" gap="lg">
|
||||||
<Flex justify="space-between">
|
<Flex justify="space-between" align="center" wrap="wrap" gap="md">
|
||||||
<Title order={3}>List Foto</Title>
|
<Title order={2} fw={700}>
|
||||||
|
Galeri Foto
|
||||||
|
</Title>
|
||||||
<TextInput
|
<TextInput
|
||||||
radius={"lg"}
|
radius="xl"
|
||||||
leftSection={<IconSearch />}
|
size="md"
|
||||||
|
placeholder="Cari foto berdasarkan nama..."
|
||||||
|
leftSection={<IconSearch size={18} />}
|
||||||
rightSection={
|
rightSection={
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="transparent"
|
variant="light"
|
||||||
onClick={() => {
|
color="gray"
|
||||||
stateFileStorage.load();
|
radius="xl"
|
||||||
}}
|
onClick={() => stateFileStorage.load()}
|
||||||
>
|
>
|
||||||
<IconX />
|
<IconX size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
}
|
}
|
||||||
placeholder="Pencarian"
|
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
if (timeOut) clearTimeout(timeOut);
|
if (timeOut) clearTimeout(timeOut);
|
||||||
timeOut = setTimeout(() => {
|
timeOut = setTimeout(() => {
|
||||||
stateFileStorage.load({ search: e.target.value });
|
stateFileStorage.load({ search: e.target.value });
|
||||||
}, 200);
|
}, 300);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
|
||||||
|
<Paper withBorder radius="lg" p="md" shadow="sm">
|
||||||
|
{list && list.length > 0 ? (
|
||||||
<SimpleGrid
|
<SimpleGrid
|
||||||
cols={{
|
cols={{ base: 2, sm: 3, md: 5, lg: 8 }}
|
||||||
base: 3,
|
spacing="md"
|
||||||
md: 5,
|
verticalSpacing="md"
|
||||||
lg: 10,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{list &&
|
{list.map((v, k) => (
|
||||||
list.map((v, k) => {
|
<Card
|
||||||
return (
|
key={k}
|
||||||
<Paper key={k} shadow="sm">
|
withBorder
|
||||||
<Stack pos={"relative"} gap={0} justify="space-between">
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
className="hover:shadow-md transition-all duration-200"
|
||||||
|
>
|
||||||
|
<Stack gap="xs">
|
||||||
<motion.div
|
<motion.div
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// copy to clipboard
|
|
||||||
navigator.clipboard.writeText(v.url);
|
navigator.clipboard.writeText(v.url);
|
||||||
toast("Berhasil disalin");
|
toast("Tautan foto berhasil disalin");
|
||||||
}}
|
}}
|
||||||
whileHover={{ scale: 1.05 }}
|
whileHover={{ scale: 1.05 }}
|
||||||
whileTap={{ scale: 0.8 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
h={100}
|
src={`${v.url}?size=200`}
|
||||||
src={v.url + "?size=100"}
|
|
||||||
alt={v.name}
|
alt={v.name}
|
||||||
|
radius="md"
|
||||||
|
h={120}
|
||||||
fit="cover"
|
fit="cover"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
style={{
|
|
||||||
objectFit: "cover",
|
|
||||||
objectPosition: "center",
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<Box p={"md"} h={54}>
|
|
||||||
<Text lineClamp={2} fz={"xs"}>
|
<Box>
|
||||||
|
<Text size="sm" fw={500} lineClamp={2}>
|
||||||
{v.name}
|
{v.name}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Group justify="end">
|
|
||||||
<IconTrash
|
<Group justify="space-between" align="center" pt="xs">
|
||||||
|
<Tooltip label="Hapus foto" withArrow>
|
||||||
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
color="red"
|
color="red"
|
||||||
|
radius="md"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
stateFileStorage.del({ name: v.name }).finally(() => {
|
stateFileStorage
|
||||||
toast("Berhasil dihapus");
|
.del({ name: v.name })
|
||||||
});
|
.finally(() => toast("Foto berhasil dihapus"));
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<IconTrash size={18} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Card>
|
||||||
);
|
))}
|
||||||
})}
|
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
) : (
|
||||||
|
<Stack align="center" justify="center" py="xl" gap="sm">
|
||||||
|
<Image
|
||||||
|
src="https://cdn-icons-png.flaticon.com/512/4076/4076549.png"
|
||||||
|
alt="Kosong"
|
||||||
|
w={120}
|
||||||
|
h={120}
|
||||||
|
fit="contain"
|
||||||
|
opacity={0.7}
|
||||||
|
/>
|
||||||
|
<Text c="dimmed" ta="center">
|
||||||
|
Belum ada foto yang tersedia
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
</Paper>
|
</Paper>
|
||||||
{total && (
|
|
||||||
|
{total && total > 1 && (
|
||||||
|
<Flex justify="center">
|
||||||
<Pagination
|
<Pagination
|
||||||
total={total}
|
total={total}
|
||||||
onChange={(e) => {
|
size="md"
|
||||||
stateFileStorage.page = e;
|
radius="md"
|
||||||
|
withEdges
|
||||||
|
onChange={(page) => {
|
||||||
|
stateFileStorage.page = page;
|
||||||
stateFileStorage.load();
|
stateFileStorage.load();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</Flex>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
|
||||||
import { usePathname, useRouter } from 'next/navigation';
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { IconPhoto, IconVideo } from '@tabler/icons-react';
|
||||||
|
|
||||||
function LayoutTabsGallery({ children }: { children: React.ReactNode }) {
|
function LayoutTabsGallery({ children }: { children: React.ReactNode }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -12,16 +13,21 @@ function LayoutTabsGallery({ children }: { children: React.ReactNode }) {
|
|||||||
{
|
{
|
||||||
label: "Foto",
|
label: "Foto",
|
||||||
value: "foto",
|
value: "foto",
|
||||||
href: "/admin/desa/gallery/foto"
|
href: "/admin/desa/gallery/foto",
|
||||||
|
icon: <IconPhoto size={18} stroke={1.8} />,
|
||||||
|
tooltip: "Kelola foto-foto galeri desa"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Video",
|
label: "Video",
|
||||||
value: "video",
|
value: "video",
|
||||||
href: "/admin/desa/gallery/video"
|
href: "/admin/desa/gallery/video",
|
||||||
|
icon: <IconVideo size={18} stroke={1.8} />,
|
||||||
|
tooltip: "Kelola video galeri desa"
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const curentTab = tabs.find(tab => tab.href === pathname)
|
|
||||||
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
|
const currentTab = tabs.find(tab => tab.href === pathname)
|
||||||
|
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
|
||||||
|
|
||||||
const handleTabChange = (value: string | null) => {
|
const handleTabChange = (value: string | null) => {
|
||||||
const tab = tabs.find(t => t.value === value)
|
const tab = tabs.find(t => t.value === value)
|
||||||
@@ -39,22 +45,62 @@ function LayoutTabsGallery({ children }: { children: React.ReactNode }) {
|
|||||||
}, [pathname])
|
}, [pathname])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack gap="lg">
|
||||||
<Title order={3}>Gallery</Title>
|
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Gallery</Title>
|
||||||
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
|
<Tabs
|
||||||
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
|
color={colors['blue-button']}
|
||||||
{tabs.map((e, i) => (
|
variant='pills'
|
||||||
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
|
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
|
||||||
|
transitionProps={{ transition: 'pop', duration: 200 }}
|
||||||
|
>
|
||||||
|
<TabsTab
|
||||||
|
value={tab.value}
|
||||||
|
leftSection={tab.icon}
|
||||||
|
style={{
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
transition: "all 0.2s ease",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</TabsTab>
|
||||||
|
</Tooltip>
|
||||||
))}
|
))}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
{tabs.map((e, i) => (
|
|
||||||
<TabsPanel key={i} value={e.value}>
|
{tabs.map((tab, i) => (
|
||||||
{/* Konten dummy, bisa diganti tergantung routing */}
|
<TabsPanel
|
||||||
<></>
|
key={i}
|
||||||
|
value={tab.value}
|
||||||
|
style={{
|
||||||
|
padding: "1.5rem",
|
||||||
|
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
|
||||||
|
borderRadius: "1rem",
|
||||||
|
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<>{children}</>
|
||||||
</TabsPanel>
|
</TabsPanel>
|
||||||
))}
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
{children}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,16 @@
|
|||||||
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
||||||
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
|
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Tooltip
|
||||||
|
} from '@mantine/core';
|
||||||
import { IconArrowBack } from '@tabler/icons-react';
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
@@ -13,8 +22,8 @@ import { convertYoutubeUrlToEmbed } from '../../../lib/youtube-utils';
|
|||||||
|
|
||||||
function EditVideo() {
|
function EditVideo() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const videoState = useProxy(stateGallery.video)
|
const videoState = useProxy(stateGallery.video);
|
||||||
const params = useParams()
|
const params = useParams();
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
@@ -66,27 +75,36 @@ function EditVideo() {
|
|||||||
console.error('Error updating video:', error);
|
console.error('Error updating video:', error);
|
||||||
toast.error('Terjadi kesalahan saat memperbarui video');
|
toast.error('Terjadi kesalahan saat memperbarui video');
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
<Group mb="md">
|
||||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Tooltip>
|
||||||
|
<Title order={4} ml="sm" c="dark">
|
||||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
Edit Video
|
||||||
<Stack gap={"xs"}>
|
</Title>
|
||||||
<Title order={4}>Edit Video</Title>
|
</Group>
|
||||||
|
|
||||||
|
<Paper
|
||||||
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Judul Video</Text>}
|
label="Judul Video"
|
||||||
placeholder='Masukkan judul video'
|
placeholder="Masukkan judul video"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(val) => {
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
setFormData({ ...formData, name: val.target.value });
|
required
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
@@ -94,36 +112,46 @@ function EditVideo() {
|
|||||||
label="Link Video YouTube"
|
label="Link Video YouTube"
|
||||||
placeholder="https://www.youtube.com/watch?v=abc123"
|
placeholder="https://www.youtube.com/watch?v=abc123"
|
||||||
value={formData.linkVideo}
|
value={formData.linkVideo}
|
||||||
onChange={(e) => {
|
onChange={(e) => setFormData({ ...formData, linkVideo: e.currentTarget.value })}
|
||||||
setFormData({ ...formData, linkVideo: e.currentTarget.value });
|
|
||||||
}}
|
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{embedLink && (
|
{embedLink && (
|
||||||
|
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
|
||||||
<iframe
|
<iframe
|
||||||
className="rounded"
|
className="rounded"
|
||||||
width="100%"
|
width="100%"
|
||||||
height="200"
|
height="220"
|
||||||
src={embedLink}
|
src={embedLink}
|
||||||
title="Preview Video"
|
title="Preview Video"
|
||||||
allowFullScreen
|
allowFullScreen
|
||||||
></iframe>
|
></iframe>
|
||||||
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw={"bold"} fz={"sm"}>Deskripsi Video</Text>
|
<Title order={6} fw="bold" fz="sm" mb={6}>
|
||||||
|
Deskripsi Video
|
||||||
|
</Title>
|
||||||
<EditEditor
|
<EditEditor
|
||||||
value={formData.deskripsi}
|
value={formData.deskripsi}
|
||||||
onChange={(val) => {
|
onChange={(val) => setFormData({ ...formData, deskripsi: val })}
|
||||||
setFormData({ ...formData, deskripsi: val });
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Group>
|
<Group justify="right">
|
||||||
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
|
color: '#fff',
|
||||||
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -2,107 +2,145 @@
|
|||||||
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||||
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
|
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
|
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
|
|
||||||
function DetailVideo() {
|
function DetailVideo() {
|
||||||
const videoState = useProxy(stateGallery.video)
|
const videoState = useProxy(stateGallery.video);
|
||||||
const [modalHapus, setModalHapus] = useState(false);
|
const [modalHapus, setModalHapus] = useState(false);
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
const params = useParams()
|
const params = useParams();
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
videoState.findUnique.load(params?.id as string)
|
videoState.findUnique.load(params?.id as string);
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
const handleHapus = () => {
|
const handleHapus = () => {
|
||||||
if (selectedId) {
|
if (selectedId) {
|
||||||
videoState.delete.byId(selectedId)
|
videoState.delete.byId(selectedId);
|
||||||
setModalHapus(false)
|
setModalHapus(false);
|
||||||
setSelectedId(null)
|
setSelectedId(null);
|
||||||
router.push("/admin/desa/gallery/video")
|
router.push("/admin/desa/gallery/video");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (!videoState.findUnique.data) {
|
if (!videoState.findUnique.data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton h={500} />
|
<Skeleton height={500} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = videoState.findUnique.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box py={10}>
|
||||||
<Box mb={10}>
|
{/* Tombol Kembali */}
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Button
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
variant="subtle"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
|
||||||
|
mb={15}
|
||||||
|
>
|
||||||
|
Kembali
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
|
||||||
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
|
{/* Detail Video */}
|
||||||
<Stack>
|
<Paper
|
||||||
<Text fz={"xl"} fw={"bold"}>Detail Video</Text>
|
withBorder
|
||||||
{videoState.findUnique.data ? (
|
w={{ base: "100%", md: "50%" }}
|
||||||
<Paper key={videoState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
bg={colors['white-1']}
|
||||||
<Stack gap={"xs"}>
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
|
||||||
|
Detail Video
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
|
||||||
|
<Stack gap="sm">
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw={"bold"} fz={"lg"}>Judul</Text>
|
<Text fz="lg" fw="bold">Judul</Text>
|
||||||
<Text fz={"lg"}>{videoState.findUnique.data?.name}</Text>
|
<Text fz="md" c="dimmed">{data?.name || '-'}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw={"bold"} fz={"lg"}>Video</Text>
|
<Text fz="lg" fw="bold">Video</Text>
|
||||||
<Box component="iframe"
|
{data?.linkVideo ? (
|
||||||
src={convertToEmbedUrl(videoState.findUnique.data?.linkVideo)}
|
<Box
|
||||||
|
component="iframe"
|
||||||
|
src={convertToEmbedUrl(data.linkVideo)}
|
||||||
width="100%"
|
width="100%"
|
||||||
height={300}
|
height={300}
|
||||||
allowFullScreen
|
allowFullScreen
|
||||||
style={{ borderRadius: 8 }}
|
style={{ borderRadius: 8 }}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<Text fz="sm" c="dimmed">Tidak ada video</Text>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw={"bold"} fz={"lg"}>Tanggal Video</Text>
|
<Text fz="lg" fw="bold">Tanggal Video</Text>
|
||||||
<Text fz={"lg"}>{new Date(videoState.findUnique.data?.createdAt).toDateString()}</Text>
|
<Text fz="md" c="dimmed">
|
||||||
|
{data?.createdAt ? new Date(data.createdAt).toDateString() : '-'}
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
|
<Text fz="lg" fw="bold">Deskripsi</Text>
|
||||||
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: videoState.findUnique.data?.deskripsi }} />
|
{data?.deskripsi ? (
|
||||||
|
<Text
|
||||||
|
fz="md"
|
||||||
|
c="dimmed"
|
||||||
|
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Text fz="sm" c="dimmed">Tidak ada deskripsi</Text>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Flex gap={"xs"} mt={10}>
|
|
||||||
|
{/* Tombol Aksi */}
|
||||||
|
<Group gap="sm">
|
||||||
|
<Tooltip label="Hapus Video" withArrow position="top">
|
||||||
<Button
|
<Button
|
||||||
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (videoState.findUnique.data) {
|
setSelectedId(data.id);
|
||||||
setSelectedId(videoState.findUnique.data.id);
|
|
||||||
setModalHapus(true);
|
setModalHapus(true);
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
disabled={videoState.delete.loading || !videoState.findUnique.data}
|
variant="light"
|
||||||
color={"red"}
|
radius="md"
|
||||||
|
size="md"
|
||||||
>
|
>
|
||||||
<IconX size={20} />
|
<IconTrash size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip label="Edit Video" withArrow position="top">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
color="green"
|
||||||
if (videoState.findUnique.data) {
|
onClick={() =>
|
||||||
router.push(`/admin/desa/gallery/video/${videoState.findUnique.data.id}/edit`);
|
router.push(`/admin/desa/gallery/video/${data.id}/edit`)
|
||||||
}
|
}
|
||||||
}}
|
variant="light"
|
||||||
disabled={!videoState.findUnique.data}
|
radius="md"
|
||||||
color={"green"}
|
size="md"
|
||||||
>
|
>
|
||||||
<IconEdit size={20} />
|
<IconEdit size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
) : null}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
@@ -111,17 +149,16 @@ function DetailVideo() {
|
|||||||
opened={modalHapus}
|
opened={modalHapus}
|
||||||
onClose={() => setModalHapus(false)}
|
onClose={() => setModalHapus(false)}
|
||||||
onConfirm={handleHapus}
|
onConfirm={handleHapus}
|
||||||
text='Apakah anda yakin ingin menghapus berita ini?'
|
text="Apakah Anda yakin ingin menghapus video ini?"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
function convertToEmbedUrl(youtubeUrl: string): string {
|
function convertToEmbedUrl(youtubeUrl: string): string {
|
||||||
try {
|
try {
|
||||||
const url = new URL(youtubeUrl);
|
const url = new URL(youtubeUrl);
|
||||||
const videoId = url.searchParams.get("v");
|
const videoId = url.searchParams.get("v");
|
||||||
if (!videoId) return youtubeUrl;
|
if (!videoId) return youtubeUrl;
|
||||||
|
|
||||||
return `https://www.youtube.com/embed/${videoId}`;
|
return `https://www.youtube.com/embed/${videoId}`;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error converting YouTube URL to embed:", err);
|
console.error("Error converting YouTube URL to embed:", err);
|
||||||
|
|||||||
@@ -1,8 +1,18 @@
|
|||||||
'use client'
|
'use client';
|
||||||
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
||||||
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
|
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
import { IconArrowBack } from '@tabler/icons-react';
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
@@ -10,77 +20,104 @@ import { toast } from 'react-toastify';
|
|||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import { convertYoutubeUrlToEmbed } from '../../lib/youtube-utils';
|
import { convertYoutubeUrlToEmbed } from '../../lib/youtube-utils';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function CreateVideo() {
|
function CreateVideo() {
|
||||||
const videoState = useProxy(stateGallery.video)
|
const videoState = useProxy(stateGallery.video);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [link, setLink] = useState("");
|
const [link, setLink] = useState('');
|
||||||
const embedLink = convertYoutubeUrlToEmbed(link);
|
const embedLink = convertYoutubeUrlToEmbed(link);
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
videoState.create.form = {
|
videoState.create.form = {
|
||||||
name: "",
|
name: '',
|
||||||
deskripsi: "",
|
deskripsi: '',
|
||||||
linkVideo: "",
|
linkVideo: '',
|
||||||
};
|
};
|
||||||
|
setLink('');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!embedLink) {
|
if (!embedLink) {
|
||||||
toast.error("Link YouTube tidak valid. Pastikan formatnya benar.");
|
toast.error('Link YouTube tidak valid. Pastikan formatnya benar.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
videoState.create.form.linkVideo = embedLink; // pastikan diset di sini juga (jaga-jaga)
|
videoState.create.form.linkVideo = embedLink;
|
||||||
await videoState.create.create();
|
await videoState.create.create();
|
||||||
resetForm();
|
resetForm();
|
||||||
router.push("/admin/desa/gallery/video");
|
router.push('/admin/desa/gallery/video');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Header Back Button + Title */}
|
||||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
p="xs"
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Tooltip>
|
||||||
|
<Title order={4} ml="sm" c="dark">
|
||||||
|
Tambah Video
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
{/* Card Form */}
|
||||||
<Stack gap={"xs"}>
|
<Paper
|
||||||
<Title order={4}>Create Video</Title>
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
{/* Judul */}
|
||||||
<TextInput
|
<TextInput
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Judul Video</Text>}
|
label="Judul Video"
|
||||||
placeholder='Masukkan judul video'
|
placeholder="Masukkan judul video"
|
||||||
value={videoState.create.form.name}
|
value={videoState.create.form.name}
|
||||||
onChange={(val) => {
|
|
||||||
videoState.create.form.name = val.target.value;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Box>
|
|
||||||
<Stack gap={"xs"}>
|
|
||||||
<TextInput
|
|
||||||
label="Link Video YouTube"
|
|
||||||
placeholder="https://www.youtube.com/watch?v=abc123"
|
|
||||||
value={link}
|
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setLink(e.currentTarget.value);
|
videoState.create.form.name = e.currentTarget.value;
|
||||||
}}
|
}}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Link YouTube */}
|
||||||
|
<TextInput
|
||||||
|
label="Link Video YouTube"
|
||||||
|
placeholder="https://www.youtube.com/watch?v=abc123"
|
||||||
|
value={link}
|
||||||
|
onChange={(e) => setLink(e.currentTarget.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Preview Video */}
|
||||||
{embedLink && (
|
{embedLink && (
|
||||||
|
<Box mt="sm">
|
||||||
<iframe
|
<iframe
|
||||||
style={{ borderRadius: 10, width: "100%", height: 400 }}
|
style={{
|
||||||
|
borderRadius: 10,
|
||||||
|
width: '100%',
|
||||||
|
height: 400,
|
||||||
|
border: '1px solid #ddd',
|
||||||
|
}}
|
||||||
src={embedLink}
|
src={embedLink}
|
||||||
title="Preview Video"
|
title="Preview Video"
|
||||||
allowFullScreen
|
allowFullScreen
|
||||||
></iframe>
|
></iframe>
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Deskripsi */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw={"bold"} fz={"sm"}>Deskripsi Video</Text>
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
|
Deskripsi Video
|
||||||
|
</Text>
|
||||||
<CreateEditor
|
<CreateEditor
|
||||||
value={videoState.create.form.deskripsi}
|
value={videoState.create.form.deskripsi}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
@@ -88,8 +125,21 @@ function CreateVideo() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Group>
|
|
||||||
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
|
{/* Button Submit */}
|
||||||
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
|
color: '#fff',
|
||||||
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -1,13 +1,30 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Pagination, Paper, Skeleton, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Group,
|
||||||
|
Pagination,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
TableTbody,
|
||||||
|
TableTd,
|
||||||
|
TableTh,
|
||||||
|
TableThead,
|
||||||
|
TableTr,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
Tooltip
|
||||||
|
} from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
|
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import HeaderSearch from '../../../_com/header';
|
import HeaderSearch from '../../../_com/header';
|
||||||
import JudulList from '../../../_com/judulList';
|
|
||||||
import stateGallery from '../../../_state/desa/gallery';
|
import stateGallery from '../../../_state/desa/gallery';
|
||||||
|
|
||||||
function Video() {
|
function Video() {
|
||||||
@@ -15,8 +32,8 @@ function Video() {
|
|||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
title='Posisi Organisasi'
|
title='Video'
|
||||||
placeholder='pencarian'
|
placeholder='Cari judul atau deskripsi video...'
|
||||||
searchIcon={<IconSearch size={20} />}
|
searchIcon={<IconSearch size={20} />}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
@@ -29,6 +46,7 @@ function Video() {
|
|||||||
function ListVideo({ search }: { search: string }) {
|
function ListVideo({ search }: { search: string }) {
|
||||||
const videoState = useProxy(stateGallery.video)
|
const videoState = useProxy(stateGallery.video)
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
page,
|
page,
|
||||||
@@ -41,72 +59,104 @@ function ListVideo({ search }: { search: string }) {
|
|||||||
load(page, 10, search)
|
load(page, 10, search)
|
||||||
}, [page, search])
|
}, [page, search])
|
||||||
|
|
||||||
const filteredData = (data || [])
|
const filteredData = data || []
|
||||||
|
|
||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton h={500} />
|
<Skeleton height={600} radius="md" />
|
||||||
</Box>
|
</Stack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={10}>
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
|
||||||
<JudulList
|
<Group justify="space-between" mb="md">
|
||||||
title='List Video'
|
<Title order={4}>Daftar Video</Title>
|
||||||
href='/admin/desa/gallery/video/create'
|
<Tooltip label="Tambah Video Baru" withArrow>
|
||||||
/>
|
<Button
|
||||||
<Table striped withTableBorder withRowBorders>
|
leftSection={<IconPlus size={18} />}
|
||||||
|
color="blue"
|
||||||
|
variant="light"
|
||||||
|
onClick={() => router.push('/admin/desa/gallery/video/create')}
|
||||||
|
>
|
||||||
|
Tambah Baru
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
|
<Box style={{ overflowX: "auto" }}>
|
||||||
|
<Table highlightOnHover>
|
||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh>Judul Video</TableTh>
|
<TableTh style={{ width: '25%' }}>Judul Video</TableTh>
|
||||||
<TableTh>Tanggal Video</TableTh>
|
<TableTh style={{ width: '20%' }}>Tanggal</TableTh>
|
||||||
<TableTh>Deskripsi Video</TableTh>
|
<TableTh style={{ width: '30%' }}>Deskripsi</TableTh>
|
||||||
<TableTh>Detail</TableTh>
|
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
</TableThead>
|
</TableThead>
|
||||||
<TableTbody>
|
<TableTbody>
|
||||||
{filteredData.map((item) => (
|
{filteredData.length > 0 ? (
|
||||||
|
filteredData.map((item) => (
|
||||||
<TableTr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
<TableTd>
|
<TableTd style={{ width: '25%' }}>
|
||||||
<Box w={200}>
|
<Box w={200}>
|
||||||
<Text lineClamp={1}>{item.name}</Text>
|
<Text fw={500} truncate="end" lineClamp={1}>{item.name}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
|
<TableTd style={{ width: '20%' }}>
|
||||||
<TableTd>
|
|
||||||
<Box w={200}>
|
<Box w={200}>
|
||||||
|
<Text fz="sm" c="dimmed">
|
||||||
{new Date(item.createdAt).toLocaleDateString('id-ID', {
|
{new Date(item.createdAt).toLocaleDateString('id-ID', {
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
month: 'long',
|
month: 'long',
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
})}
|
})}
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd>
|
<TableTd style={{ width: '30%' }}>
|
||||||
<Box w={200}>
|
<Box w={200}>
|
||||||
<Text lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
<Text fz="sm" truncate="end" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||||
</Box>
|
</Box>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd>
|
<TableTd style={{ width: '15%' }}>
|
||||||
<Button onClick={() => router.push(`/admin/desa/gallery/video/${item.id}`)}>
|
<Button
|
||||||
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
onClick={() => router.push(`/admin/desa/gallery/video/${item.id}`)}
|
||||||
|
>
|
||||||
<IconDeviceImac size={20} />
|
<IconDeviceImac size={20} />
|
||||||
|
<Text ml={5}>Detail</Text>
|
||||||
</Button>
|
</Button>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))}
|
))
|
||||||
|
) : (
|
||||||
|
<TableTr>
|
||||||
|
<TableTd colSpan={4}>
|
||||||
|
<Center py={20}>
|
||||||
|
<Text c="dimmed">Tidak ada video yang cocok</Text>
|
||||||
|
</Center>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
)}
|
||||||
</TableTbody>
|
</TableTbody>
|
||||||
</Table>
|
</Table>
|
||||||
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
<Center>
|
<Center>
|
||||||
<Pagination
|
<Pagination
|
||||||
value={page}
|
value={page}
|
||||||
onChange={(newPage) => load(newPage)} // ini penting!
|
onChange={(newPage) => {
|
||||||
|
load(newPage, 10)
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||||
|
}}
|
||||||
total={totalPages}
|
total={totalPages}
|
||||||
mt="md"
|
mt="md"
|
||||||
mb="md"
|
mb="md"
|
||||||
|
color="blue"
|
||||||
|
radius="md"
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
||||||
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
|
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
|
||||||
import { IconArrowBack } from '@tabler/icons-react';
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
@@ -49,52 +49,74 @@ function EditPelayananPendudukNonPermanent() {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Stack gap={'xs'}>
|
<Stack gap="xs">
|
||||||
<Box>
|
<Group mb="md">
|
||||||
<Button
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
variant={'subtle'}
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
onClick={() => router.back()}
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
>
|
|
||||||
<IconArrowBack color={colors['blue-button']} size={20} />
|
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Tooltip>
|
||||||
<Box>
|
<Title order={4} ml="sm" c="dark">
|
||||||
<Paper bg={colors['white-1']} p={'md'} radius={10} w={{ base: '100%', md: '50%' }}>
|
Edit Pelayanan Penduduk Non Permanent
|
||||||
<Stack gap={'xs'}>
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<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 Pelayanan Penduduk Non Permanent</Title>
|
<Title order={3}>Edit Pelayanan Penduduk Non Permanent</Title>
|
||||||
<Text fw={"bold"}>Judul</Text>
|
|
||||||
|
{/* Nama Field */}
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label="Judul"
|
||||||
|
placeholder="Masukkan judul"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(val) => {
|
onChange={(e) =>
|
||||||
setFormData({
|
setFormData({ ...formData, name: e.target.value })
|
||||||
...formData,
|
}
|
||||||
name: val.target.value,
|
required
|
||||||
})
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Text fw={"bold"}>Deskripsi</Text>
|
|
||||||
<EditEditor
|
|
||||||
value={formData.deskripsi}
|
|
||||||
onChange={(val) => {
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
deskripsi: val,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Posisi Field */}
|
||||||
|
<Box>
|
||||||
|
<Text fz="sm" fw="bold">
|
||||||
|
Deskripsi
|
||||||
|
</Text>
|
||||||
|
<EditEditor
|
||||||
|
value={formData.deskripsi}
|
||||||
|
onChange={(htmlContent) => {
|
||||||
|
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Submit Button */}
|
||||||
<Group>
|
<Group>
|
||||||
<Button
|
<Button
|
||||||
bg={colors['blue-button']}
|
bg={colors['blue-button']}
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
loading={statePendudukNonPermanent.update.loading}
|
loading={statePendudukNonPermanent.update.loading}
|
||||||
|
disabled={!formData.name}
|
||||||
>
|
>
|
||||||
Submit
|
{statePendudukNonPermanent.update.loading ? 'Menyimpan...' : 'Simpan Perubahan'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
disabled={statePendudukNonPermanent.update.loading}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,51 +1,103 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Grid, GridCol, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Divider,
|
||||||
|
Grid,
|
||||||
|
GridCol,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconEdit } from '@tabler/icons-react';
|
import { IconEdit } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import stateLayananDesa from '../../../_state/desa/layananDesa';
|
import stateLayananDesa from '../../../_state/desa/layananDesa';
|
||||||
|
|
||||||
function SuratKeterangan() {
|
function PelayananPendudukNonPermanent() {
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
const pelayananPendudukNonPermanen = useProxy(stateLayananDesa.pelayananPendudukNonPermanen)
|
const pelayananPendudukNonPermanen = useProxy(
|
||||||
|
stateLayananDesa.pelayananPendudukNonPermanen
|
||||||
|
);
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
pelayananPendudukNonPermanen.findById.load('1')
|
pelayananPendudukNonPermanen.findById.load('1');
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
if (!pelayananPendudukNonPermanen.findById.data) {
|
if (!pelayananPendudukNonPermanen.findById.data) {
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack align="center" justify="center" py="xl">
|
||||||
<Skeleton radius={10} h={800} />
|
<Skeleton radius="md" height={800} />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Box py={10}>
|
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
|
||||||
<Paper bg={colors['BG-trans']} p={'md'}>
|
|
||||||
<Box py={15}>
|
|
||||||
<Stack gap={"xs"}>
|
|
||||||
<Grid>
|
|
||||||
<GridCol span={{ base: 12, md: 11 }}>
|
|
||||||
<Text fz={"h4"} fw={"bold"}>Preview Pelayanan Perizinan Berusaha</Text>
|
|
||||||
</GridCol>
|
|
||||||
<GridCol span={{ base: 12, md: 1 }}>
|
|
||||||
<Button bg={colors['blue-button']} onClick={() => router.push('/admin/desa/layanan/pelayanan_penduduk_non_permanent/edit')}>
|
|
||||||
<IconEdit size={16} />
|
|
||||||
</Button>
|
|
||||||
</GridCol>
|
|
||||||
</Grid>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
<Text fz={{ base: "h4", md: 'h2' }} fw={"bold"}>{pelayananPendudukNonPermanen.findById.data.name}</Text>
|
|
||||||
<Text py={10} ta={"justify"} fz={{ base: "sm", md: 'h3' }} dangerouslySetInnerHTML={{ __html: pelayananPendudukNonPermanen.findById.data.deskripsi }} />
|
|
||||||
</Paper>
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SuratKeterangan;
|
const data = pelayananPendudukNonPermanen.findById.data;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
|
||||||
|
<Stack gap="md">
|
||||||
|
{/* Header */}
|
||||||
|
<Grid align="center">
|
||||||
|
<GridCol span={{ base: 12, md: 11 }}>
|
||||||
|
<Title order={3} c={colors['blue-button']}>
|
||||||
|
Preview Pelayanan Penduduk Non Permanen
|
||||||
|
</Title>
|
||||||
|
</GridCol>
|
||||||
|
<GridCol span={{ base: 12, md: 1 }}>
|
||||||
|
<Tooltip label="Edit Data Pelayanan" withArrow>
|
||||||
|
<Button
|
||||||
|
c="green"
|
||||||
|
variant="light"
|
||||||
|
leftSection={<IconEdit size={18} stroke={2} />}
|
||||||
|
radius="md"
|
||||||
|
onClick={() =>
|
||||||
|
router.push(
|
||||||
|
'/admin/desa/layanan/pelayanan_penduduk_non_permanent/edit'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</GridCol>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">
|
||||||
|
<Box px={{ base: 0, md: 50 }} pb="xl">
|
||||||
|
<Center>
|
||||||
|
<Text
|
||||||
|
ta="center"
|
||||||
|
fz={{ base: '1.2rem', md: '1.8rem' }}
|
||||||
|
fw="bold"
|
||||||
|
c={colors['blue-button']}
|
||||||
|
>
|
||||||
|
{data.name}
|
||||||
|
</Text>
|
||||||
|
</Center>
|
||||||
|
|
||||||
|
<Divider my="md" color={colors['blue-button']} />
|
||||||
|
|
||||||
|
<Box mt="lg">
|
||||||
|
<Text
|
||||||
|
py={10}
|
||||||
|
ta="justify"
|
||||||
|
fz={{ base: '1rem', md: '1.2rem' }}
|
||||||
|
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PelayananPendudukNonPermanent;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
||||||
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
|
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import { Box, Button, Group, Paper, Stack, TextInput, Title, Tooltip } from '@mantine/core';
|
||||||
import { IconArrowBack } from '@tabler/icons-react';
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
@@ -14,6 +14,7 @@ function EditPelayananPerizinanBerusaha() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const statePerizinanBerusaha = useProxy(stateLayananDesa.pelayananPerizinanBerusaha)
|
const statePerizinanBerusaha = useProxy(stateLayananDesa.pelayananPerizinanBerusaha)
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: statePerizinanBerusaha.findById.data?.name || '',
|
name: statePerizinanBerusaha.findById.data?.name || '',
|
||||||
deskripsi: statePerizinanBerusaha.findById.data?.deskripsi || '',
|
deskripsi: statePerizinanBerusaha.findById.data?.deskripsi || '',
|
||||||
@@ -50,64 +51,81 @@ function EditPelayananPerizinanBerusaha() {
|
|||||||
}
|
}
|
||||||
router.push('/admin/desa/layanan/pelayanan_perizinan_berusaha')
|
router.push('/admin/desa/layanan/pelayanan_perizinan_berusaha')
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Stack gap={'xs'}>
|
<Stack gap="xs">
|
||||||
<Box>
|
{/* Header Section */}
|
||||||
<Button
|
<Group mb="md">
|
||||||
variant={'subtle'}
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
onClick={() => router.back()}
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
>
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
<IconArrowBack color={colors['blue-button']} size={20} />
|
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Tooltip>
|
||||||
<Box>
|
<Title order={4} ml="sm" c="dark">
|
||||||
<Paper bg={colors['white-1']} p={'md'} radius={10} w={{ base: '100%', md: '50%' }}>
|
Edit Pelayanan Perizinan Berusaha
|
||||||
<Stack gap={'xs'}>
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{/* Form Section */}
|
||||||
|
<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 Pelayanan Perizinan Berusaha</Title>
|
<Title order={3}>Edit Pelayanan Perizinan Berusaha</Title>
|
||||||
<Text fw={"bold"}>Judul</Text>
|
|
||||||
|
{/* Nama Field */}
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label="Judul"
|
||||||
|
placeholder="Masukkan judul"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(val) => {
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
setFormData({
|
required
|
||||||
...formData,
|
|
||||||
name: val.target.value,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Text fw={"bold"}>Link</Text>
|
|
||||||
<TextInput
|
|
||||||
value={formData.link}
|
|
||||||
onChange={(val) => {
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
link: val.target.value,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Text fw={"bold"}>Deskripsi</Text>
|
|
||||||
<EditEditor
|
|
||||||
value={formData.deskripsi}
|
|
||||||
onChange={(val) => {
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
deskripsi: val,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Link Field */}
|
||||||
|
<TextInput
|
||||||
|
label="Link"
|
||||||
|
placeholder="Masukkan link terkait"
|
||||||
|
value={formData.link}
|
||||||
|
onChange={(e) => setFormData({ ...formData, link: e.target.value })}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Deskripsi Field */}
|
||||||
|
<Box>
|
||||||
|
<Title order={6}>Deskripsi</Title>
|
||||||
|
<EditEditor
|
||||||
|
value={formData.deskripsi}
|
||||||
|
onChange={(val) => setFormData({ ...formData, deskripsi: val })}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
<Group>
|
<Group>
|
||||||
<Button
|
<Button
|
||||||
bg={colors['blue-button']}
|
bg={colors['blue-button']}
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
loading={statePerizinanBerusaha.update.loading}
|
loading={statePerizinanBerusaha.update.loading}
|
||||||
|
disabled={!formData.name}
|
||||||
>
|
>
|
||||||
Submit
|
{statePerizinanBerusaha.update.loading ? 'Menyimpan...' : 'Simpan Perubahan'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
disabled={statePerizinanBerusaha.update.loading}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,23 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Grid, GridCol, Group, Paper, Skeleton, Stack, Stepper, StepperCompleted, StepperStep, Text } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Divider,
|
||||||
|
Grid,
|
||||||
|
GridCol,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Stepper,
|
||||||
|
StepperCompleted,
|
||||||
|
StepperStep,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
import { IconEdit } from '@tabler/icons-react';
|
import { IconEdit } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
@@ -9,54 +26,103 @@ import { useProxy } from 'valtio/utils';
|
|||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
|
|
||||||
function PerizinanBerusaha() {
|
function PerizinanBerusaha() {
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
const pelayananPerizinanBerusaha = useProxy(stateLayananDesa.pelayananPerizinanBerusaha)
|
const pelayananPerizinanBerusaha = useProxy(
|
||||||
|
stateLayananDesa.pelayananPerizinanBerusaha
|
||||||
|
);
|
||||||
const [active, setActive] = useState(1);
|
const [active, setActive] = useState(1);
|
||||||
const nextStep = () => setActive((current) => (current < 6 ? current + 1 : current));
|
const nextStep = () =>
|
||||||
const prevStep = () => setActive((current) => (current > 0 ? current - 1 : current));
|
setActive((current) => (current < 6 ? current + 1 : current));
|
||||||
|
const prevStep = () =>
|
||||||
|
setActive((current) => (current > 0 ? current - 1 : current));
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
pelayananPerizinanBerusaha.findById.load('1')
|
pelayananPerizinanBerusaha.findById.load('1');
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
if (!pelayananPerizinanBerusaha.findById.data) {
|
if (!pelayananPerizinanBerusaha.findById.data) {
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack align="center" justify="center" py="xl">
|
||||||
<Skeleton radius={10} h={800} />
|
<Skeleton radius="md" height={800} />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = pelayananPerizinanBerusaha.findById.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
<Stack gap="md">
|
||||||
<Paper bg={colors['BG-trans']} p={'md'}>
|
{/* Header */}
|
||||||
<Box py={15}>
|
<Grid align="center">
|
||||||
<Stack gap={"xs"}>
|
|
||||||
<Grid>
|
|
||||||
<GridCol span={{ base: 12, md: 11 }}>
|
<GridCol span={{ base: 12, md: 11 }}>
|
||||||
<Text fz={"h4"} fw={"bold"}>Preview Pelayanan Perizinan Berusaha</Text>
|
<Title order={3} c={colors['blue-button']}>
|
||||||
|
Preview Pelayanan Perizinan Berusaha
|
||||||
|
</Title>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
<GridCol span={{ base: 12, md: 1 }}>
|
<GridCol span={{ base: 12, md: 1 }}>
|
||||||
<Button bg={colors['blue-button']} onClick={() => router.push('/admin/desa/layanan/pelayanan_perizinan_berusaha/edit')}>
|
<Tooltip label="Edit Data Perizinan" withArrow>
|
||||||
<IconEdit size={16} />
|
<Button
|
||||||
|
c="green"
|
||||||
|
variant="light"
|
||||||
|
leftSection={<IconEdit size={18} stroke={2} />}
|
||||||
|
radius="md"
|
||||||
|
onClick={() =>
|
||||||
|
router.push(
|
||||||
|
'/admin/desa/layanan/pelayanan_perizinan_berusaha/edit'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
<Text fz={{ base: "h4", md: 'h2' }} fw={"bold"}>{pelayananPerizinanBerusaha.findById.data.name}</Text>
|
|
||||||
<Text py={10} ta={"justify"} fz={{ base: "sm", md: 'h3' }} dangerouslySetInnerHTML={{__html: pelayananPerizinanBerusaha.findById.data.deskripsi}} />
|
|
||||||
<Text py={10} fz={{ base: "sm", md: 'h3' }}>Proses pendaftaran NIB melalui OSS mencakup beberapa langkah umum, seperti:</Text>
|
|
||||||
<Box p={"xl"} w={{ base: "100%", md: "100%" }} >
|
|
||||||
<Stepper active={active} onStepClick={setActive} orientation="vertical"
|
|
||||||
styles={{
|
|
||||||
separator: {
|
|
||||||
marginLeft: 25
|
|
||||||
},
|
|
||||||
|
|
||||||
step: {
|
{/* Content */}
|
||||||
padding: '12px 0'
|
<Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">
|
||||||
}
|
<Box px={{ base: 0, md: 50 }} pb="xl">
|
||||||
}}>
|
<Center>
|
||||||
|
<Text
|
||||||
|
ta="center"
|
||||||
|
fz={{ base: '1.2rem', md: '1.8rem' }}
|
||||||
|
fw="bold"
|
||||||
|
c={colors['blue-button']}
|
||||||
|
>
|
||||||
|
{data.name}
|
||||||
|
</Text>
|
||||||
|
</Center>
|
||||||
|
|
||||||
|
<Divider my="md" color={colors['blue-button']} />
|
||||||
|
|
||||||
|
<Box mt="lg">
|
||||||
|
<Text
|
||||||
|
py={10}
|
||||||
|
ta="justify"
|
||||||
|
fz={{ base: '1rem', md: '1.2rem' }}
|
||||||
|
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text
|
||||||
|
py={10}
|
||||||
|
fz={{ base: '1rem', md: '1.2rem' }}
|
||||||
|
fw="bold"
|
||||||
|
c={colors['blue-button']}
|
||||||
|
>
|
||||||
|
Proses pendaftaran NIB melalui OSS mencakup beberapa langkah
|
||||||
|
umum:
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Box p="xl" w="100%">
|
||||||
|
<Stepper
|
||||||
|
active={active}
|
||||||
|
onStepClick={setActive}
|
||||||
|
orientation="vertical"
|
||||||
|
styles={{
|
||||||
|
separator: { marginLeft: 25 },
|
||||||
|
step: { padding: '12px 0' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
<StepperStep label="Langkah Pertama" description="Pendaftaran Akun">
|
<StepperStep label="Langkah Pertama" description="Pendaftaran Akun">
|
||||||
Pendaftaran akun pada portal OSS
|
Pendaftaran akun pada portal OSS
|
||||||
</StepperStep>
|
</StepperStep>
|
||||||
@@ -81,16 +147,37 @@ function PerizinanBerusaha() {
|
|||||||
</Stepper>
|
</Stepper>
|
||||||
|
|
||||||
<Group justify="center" mt="xl">
|
<Group justify="center" mt="xl">
|
||||||
<Button variant="default" onClick={prevStep}>Back</Button>
|
<Button variant="default" onClick={prevStep}>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
<Button onClick={nextStep}>Next step</Button>
|
<Button onClick={nextStep}>Next step</Button>
|
||||||
</Group>
|
</Group>
|
||||||
<Text py={35} ta={"justify"} fz={{ base: "sm", md: 'h3' }}>Penting untuk diingat bahwa prosedur dan persyaratan dapat berubah
|
</Box>
|
||||||
seiring waktu. Untuk informasi yang lebih akurat dan terkini, saya sarankan untuk mengunjungi situs
|
|
||||||
resmi OSS <a href={pelayananPerizinanBerusaha.findById.data.link}>{pelayananPerizinanBerusaha.findById.data.link}</a> atau menghubungi instansi terkait di pemerintah Indonesia yang bertanggung jawab atas urusan perizinan usaha.</Text>
|
<Text
|
||||||
|
py={35}
|
||||||
|
ta="justify"
|
||||||
|
fz={{ base: '1rem', md: '1.2rem' }}
|
||||||
|
>
|
||||||
|
Penting untuk diingat bahwa prosedur dan persyaratan dapat
|
||||||
|
berubah seiring waktu. Untuk informasi yang lebih akurat dan
|
||||||
|
terkini, silakan kunjungi situs resmi OSS{' '}
|
||||||
|
<a
|
||||||
|
href={data.link}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
style={{ color: colors['blue-button'] }}
|
||||||
|
>
|
||||||
|
{data.link}
|
||||||
|
</a>{' '}
|
||||||
|
atau hubungi instansi terkait di pemerintah Indonesia yang
|
||||||
|
bertanggung jawab atas urusan perizinan usaha.
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,18 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
|||||||
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
|
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import ApiFetch from '@/lib/api-fetch';
|
import ApiFetch from '@/lib/api-fetch';
|
||||||
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Image,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
import { Dropzone } from '@mantine/dropzone';
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
@@ -13,9 +24,10 @@ import { toast } from 'react-toastify';
|
|||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
function EditSuratKeterangan() {
|
function EditSuratKeterangan() {
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
const params = useParams()
|
const params = useParams();
|
||||||
const stateSurat = useProxy(stateLayananDesa.suratKeterangan)
|
const stateSurat = useProxy(stateLayananDesa.suratKeterangan);
|
||||||
|
|
||||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
const [previewImage2, setPreviewImage2] = useState<string | null>(null);
|
const [previewImage2, setPreviewImage2] = useState<string | null>(null);
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
@@ -25,39 +37,32 @@ function EditSuratKeterangan() {
|
|||||||
deskripsi: stateSurat.edit.form.deskripsi,
|
deskripsi: stateSurat.edit.form.deskripsi,
|
||||||
imageId: stateSurat.edit.form.imageId,
|
imageId: stateSurat.edit.form.imageId,
|
||||||
image2Id: stateSurat.edit.form.image2Id,
|
image2Id: stateSurat.edit.form.image2Id,
|
||||||
})
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadSurat = async () => {
|
const loadSurat = async () => {
|
||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await stateSurat.edit.load(id);
|
const data = await stateSurat.edit.load(id);
|
||||||
if (data) {
|
if (data) {
|
||||||
setFormData({
|
setFormData({
|
||||||
name: data.name || "",
|
name: data.name || '',
|
||||||
deskripsi: data.deskripsi || "",
|
deskripsi: data.deskripsi || '',
|
||||||
imageId: data.imageId || "",
|
imageId: data.imageId || '',
|
||||||
image2Id: data.image2Id || "",
|
image2Id: data.image2Id || '',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data.image?.link) {
|
setPreviewImage(data.image?.link || null);
|
||||||
setPreviewImage(data.image.link);
|
setPreviewImage2(data.image2?.link || null);
|
||||||
} else {
|
|
||||||
setPreviewImage(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.image2?.link) {
|
|
||||||
setPreviewImage2(data.image2.link);
|
|
||||||
} else {
|
|
||||||
setPreviewImage2(null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading surat:", error);
|
console.error('Error loading surat:', error);
|
||||||
toast.error("Gagal memuat data surat");
|
toast.error('Gagal memuat data surat');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadSurat();
|
loadSurat();
|
||||||
}, [params?.id]);
|
}, [params?.id]);
|
||||||
|
|
||||||
@@ -65,171 +70,199 @@ function EditSuratKeterangan() {
|
|||||||
try {
|
try {
|
||||||
stateSurat.edit.form = {
|
stateSurat.edit.form = {
|
||||||
...stateSurat.edit.form,
|
...stateSurat.edit.form,
|
||||||
name: formData.name,
|
...formData,
|
||||||
deskripsi: formData.deskripsi,
|
};
|
||||||
imageId: formData.imageId,
|
|
||||||
image2Id: formData.image2Id,
|
|
||||||
}
|
|
||||||
if (file) {
|
if (file) {
|
||||||
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
|
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
|
||||||
const uploaded = res.data?.data;
|
const uploaded = res.data?.data;
|
||||||
|
if (!uploaded?.id) return toast.error('Gagal upload gambar');
|
||||||
if (!uploaded?.id) {
|
|
||||||
return toast.error("Gagal upload gambar");
|
|
||||||
}
|
|
||||||
|
|
||||||
stateSurat.edit.form.imageId = uploaded.id;
|
stateSurat.edit.form.imageId = uploaded.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file2) {
|
if (file2) {
|
||||||
const res = await ApiFetch.api.fileStorage.create.post({ file: file2, name: file2.name });
|
const res = await ApiFetch.api.fileStorage.create.post({ file: file2, name: file2.name });
|
||||||
const uploaded = res.data?.data;
|
const uploaded = res.data?.data;
|
||||||
|
if (!uploaded?.id) return toast.error('Gagal upload gambar');
|
||||||
if (!uploaded?.id) {
|
|
||||||
return toast.error("Gagal upload gambar");
|
|
||||||
}
|
|
||||||
|
|
||||||
stateSurat.edit.form.image2Id = uploaded.id;
|
stateSurat.edit.form.image2Id = uploaded.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
await stateSurat.edit.update()
|
await stateSurat.edit.update();
|
||||||
toast.success("Surat berhasil diperbarui!")
|
toast.success('Surat berhasil diperbarui!');
|
||||||
router.push("/admin/desa/layanan/pelayanan_surat_keterangan")
|
router.push('/admin/desa/layanan/pelayanan_surat_keterangan');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating surat:", error);
|
console.error('Error updating surat:', error);
|
||||||
toast.error("Terjadi kesalahan saat memperbarui surat");
|
toast.error('Terjadi kesalahan saat memperbarui surat');
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Back Button */}
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Tooltip>
|
||||||
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
|
<Title order={4} ml="sm" c="dark">
|
||||||
<Stack gap={"xs"}>
|
Edit Surat Keterangan
|
||||||
<Title order={3}>Edit Surat Keterangan</Title>
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Paper
|
||||||
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label="Nama Surat Keterangan"
|
||||||
|
placeholder="Masukkan nama surat keterangan"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(val) => {
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
setFormData({ ...formData, name: val.target.value });
|
required
|
||||||
}}
|
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Nama Surat Keterangan</Text>}
|
|
||||||
placeholder="masukkan nama surat keterangan"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"sm"} fw={"bold"}>Konten</Text>
|
<Text fz="sm" fw="bold" mb={6}>
|
||||||
|
Konten
|
||||||
|
</Text>
|
||||||
<EditEditor
|
<EditEditor
|
||||||
value={formData.deskripsi}
|
value={formData.deskripsi}
|
||||||
onChange={(htmlContent) => {
|
onChange={(htmlContent) => setFormData({ ...formData, deskripsi: htmlContent })}
|
||||||
setFormData({ ...formData, deskripsi: htmlContent });
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Upload Gambar 1 */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
<Box >
|
Gambar 1
|
||||||
|
</Text>
|
||||||
<Dropzone
|
<Dropzone
|
||||||
onDrop={(files) => {
|
onDrop={(files) => {
|
||||||
const file = files[0]; // Hanya ambil file pertama
|
const selectedFile = files[0];
|
||||||
if (file) {
|
if (selectedFile) {
|
||||||
setFile(file);
|
setFile(selectedFile);
|
||||||
setPreviewImage(URL.createObjectURL(file)); // Buat preview
|
setPreviewImage(URL.createObjectURL(selectedFile));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
maxSize={5 * 1024 ** 2} // 5MB
|
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
||||||
accept={{
|
maxSize={5 * 1024 ** 2}
|
||||||
'image/*': ['.jpeg', '.jpg', '.png', '.webp']
|
accept={{ 'image/*': [] }}
|
||||||
}}
|
radius="md"
|
||||||
|
p="xl"
|
||||||
>
|
>
|
||||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
<Group justify="center" gap="xl" mih={180}>
|
||||||
<Dropzone.Accept>
|
<Dropzone.Accept>
|
||||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
|
||||||
</Dropzone.Accept>
|
</Dropzone.Accept>
|
||||||
<Dropzone.Reject>
|
<Dropzone.Reject>
|
||||||
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
<IconX size={48} color="red" stroke={1.5} />
|
||||||
</Dropzone.Reject>
|
</Dropzone.Reject>
|
||||||
<Dropzone.Idle>
|
<Dropzone.Idle>
|
||||||
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
<IconPhoto size={48} color="#868e96" stroke={1.5} />
|
||||||
</Dropzone.Idle>
|
</Dropzone.Idle>
|
||||||
|
<Stack gap="xs" align="center">
|
||||||
<div>
|
<Text size="md" fw={500}>
|
||||||
<Text size="xl" inline>
|
Seret gambar atau klik untuk memilih file
|
||||||
Drag images here or click to select files
|
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="sm" c="dimmed" inline mt={7}>
|
<Text size="sm" c="dimmed">
|
||||||
Attach as many files as you like, each file should not exceed 5mb
|
Maksimal 5MB, format gambar wajib
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</Stack>
|
||||||
</Group>
|
</Group>
|
||||||
</Dropzone>
|
</Dropzone>
|
||||||
|
|
||||||
{previewImage && (
|
{previewImage && (
|
||||||
|
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
|
||||||
<Image
|
<Image
|
||||||
src={previewImage}
|
src={previewImage}
|
||||||
alt="Preview"
|
alt="Preview Gambar 1"
|
||||||
width={280}
|
radius="md"
|
||||||
height={180}
|
style={{
|
||||||
fit="cover"
|
maxHeight: 220,
|
||||||
radius="sm"
|
objectFit: 'contain',
|
||||||
mt="md"
|
border: `1px solid ${colors['blue-button']}`,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
|
||||||
<Box>
|
{/* Upload Gambar 2 */}
|
||||||
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
|
||||||
<Box>
|
<Box>
|
||||||
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
|
Gambar 2
|
||||||
|
</Text>
|
||||||
<Dropzone
|
<Dropzone
|
||||||
onDrop={(files) => {
|
onDrop={(files) => {
|
||||||
const file = files[0]; // Hanya ambil file pertama
|
const selectedFile = files[0];
|
||||||
if (file) {
|
if (selectedFile) {
|
||||||
setFile2(file);
|
setFile2(selectedFile);
|
||||||
setPreviewImage2(URL.createObjectURL(file)); // Buat preview
|
setPreviewImage2(URL.createObjectURL(selectedFile));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
maxSize={5 * 1024 ** 2} // 5MB
|
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
||||||
accept={{
|
maxSize={5 * 1024 ** 2}
|
||||||
'image/*': ['.jpeg', '.jpg', '.png', '.webp']
|
accept={{ 'image/*': [] }}
|
||||||
}}
|
radius="md"
|
||||||
|
p="xl"
|
||||||
>
|
>
|
||||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
<Group justify="center" gap="xl" mih={180}>
|
||||||
<Dropzone.Accept>
|
<Dropzone.Accept>
|
||||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
|
||||||
</Dropzone.Accept>
|
</Dropzone.Accept>
|
||||||
<Dropzone.Reject>
|
<Dropzone.Reject>
|
||||||
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
<IconX size={48} color="red" stroke={1.5} />
|
||||||
</Dropzone.Reject>
|
</Dropzone.Reject>
|
||||||
<Dropzone.Idle>
|
<Dropzone.Idle>
|
||||||
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
<IconPhoto size={48} color="#868e96" stroke={1.5} />
|
||||||
</Dropzone.Idle>
|
</Dropzone.Idle>
|
||||||
|
<Stack gap="xs" align="center">
|
||||||
<div>
|
<Text size="md" fw={500}>
|
||||||
<Text size="xl" inline>
|
Seret gambar atau klik untuk memilih file
|
||||||
Drag images here or click to select files
|
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="sm" c="dimmed" inline mt={7}>
|
<Text size="sm" c="dimmed">
|
||||||
Attach as many files as you like, each file should not exceed 5mb
|
Maksimal 5MB, format gambar wajib
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</Stack>
|
||||||
</Group>
|
</Group>
|
||||||
</Dropzone>
|
</Dropzone>
|
||||||
|
|
||||||
{previewImage2 && (
|
{previewImage2 && (
|
||||||
|
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
|
||||||
<Image
|
<Image
|
||||||
src={previewImage2}
|
src={previewImage2}
|
||||||
alt="Preview"
|
alt="Preview Gambar 2"
|
||||||
width={280}
|
radius="md"
|
||||||
height={180}
|
style={{
|
||||||
fit="cover"
|
maxHeight: 220,
|
||||||
radius="sm"
|
objectFit: 'contain',
|
||||||
mt="md"
|
border: `1px solid ${colors['blue-button']}`,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
|
||||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</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>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -2,100 +2,177 @@
|
|||||||
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||||
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
|
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Image,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
|
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
function DetailSuratKeterangan() {
|
function DetailSuratKeterangan() {
|
||||||
const suratKeteranganState = useProxy(stateLayananDesa.suratKeterangan)
|
const suratKeteranganState = useProxy(stateLayananDesa.suratKeterangan);
|
||||||
const [modalHapus, setModalHapus] = useState(false)
|
const [modalHapus, setModalHapus] = useState(false);
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
const params = useParams()
|
const params = useParams();
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
suratKeteranganState.findUnique.load(params?.id as string)
|
suratKeteranganState.findUnique.load(params?.id as string);
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
const handleHapus = () => {
|
const handleHapus = () => {
|
||||||
if (selectedId) {
|
if (selectedId) {
|
||||||
suratKeteranganState.delete.byId(selectedId)
|
suratKeteranganState.delete.byId(selectedId);
|
||||||
setModalHapus(false)
|
setModalHapus(false);
|
||||||
setSelectedId(null)
|
setSelectedId(null);
|
||||||
router.push("/admin/desa/layanan/pelayanan_surat_keterangan")
|
router.push('/admin/desa/layanan/pelayanan_surat_keterangan');
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (!suratKeteranganState.findUnique.data) {
|
if (!suratKeteranganState.findUnique.data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
{Array.from({ length: 10 }).map((_, k) => (
|
<Skeleton height={500} radius="md" />
|
||||||
<Skeleton key={k} h={40} />
|
|
||||||
))}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = suratKeteranganState.findUnique.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box py={10}>
|
||||||
<Box mb={10}>
|
{/* Tombol Kembali */}
|
||||||
<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 Surat Keterangan</Text>
|
|
||||||
{suratKeteranganState.findUnique.data ? (
|
|
||||||
<Paper key={suratKeteranganState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
|
||||||
<Stack gap={"xs"}>
|
|
||||||
<Box>
|
|
||||||
<Text fw={"bold"} fz={"lg"}>Nama</Text>
|
|
||||||
<Text fz={"lg"}>{suratKeteranganState.findUnique.data?.name}</Text>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
|
|
||||||
<Text fz={"lg"}dangerouslySetInnerHTML={{ __html: suratKeteranganState.findUnique.data?.deskripsi }}></Text>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
|
|
||||||
<Image w={{ base: 150, md: 150, lg: 150 }} src={suratKeteranganState.findUnique.data?.image?.link} alt="gambar" />
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
|
|
||||||
<Image w={{ base: 150, md: 150, lg: 150 }} src={suratKeteranganState.findUnique.data?.image2?.link} alt="gambar" />
|
|
||||||
</Box>
|
|
||||||
<Flex gap={"xs"} mt={10}>
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
variant="subtle"
|
||||||
if (suratKeteranganState.findUnique.data) {
|
onClick={() => router.back()}
|
||||||
setSelectedId(suratKeteranganState.findUnique.data.id);
|
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
|
||||||
setModalHapus(true);
|
mb={15}
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={suratKeteranganState.delete.loading || !suratKeteranganState.findUnique.data}
|
|
||||||
color={"red"}
|
|
||||||
>
|
>
|
||||||
<IconX size={20} />
|
Kembali
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
|
||||||
onClick={() => {
|
<Paper
|
||||||
if (suratKeteranganState.findUnique.data) {
|
withBorder
|
||||||
router.push(`/admin/desa/layanan/pelayanan_surat_keterangan/${suratKeteranganState.findUnique.data.id}/edit`);
|
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 Surat Keterangan
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">
|
||||||
|
Nama
|
||||||
|
</Text>
|
||||||
|
<Text fz="md" c="dimmed">
|
||||||
|
{data?.name || '-'}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">
|
||||||
|
Deskripsi
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
fz="md"
|
||||||
|
c="dimmed"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: data?.deskripsi || '-',
|
||||||
}}
|
}}
|
||||||
disabled={!suratKeteranganState.findUnique.data}
|
/>
|
||||||
color={"green"}
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">
|
||||||
|
Gambar
|
||||||
|
</Text>
|
||||||
|
{data?.image?.link ? (
|
||||||
|
<Image
|
||||||
|
src={data.image.link}
|
||||||
|
alt="gambar"
|
||||||
|
w={200}
|
||||||
|
h={200}
|
||||||
|
radius="md"
|
||||||
|
fit="cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Text fz="sm" c="dimmed">
|
||||||
|
Tidak ada gambar
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">
|
||||||
|
Gambar 2
|
||||||
|
</Text>
|
||||||
|
{data?.image2?.link ? (
|
||||||
|
<Image
|
||||||
|
src={data.image2.link}
|
||||||
|
alt="gambar"
|
||||||
|
w={200}
|
||||||
|
h={200}
|
||||||
|
radius="md"
|
||||||
|
fit="cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Text fz="sm" c="dimmed">
|
||||||
|
Tidak ada gambar
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Group gap="sm">
|
||||||
|
<Tooltip label="Hapus Surat" withArrow position="top">
|
||||||
|
<Button
|
||||||
|
color="red"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedId(data.id);
|
||||||
|
setModalHapus(true);
|
||||||
|
}}
|
||||||
|
variant="light"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
disabled={suratKeteranganState.delete.loading}
|
||||||
|
>
|
||||||
|
<IconTrash size={20} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip label="Edit Surat" withArrow position="top">
|
||||||
|
<Button
|
||||||
|
color="green"
|
||||||
|
onClick={() =>
|
||||||
|
router.push(
|
||||||
|
`/admin/desa/layanan/pelayanan_surat_keterangan/${data.id}/edit`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
variant="light"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
>
|
>
|
||||||
<IconEdit size={20} />
|
<IconEdit size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
) : null}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
@@ -104,7 +181,7 @@ function DetailSuratKeterangan() {
|
|||||||
opened={modalHapus}
|
opened={modalHapus}
|
||||||
onClose={() => setModalHapus(false)}
|
onClose={() => setModalHapus(false)}
|
||||||
onConfirm={handleHapus}
|
onConfirm={handleHapus}
|
||||||
text='Apakah anda yakin ingin menghapus berita ini?'
|
text="Apakah Anda yakin ingin menghapus surat keterangan ini?"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,21 @@
|
|||||||
'use client'
|
'use client';
|
||||||
|
|
||||||
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
||||||
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
|
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import ApiFetch from '@/lib/api-fetch';
|
import ApiFetch from '@/lib/api-fetch';
|
||||||
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Image,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
import { Dropzone } from '@mantine/dropzone';
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
@@ -12,25 +24,25 @@ import { toast } from 'react-toastify';
|
|||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
function CreateSuratKeterangan() {
|
function CreateSuratKeterangan() {
|
||||||
const stateSurat = useProxy(stateLayananDesa.suratKeterangan)
|
const stateSurat = useProxy(stateLayananDesa.suratKeterangan);
|
||||||
const [previewImage2, setPreviewImage2] = useState<{ preview: string; file: File } | null>(null);
|
const [previewImage2, setPreviewImage2] = useState<{ preview: string; file: File } | null>(null);
|
||||||
const [previewImage, setPreviewImage] = useState<{ preview: string; file: File } | null>(null);
|
const [previewImage, setPreviewImage] = useState<{ preview: string; file: File } | null>(null);
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
stateSurat.create.form = {
|
stateSurat.create.form = {
|
||||||
name: "",
|
name: '',
|
||||||
deskripsi: "",
|
deskripsi: '',
|
||||||
imageId: "",
|
imageId: '',
|
||||||
image2Id: ""
|
image2Id: '',
|
||||||
}
|
};
|
||||||
setPreviewImage(null)
|
setPreviewImage(null);
|
||||||
setPreviewImage2(null)
|
setPreviewImage2(null);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!previewImage) {
|
if (!previewImage) {
|
||||||
return toast.warn("Pilih file gambar utama terlebih dahulu");
|
return toast.warn('Pilih file gambar utama terlebih dahulu');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -42,11 +54,10 @@ function CreateSuratKeterangan() {
|
|||||||
|
|
||||||
const uploadedImage1 = res1.data?.data;
|
const uploadedImage1 = res1.data?.data;
|
||||||
if (!uploadedImage1?.id) {
|
if (!uploadedImage1?.id) {
|
||||||
return toast.error("Gagal upload gambar utama");
|
return toast.error('Gagal upload gambar utama');
|
||||||
}
|
}
|
||||||
|
|
||||||
let uploadedImage2 = null;
|
let uploadedImage2 = null;
|
||||||
// Upload gambar kedua jika ada
|
|
||||||
if (previewImage2) {
|
if (previewImage2) {
|
||||||
const res2 = await ApiFetch.api.fileStorage.create.post({
|
const res2 = await ApiFetch.api.fileStorage.create.post({
|
||||||
file: previewImage2.file,
|
file: previewImage2.file,
|
||||||
@@ -55,44 +66,58 @@ function CreateSuratKeterangan() {
|
|||||||
uploadedImage2 = res2.data?.data;
|
uploadedImage2 = res2.data?.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set form data
|
|
||||||
stateSurat.create.form.imageId = uploadedImage1.id;
|
stateSurat.create.form.imageId = uploadedImage1.id;
|
||||||
if (uploadedImage2?.id) {
|
if (uploadedImage2?.id) {
|
||||||
stateSurat.create.form.image2Id = uploadedImage2.id;
|
stateSurat.create.form.image2Id = uploadedImage2.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the record
|
|
||||||
await stateSurat.create.create();
|
await stateSurat.create.create();
|
||||||
|
|
||||||
// Reset form dan redirect
|
|
||||||
resetForm();
|
resetForm();
|
||||||
toast.success("Data surat keterangan berhasil ditambahkan");
|
toast.success('Data surat keterangan berhasil ditambahkan');
|
||||||
router.push("/admin/desa/layanan/pelayanan_surat_keterangan");
|
router.push('/admin/desa/layanan/pelayanan_surat_keterangan');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating surat keterangan:", error);
|
console.error('Error creating surat keterangan:', error);
|
||||||
toast.error("Terjadi kesalahan saat menambahkan surat keterangan");
|
toast.error('Terjadi kesalahan saat menambahkan surat keterangan');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Header */}
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Tooltip>
|
||||||
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
|
<Title order={4} ml="sm" c="dark">
|
||||||
<Stack gap={"xs"}>
|
Tambah Surat Keterangan
|
||||||
<Title order={3}>Create Surat Keterangan</Title>
|
</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">
|
||||||
|
{/* Nama Surat */}
|
||||||
<TextInput
|
<TextInput
|
||||||
value={stateSurat.create.form.name}
|
value={stateSurat.create.form.name}
|
||||||
onChange={(val) => {
|
onChange={(val) => (stateSurat.create.form.name = val.target.value)}
|
||||||
stateSurat.create.form.name = val.target.value;
|
label={<Text fz="sm" fw="bold">Nama Surat Keterangan</Text>}
|
||||||
}}
|
placeholder="Masukkan nama surat keterangan"
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Nama Surat Keterangan</Text>}
|
required
|
||||||
placeholder="masukkan nama surat keterangan"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Konten */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"sm"} fw={"bold"}>Konten</Text>
|
<Text fz="sm" fw="bold" mb={6}>
|
||||||
|
Konten
|
||||||
|
</Text>
|
||||||
<CreateEditor
|
<CreateEditor
|
||||||
value={stateSurat.create.form.deskripsi}
|
value={stateSurat.create.form.deskripsi}
|
||||||
onChange={(htmlContent) => {
|
onChange={(htmlContent) => {
|
||||||
@@ -100,106 +125,124 @@ function CreateSuratKeterangan() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Gambar Utama */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"md"} fw={"bold"} mb="sm">Gambar Utama</Text>
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
|
Gambar Utama
|
||||||
|
</Text>
|
||||||
<Dropzone
|
<Dropzone
|
||||||
onDrop={(files) => {
|
onDrop={(files) => {
|
||||||
const file = files[0];
|
const file = files[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
setPreviewImage({
|
setPreviewImage({
|
||||||
file,
|
file,
|
||||||
preview: URL.createObjectURL(file)
|
preview: URL.createObjectURL(file),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
||||||
maxSize={5 * 1024 ** 2}
|
maxSize={5 * 1024 ** 2}
|
||||||
accept={{
|
accept={{ 'image/*': [] }}
|
||||||
'image/*': ['.jpeg', '.jpg', '.png', '.webp']
|
radius="md"
|
||||||
}}
|
p="xl"
|
||||||
>
|
>
|
||||||
<Group justify="center" gap="xl" mih={120} style={{ pointerEvents: 'none' }}>
|
<Group justify="center" gap="xl" mih={180}>
|
||||||
<Dropzone.Accept>
|
<Dropzone.Accept>
|
||||||
<IconUpload size={32} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
<IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||||
</Dropzone.Accept>
|
</Dropzone.Accept>
|
||||||
<Dropzone.Reject>
|
<Dropzone.Reject>
|
||||||
<IconX size={32} color="var(--mantine-color-red-6)" stroke={1.5} />
|
<IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||||
</Dropzone.Reject>
|
</Dropzone.Reject>
|
||||||
<Dropzone.Idle>
|
<Dropzone.Idle>
|
||||||
<IconPhoto size={32} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
<IconPhoto size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||||
</Dropzone.Idle>
|
</Dropzone.Idle>
|
||||||
<div>
|
|
||||||
<Text size="md" inline>Seret gambar ke sini atau klik untuk memilih</Text>
|
|
||||||
<Text size="sm" c="dimmed" inline mt={7} display="block">
|
|
||||||
Ukuran maksimal 5MB (JPEG, JPG, PNG, WebP)
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</Group>
|
</Group>
|
||||||
|
<Text ta="center" mt="sm" size="sm" color="dimmed">
|
||||||
|
Seret gambar atau klik untuk memilih file (maks 5MB)
|
||||||
|
</Text>
|
||||||
</Dropzone>
|
</Dropzone>
|
||||||
|
|
||||||
{previewImage && (
|
{previewImage && (
|
||||||
|
<Box mt="sm" style={{ textAlign: 'center' }}>
|
||||||
<Image
|
<Image
|
||||||
src={previewImage.preview}
|
src={previewImage.preview}
|
||||||
alt="Preview Gambar Utama"
|
alt="Preview Gambar Utama"
|
||||||
width={280}
|
radius="md"
|
||||||
height={180}
|
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
|
||||||
fit="cover"
|
|
||||||
radius="sm"
|
|
||||||
mt="md"
|
|
||||||
/>
|
/>
|
||||||
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box mt="lg">
|
{/* Gambar Tambahan */}
|
||||||
<Text fz={"md"} fw={"bold"} mb="sm">Gambar Tambahan (Opsional)</Text>
|
<Box>
|
||||||
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
|
Gambar Tambahan (Opsional)
|
||||||
|
</Text>
|
||||||
<Dropzone
|
<Dropzone
|
||||||
onDrop={(files) => {
|
onDrop={(files) => {
|
||||||
const file = files[0];
|
const file = files[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
setPreviewImage2({
|
setPreviewImage2({
|
||||||
file,
|
file,
|
||||||
preview: URL.createObjectURL(file)
|
preview: URL.createObjectURL(file),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
||||||
maxSize={5 * 1024 ** 2}
|
maxSize={5 * 1024 ** 2}
|
||||||
accept={{
|
accept={{ 'image/*': [] }}
|
||||||
'image/*': ['.jpeg', '.jpg', '.png', '.webp']
|
radius="md"
|
||||||
}}
|
p="xl"
|
||||||
>
|
>
|
||||||
<Group justify="center" gap="xl" mih={120} style={{ pointerEvents: 'none' }}>
|
<Group justify="center" gap="xl" mih={180}>
|
||||||
<Dropzone.Accept>
|
<Dropzone.Accept>
|
||||||
<IconUpload size={32} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
<IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||||
</Dropzone.Accept>
|
</Dropzone.Accept>
|
||||||
<Dropzone.Reject>
|
<Dropzone.Reject>
|
||||||
<IconX size={32} color="var(--mantine-color-red-6)" stroke={1.5} />
|
<IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||||
</Dropzone.Reject>
|
</Dropzone.Reject>
|
||||||
<Dropzone.Idle>
|
<Dropzone.Idle>
|
||||||
<IconPhoto size={32} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
<IconPhoto size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||||
</Dropzone.Idle>
|
</Dropzone.Idle>
|
||||||
<div>
|
|
||||||
<Text size="md" inline>Seret gambar ke sini atau klik untuk memilih</Text>
|
|
||||||
<Text size="sm" c="dimmed" inline mt={7} display="block">
|
|
||||||
Ukuran maksimal 5MB (JPEG, JPG, PNG, WebP)
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</Group>
|
</Group>
|
||||||
|
<Text ta="center" mt="sm" size="sm" color="dimmed">
|
||||||
|
Seret gambar atau klik untuk memilih file (maks 5MB)
|
||||||
|
</Text>
|
||||||
</Dropzone>
|
</Dropzone>
|
||||||
|
|
||||||
{previewImage2 ? (
|
{previewImage2 ? (
|
||||||
|
<Box mt="sm" style={{ textAlign: 'center' }}>
|
||||||
<Image
|
<Image
|
||||||
src={previewImage2.preview}
|
src={previewImage2.preview}
|
||||||
alt="Preview Gambar Tambahan"
|
alt="Preview Gambar Tambahan"
|
||||||
width={280}
|
radius="md"
|
||||||
height={180}
|
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
|
||||||
fit="cover"
|
|
||||||
radius="sm"
|
|
||||||
mt="md"
|
|
||||||
/>
|
/>
|
||||||
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
<Text size="sm" c="dimmed" mt="sm">
|
<Text size="sm" c="dimmed" mt="sm" ta="center">
|
||||||
Kosongkan jika tidak ada gambar tambahan
|
Kosongkan jika tidak ada gambar tambahan
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
|
|
||||||
|
{/* Tombol Simpan */}
|
||||||
|
<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>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,13 +1,30 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
import {
|
||||||
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
|
Box,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Group,
|
||||||
|
Pagination,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
TableTbody,
|
||||||
|
TableTd,
|
||||||
|
TableTh,
|
||||||
|
TableThead,
|
||||||
|
TableTr,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
Tooltip
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import HeaderSearch from '../../../_com/header';
|
import HeaderSearch from '../../../_com/header';
|
||||||
import JudulList from '../../../_com/judulList';
|
|
||||||
import stateLayananDesa from '../../../_state/desa/layananDesa';
|
import stateLayananDesa from '../../../_state/desa/layananDesa';
|
||||||
|
|
||||||
function SuratKeterangan() {
|
function SuratKeterangan() {
|
||||||
@@ -16,7 +33,7 @@ function SuratKeterangan() {
|
|||||||
<Box>
|
<Box>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
title='Pelayanan Surat Keterangan'
|
title='Pelayanan Surat Keterangan'
|
||||||
placeholder='pencarian'
|
placeholder='Cari nama atau deskripsi...'
|
||||||
searchIcon={<IconSearch size={20} />}
|
searchIcon={<IconSearch size={20} />}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
@@ -27,8 +44,8 @@ function SuratKeterangan() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ListSuratKeterangan({ search }: { search: string }) {
|
function ListSuratKeterangan({ search }: { search: string }) {
|
||||||
const suratKeteranganState = useProxy(stateLayananDesa.suratKeterangan)
|
const suratKeteranganState = useProxy(stateLayananDesa.suratKeterangan);
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
@@ -39,102 +56,111 @@ function ListSuratKeterangan({ search }: { search: string }) {
|
|||||||
} = suratKeteranganState.findMany;
|
} = suratKeteranganState.findMany;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
load(page, 10)
|
load(page, 10, search);
|
||||||
}, [])
|
}, [page, search]);
|
||||||
|
|
||||||
const filteredData = useMemo(() => {
|
const filteredData = useMemo(() => {
|
||||||
if (!data) return [];
|
if (!data) return [];
|
||||||
return data.filter(item => {
|
|
||||||
const keyword = search.toLowerCase();
|
const keyword = search.toLowerCase();
|
||||||
return (
|
return data.filter(item =>
|
||||||
item.name?.toLowerCase().includes(keyword) ||
|
item.name?.toLowerCase().includes(keyword) ||
|
||||||
item.deskripsi?.toLowerCase().includes(keyword)
|
item.deskripsi?.toLowerCase().includes(keyword)
|
||||||
);
|
);
|
||||||
})
|
|
||||||
}, [data, search]);
|
}, [data, search]);
|
||||||
|
|
||||||
// Handle loading state
|
// Loading state
|
||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton height={300} />
|
<Skeleton height={600} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.length === 0) {
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={10}>
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
|
||||||
<JudulList
|
<Group justify="space-between" mb="md">
|
||||||
title='List Surat Keterangan'
|
<Title order={4}>List Surat Keterangan</Title>
|
||||||
href='/admin/desa/layanan/pelayanan_surat_keterangan/create'
|
<Tooltip label="Tambah Surat Keterangan" withArrow>
|
||||||
/>
|
<Button
|
||||||
<Box style={{ overflowX: "auto" }}>
|
leftSection={<IconPlus size={18} />}
|
||||||
<Table striped withTableBorder withRowBorders>
|
color="blue"
|
||||||
<TableThead>
|
variant="light"
|
||||||
<TableTr>
|
onClick={() =>
|
||||||
<TableTh>Nama</TableTh>
|
router.push('/admin/desa/layanan/pelayanan_surat_keterangan/create')
|
||||||
<TableTh>Deskripsi</TableTh>
|
|
||||||
<TableTh>Detail</TableTh>
|
|
||||||
</TableTr>
|
|
||||||
</TableThead>
|
|
||||||
</Table>
|
|
||||||
</Box>
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return (
|
>
|
||||||
<Box py={10}>
|
Tambah Baru
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
</Button>
|
||||||
<JudulList
|
</Tooltip>
|
||||||
title='List Surat Keterangan'
|
</Group>
|
||||||
href='/admin/desa/layanan/pelayanan_surat_keterangan/create'
|
<Box style={{ overflowX: "auto" }}>
|
||||||
/>
|
<Table highlightOnHover>
|
||||||
<Table striped withTableBorder withRowBorders>
|
|
||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh>Nama</TableTh>
|
<TableTh style={{ width: '30%' }}>Nama</TableTh>
|
||||||
<TableTh>Deskripsi</TableTh>
|
<TableTh style={{ width: '45%' }}>Deskripsi</TableTh>
|
||||||
<TableTh>Detail</TableTh>
|
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
</TableThead>
|
</TableThead>
|
||||||
<TableTbody>
|
<TableTbody>
|
||||||
{filteredData.map((item) => (
|
{filteredData.length > 0 ? (
|
||||||
|
filteredData.map((item) => (
|
||||||
<TableTr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
<TableTd>
|
<TableTd style={{ width: '30%' }}>
|
||||||
<Box w={200}>
|
<Box w={200}>
|
||||||
<Text truncate="end" fz={"sm"}>{item.name}</Text>
|
<Text fw={500} truncate="end" lineClamp={1}>
|
||||||
</Box>
|
{item.name}
|
||||||
</TableTd>
|
|
||||||
<TableTd>
|
|
||||||
<Box w={300}>
|
|
||||||
<Text truncate="end" lineClamp={1} fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
|
||||||
</Box>
|
|
||||||
</TableTd>
|
|
||||||
<TableTd>
|
|
||||||
<Text>
|
|
||||||
<Button onClick={() => router.push(`/admin/desa/layanan/pelayanan_surat_keterangan/${item.id}`)}>
|
|
||||||
<IconDeviceImac size={20} />
|
|
||||||
</Button>
|
|
||||||
</Text>
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd style={{ width: '45%' }}>
|
||||||
|
<Box w={200}>
|
||||||
|
<Text truncate="end" lineClamp={1} fz="sm" c="dimmed"
|
||||||
|
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd style={{ width: '15%' }}>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
onClick={() =>
|
||||||
|
router.push(`/admin/desa/layanan/pelayanan_surat_keterangan/${item.id}`)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<IconDeviceImac size={20} />
|
||||||
|
<Text ml={5}>Detail</Text>
|
||||||
|
</Button>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))}
|
))
|
||||||
|
) : (
|
||||||
|
<TableTr>
|
||||||
|
<TableTd colSpan={3}>
|
||||||
|
<Center py={20}>
|
||||||
|
<Text color="dimmed">Tidak ada data surat keterangan yang cocok</Text>
|
||||||
|
</Center>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
)}
|
||||||
</TableTbody>
|
</TableTbody>
|
||||||
</Table>
|
</Table>
|
||||||
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
<Center>
|
<Center>
|
||||||
<Pagination
|
<Pagination
|
||||||
value={page}
|
value={page}
|
||||||
onChange={(newPage) => {
|
onChange={(newPage) => {
|
||||||
load(newPage, 10);
|
load(newPage, 10, search);
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
}}
|
}}
|
||||||
total={totalPages}
|
total={totalPages}
|
||||||
mt="md"
|
mt="md"
|
||||||
mb="md"
|
mb="md"
|
||||||
|
color="blue"
|
||||||
|
radius="md"
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -2,22 +2,34 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
|
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
import { IconArrowBack } from '@tabler/icons-react';
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
||||||
|
|
||||||
function EditPelayananTelunjukSakti() {
|
function EditPelayananTelunjukSakti() {
|
||||||
const stateTelunjukDesa = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa)
|
const stateTelunjukDesa = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa);
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
const params = useParams()
|
const params = useParams();
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: stateTelunjukDesa.edit.form.name,
|
name: stateTelunjukDesa.edit.form.name,
|
||||||
deskripsi: stateTelunjukDesa.edit.form.deskripsi,
|
deskripsi: stateTelunjukDesa.edit.form.deskripsi,
|
||||||
link: stateTelunjukDesa.edit.form.link,
|
link: stateTelunjukDesa.edit.form.link,
|
||||||
})
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadPelayananTelunjukSakti = async () => {
|
const loadPelayananTelunjukSakti = async () => {
|
||||||
@@ -27,14 +39,14 @@ function EditPelayananTelunjukSakti() {
|
|||||||
const data = await stateTelunjukDesa.edit.load(id);
|
const data = await stateTelunjukDesa.edit.load(id);
|
||||||
if (data) {
|
if (data) {
|
||||||
setFormData({
|
setFormData({
|
||||||
name: data.name,
|
name: data.name || '',
|
||||||
deskripsi: data.deskripsi,
|
deskripsi: data.deskripsi || '',
|
||||||
link: data.link,
|
link: data.link || '',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading pelayanan telunjuk sakti:", error);
|
console.error('Error loading pelayanan telunjuk sakti:', error);
|
||||||
toast.error("Gagal memuat data pelayanan telunjuk sakti");
|
toast.error('Gagal memuat data pelayanan telunjuk sakti');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
loadPelayananTelunjukSakti();
|
loadPelayananTelunjukSakti();
|
||||||
@@ -44,54 +56,83 @@ function EditPelayananTelunjukSakti() {
|
|||||||
try {
|
try {
|
||||||
stateTelunjukDesa.edit.form = {
|
stateTelunjukDesa.edit.form = {
|
||||||
...stateTelunjukDesa.edit.form,
|
...stateTelunjukDesa.edit.form,
|
||||||
name: formData.name,
|
...formData,
|
||||||
deskripsi: formData.deskripsi,
|
};
|
||||||
link: formData.link,
|
await stateTelunjukDesa.edit.update();
|
||||||
}
|
toast.success('Pelayanan telunjuk sakti berhasil diperbarui!');
|
||||||
await stateTelunjukDesa.edit.update()
|
router.push('/admin/desa/layanan/pelayanan_telunjuk_sakti_desa');
|
||||||
toast.success("Pelayanan telunjuk sakti berhasil diperbarui!")
|
|
||||||
router.push("/admin/desa/layanan/pelayanan_telunjuk_sakti_desa")
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating pelayanan telunjuk sakti:", error);
|
console.error('Error updating pelayanan telunjuk sakti:', error);
|
||||||
toast.error("Terjadi kesalahan saat memperbarui pelayanan telunjuk sakti");
|
toast.error('Terjadi kesalahan saat memperbarui pelayanan telunjuk sakti');
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Back Button + Title */}
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Tooltip>
|
||||||
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
|
<Title order={4} ml="sm" c="dark">
|
||||||
<Stack gap={"xs"}>
|
Edit Pelayanan Telunjuk Sakti Desa
|
||||||
<Title order={3}>Edit Surat Keterangan</Title>
|
</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">
|
||||||
|
{/* Nama */}
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label="Nama Pelayanan"
|
||||||
|
placeholder="Masukkan nama pelayanan"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(val) => {
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
setFormData({ ...formData, name: val.target.value });
|
required
|
||||||
}}
|
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Nama Surat Keterangan</Text>}
|
|
||||||
placeholder="masukkan nama surat keterangan"
|
|
||||||
/>
|
/>
|
||||||
<TextInput
|
|
||||||
|
{/* Deskripsi pakai editor */}
|
||||||
|
<Box>
|
||||||
|
<Text fz="sm" fw="bold" mb={6}>
|
||||||
|
Deskripsi
|
||||||
|
</Text>
|
||||||
|
<EditEditor
|
||||||
value={formData.deskripsi}
|
value={formData.deskripsi}
|
||||||
onChange={(val) => {
|
onChange={(htmlContent) => setFormData({ ...formData, deskripsi: htmlContent })}
|
||||||
setFormData({ ...formData, deskripsi: val.target.value });
|
|
||||||
}}
|
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Tautan Link</Text>}
|
|
||||||
placeholder="masukkan tautan link"
|
|
||||||
/>
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Link */}
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label="Link"
|
||||||
|
placeholder="Masukkan link terkait"
|
||||||
value={formData.link}
|
value={formData.link}
|
||||||
onChange={(val) => {
|
onChange={(e) => setFormData({ ...formData, link: e.target.value })}
|
||||||
setFormData({ ...formData, link: val.target.value });
|
|
||||||
}}
|
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Link</Text>}
|
|
||||||
placeholder="masukkan link"
|
|
||||||
/>
|
/>
|
||||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
|
|
||||||
|
{/* Tombol Simpan */}
|
||||||
|
<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>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -2,109 +2,166 @@
|
|||||||
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||||
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
|
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
|
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
function DetailPelayananTelunjukSakti() {
|
function DetailPelayananTelunjukSakti() {
|
||||||
const telunjukSaktiState = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa)
|
const telunjukSaktiState = useProxy(
|
||||||
const [modalHapus, setModalHapus] = useState(false)
|
stateLayananDesa.pelayananTelunjukSaktiDesa
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
);
|
||||||
const params = useParams()
|
const [modalHapus, setModalHapus] = useState(false);
|
||||||
const router = useRouter()
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
|
const params = useParams();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
telunjukSaktiState.findUnique.load(params?.id as string)
|
telunjukSaktiState.findUnique.load(params?.id as string);
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
const handleHapus = () => {
|
const handleHapus = () => {
|
||||||
if (selectedId) {
|
if (selectedId) {
|
||||||
telunjukSaktiState.delete.byId(selectedId)
|
telunjukSaktiState.delete.byId(selectedId);
|
||||||
setModalHapus(false)
|
setModalHapus(false);
|
||||||
setSelectedId(null)
|
setSelectedId(null);
|
||||||
router.push("/admin/desa/layanan/pelayanan_telunjuk_sakti_desa")
|
router.push('/admin/desa/layanan/pelayanan_telunjuk_sakti_desa');
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (!telunjukSaktiState.findUnique.data) {
|
if (!telunjukSaktiState.findUnique.data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
{Array.from({ length: 10 }).map((_, k) => (
|
<Skeleton height={500} radius="md" />
|
||||||
<Skeleton key={k} h={40} />
|
|
||||||
))}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = telunjukSaktiState.findUnique.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box py={10}>
|
||||||
<Box mb={10}>
|
{/* Tombol Kembali */}
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Button
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
variant="subtle"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
|
||||||
|
mb={15}
|
||||||
|
>
|
||||||
|
Kembali
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
|
||||||
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
|
<Paper
|
||||||
<Stack>
|
withBorder
|
||||||
<Text fz={"xl"} fw={"bold"}>Detail Pelayanan Telunjuk Sakti Desa</Text>
|
w={{ base: '100%', md: '60%' }}
|
||||||
{telunjukSaktiState.findUnique.data ? (
|
bg={colors['white-1']}
|
||||||
<Paper key={telunjukSaktiState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
p="lg"
|
||||||
<Stack gap={"xs"}>
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
|
||||||
|
Detail Pelayanan Telunjuk Sakti Desa
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
|
||||||
|
<Stack gap="sm">
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw={"bold"} fz={"lg"}>Nama</Text>
|
<Text fz="lg" fw="bold">
|
||||||
<Text fz={"lg"}>{telunjukSaktiState.findUnique.data?.name}</Text>
|
Nama
|
||||||
|
</Text>
|
||||||
|
<Text fz="md" c="dimmed">
|
||||||
|
{data?.name || '-'}
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw={"bold"} fz={"lg"}>Link</Text>
|
<Text fz="lg" fw="bold">
|
||||||
|
Link
|
||||||
|
</Text>
|
||||||
|
{data?.link ? (
|
||||||
<Text
|
<Text
|
||||||
|
fz="md"
|
||||||
component="a"
|
component="a"
|
||||||
href={telunjukSaktiState.findUnique.data?.link}
|
href={data.link}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
|
c="blue"
|
||||||
style={{
|
style={{
|
||||||
display: 'block',
|
display: 'block',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
whiteSpace: 'nowrap'
|
whiteSpace: 'nowrap',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{telunjukSaktiState.findUnique.data?.link}
|
{data.link}
|
||||||
</Text>
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Text fz="sm" c="dimmed">
|
||||||
|
Tidak ada link
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
|
<Text fz="lg" fw="bold">
|
||||||
<Text fz={"lg"}>{telunjukSaktiState.findUnique.data?.deskripsi}</Text>
|
Deskripsi
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
fz="md"
|
||||||
|
c="dimmed"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: data?.deskripsi || '-',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Flex gap={"xs"} mt={10}>
|
|
||||||
|
<Group gap="sm">
|
||||||
|
<Tooltip label="Hapus Layanan" withArrow position="top">
|
||||||
<Button
|
<Button
|
||||||
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (telunjukSaktiState.findUnique.data) {
|
setSelectedId(data.id);
|
||||||
setSelectedId(telunjukSaktiState.findUnique.data.id);
|
|
||||||
setModalHapus(true);
|
setModalHapus(true);
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
disabled={telunjukSaktiState.delete.loading || !telunjukSaktiState.findUnique.data}
|
variant="light"
|
||||||
color={"red"}
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
disabled={telunjukSaktiState.delete.loading}
|
||||||
>
|
>
|
||||||
<IconX size={20} />
|
<IconTrash size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip label="Edit Layanan" withArrow position="top">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
color="green"
|
||||||
if (telunjukSaktiState.findUnique.data) {
|
onClick={() =>
|
||||||
router.push(`/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/${telunjukSaktiState.findUnique.data.id}/edit`);
|
router.push(
|
||||||
|
`/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/${data.id}/edit`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}}
|
variant="light"
|
||||||
disabled={!telunjukSaktiState.findUnique.data}
|
radius="md"
|
||||||
color={"green"}
|
size="md"
|
||||||
>
|
>
|
||||||
<IconEdit size={20} />
|
<IconEdit size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
) : null}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
@@ -113,7 +170,7 @@ function DetailPelayananTelunjukSakti() {
|
|||||||
opened={modalHapus}
|
opened={modalHapus}
|
||||||
onClose={() => setModalHapus(false)}
|
onClose={() => setModalHapus(false)}
|
||||||
onConfirm={handleHapus}
|
onConfirm={handleHapus}
|
||||||
text='Apakah anda yakin ingin menghapus berita ini?'
|
text="Apakah Anda yakin ingin menghapus layanan ini?"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,64 +1,117 @@
|
|||||||
'use client'
|
'use client';
|
||||||
|
|
||||||
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
|
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
import { IconArrowBack } from '@tabler/icons-react';
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
function CreatePelayananTelunjukDesa() {
|
function CreatePelayananTelunjukDesa() {
|
||||||
const stateTelunjukDesa = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa)
|
const stateTelunjukDesa = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa);
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
stateTelunjukDesa.create.form = {
|
stateTelunjukDesa.create.form = {
|
||||||
name: "",
|
name: '',
|
||||||
deskripsi: "",
|
deskripsi: '',
|
||||||
link: "",
|
link: '',
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
await stateTelunjukDesa.create.create()
|
try {
|
||||||
resetForm()
|
await stateTelunjukDesa.create.create();
|
||||||
router.push("/admin/desa/layanan/pelayanan_telunjuk_sakti_desa")
|
resetForm();
|
||||||
|
toast.success('Data pelayanan telunjuk sakti berhasil ditambahkan');
|
||||||
|
router.push('/admin/desa/layanan/pelayanan_telunjuk_sakti_desa');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error create pelayanan telunjuk sakti:', error);
|
||||||
|
toast.error('Terjadi kesalahan saat menambahkan data');
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Header */}
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Tooltip>
|
||||||
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
|
<Title order={4} ml="sm" c="dark">
|
||||||
<Stack gap={"xs"}>
|
Tambah Pelayanan Telunjuk Sakti Desa
|
||||||
<Title order={3}>Create Pelayanan Telunjuk Sakti Desa</Title>
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{/* Form */}
|
||||||
|
<Paper
|
||||||
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
{/* Nama */}
|
||||||
<TextInput
|
<TextInput
|
||||||
value={stateTelunjukDesa.create.form.name}
|
value={stateTelunjukDesa.create.form.name}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
stateTelunjukDesa.create.form.name = val.target.value;
|
stateTelunjukDesa.create.form.name = val.target.value;
|
||||||
}}
|
}}
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Nama Pelayanan Telunjuk Sakti Desa</Text>}
|
label={<Text fz="sm" fw="bold">Nama Pelayanan</Text>}
|
||||||
placeholder="masukkan nama pelayanan telunjuk sakti desa"
|
placeholder="Masukkan nama pelayanan telunjuk sakti desa"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Deskripsi */}
|
||||||
<TextInput
|
<TextInput
|
||||||
value={stateTelunjukDesa.create.form.deskripsi}
|
value={stateTelunjukDesa.create.form.deskripsi}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
stateTelunjukDesa.create.form.deskripsi = val.target.value;
|
stateTelunjukDesa.create.form.deskripsi = val.target.value;
|
||||||
}}
|
}}
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Tautan Link</Text>}
|
label={<Text fz="sm" fw="bold">Deskripsi</Text>}
|
||||||
placeholder="masukkan tautan link"
|
placeholder="Masukkan deskripsi pelayanan"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Link */}
|
||||||
<TextInput
|
<TextInput
|
||||||
value={stateTelunjukDesa.create.form.link}
|
value={stateTelunjukDesa.create.form.link}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
stateTelunjukDesa.create.form.link = val.target.value;
|
stateTelunjukDesa.create.form.link = val.target.value;
|
||||||
}}
|
}}
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Link</Text>}
|
label={<Text fz="sm" fw="bold">Link</Text>}
|
||||||
placeholder="masukkan link"
|
placeholder="Masukkan link pelayanan"
|
||||||
/>
|
/>
|
||||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
|
|
||||||
|
{/* Tombol Simpan */}
|
||||||
|
<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>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,13 +1,187 @@
|
|||||||
|
// /* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
// '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 { IconDeviceImac, IconSearch } from '@tabler/icons-react';
|
||||||
|
// import { useRouter } from 'next/navigation';
|
||||||
|
// import { useEffect, useMemo, useState } from 'react';
|
||||||
|
// import { useProxy } from 'valtio/utils';
|
||||||
|
// import HeaderSearch from '../../../_com/header';
|
||||||
|
// import JudulList from '../../../_com/judulList';
|
||||||
|
// import stateLayananDesa from '../../../_state/desa/layananDesa';
|
||||||
|
|
||||||
|
// function PelayananTelunjukSakti() {
|
||||||
|
// const [search, setSearch] = useState("");
|
||||||
|
// return (
|
||||||
|
// <Box>
|
||||||
|
// <HeaderSearch
|
||||||
|
// title='Posisi Organisasi'
|
||||||
|
// placeholder='pencarian'
|
||||||
|
// searchIcon={<IconSearch size={20} />}
|
||||||
|
// value={search}
|
||||||
|
// onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
|
// />
|
||||||
|
// <ListPelayananTelunjukSakti search={search} />
|
||||||
|
// </Box>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// function ListPelayananTelunjukSakti({ search }: { search: string }) {
|
||||||
|
// const telunjukSaktiState = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa)
|
||||||
|
// const router = useRouter()
|
||||||
|
|
||||||
|
// const {
|
||||||
|
// data,
|
||||||
|
// page,
|
||||||
|
// totalPages,
|
||||||
|
// loading,
|
||||||
|
// load,
|
||||||
|
// } = telunjukSaktiState.findMany;
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// load(page, 10)
|
||||||
|
// }, [])
|
||||||
|
|
||||||
|
// const filteredData = useMemo(() => {
|
||||||
|
// if (!data) return [];
|
||||||
|
// return data.filter(item => {
|
||||||
|
// const keyword = search.toLowerCase();
|
||||||
|
// return (
|
||||||
|
// item.name?.toLowerCase().includes(keyword) ||
|
||||||
|
// item.link?.toLowerCase().includes(keyword) ||
|
||||||
|
// item.deskripsi?.toLowerCase().includes(keyword)
|
||||||
|
// );
|
||||||
|
// })
|
||||||
|
// .sort((a, b) => a.posisi?.hierarki - b.posisi?.hierarki);
|
||||||
|
// }, [data, search]);
|
||||||
|
|
||||||
|
// if (loading || !data) {
|
||||||
|
// return (
|
||||||
|
// <Stack py={10}>
|
||||||
|
// <Skeleton height={300} />
|
||||||
|
// </Stack>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (data.length === 0) {
|
||||||
|
// return (
|
||||||
|
// <Box py={10}>
|
||||||
|
// <Paper bg={colors['white-1']} p={'md'}>
|
||||||
|
// <JudulList
|
||||||
|
// title='List Pelayanan Telunjuk Sakti Desa'
|
||||||
|
// href='/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/create'
|
||||||
|
// />
|
||||||
|
// <Table striped withTableBorder withRowBorders>
|
||||||
|
// <TableThead>
|
||||||
|
// <TableTr>
|
||||||
|
// <TableTh>Nama</TableTh>
|
||||||
|
// <TableTh>Link</TableTh>
|
||||||
|
// <TableTh>Detail</TableTh>
|
||||||
|
// </TableTr>
|
||||||
|
// </TableThead>
|
||||||
|
// <TableTbody>
|
||||||
|
// <TableTr>
|
||||||
|
// <TableTd colSpan={3}>
|
||||||
|
// <Text fz={"sm"} color="gray.5">
|
||||||
|
// Tidak ada data
|
||||||
|
// </Text>
|
||||||
|
// </TableTd>
|
||||||
|
// </TableTr>
|
||||||
|
// </TableTbody>
|
||||||
|
// </Table>
|
||||||
|
// </Paper>
|
||||||
|
// </Box>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <Box py={10}>
|
||||||
|
// <Paper bg={colors['white-1']} p={'md'}>
|
||||||
|
// <JudulList
|
||||||
|
// title='List Pelayanan Telunjuk Sakti Desa'
|
||||||
|
// href='/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/create'
|
||||||
|
// />
|
||||||
|
// <Table striped withTableBorder withRowBorders>
|
||||||
|
// <TableThead>
|
||||||
|
// <TableTr>
|
||||||
|
// <TableTh>Nama</TableTh>
|
||||||
|
// <TableTh>Link</TableTh>
|
||||||
|
// <TableTh>Detail</TableTh>
|
||||||
|
// </TableTr>
|
||||||
|
// </TableThead>
|
||||||
|
// <TableTbody>
|
||||||
|
// {filteredData.map((item) => (
|
||||||
|
// <TableTr key={item.id}>
|
||||||
|
// <TableTd>
|
||||||
|
// <Box w={100}>
|
||||||
|
// <Text truncate="end" lineClamp={1} fz={"sm"} dangerouslySetInnerHTML={{ __html: item.name }} />
|
||||||
|
// </Box>
|
||||||
|
// </TableTd>
|
||||||
|
// <TableTd>
|
||||||
|
// <Box w={100}>
|
||||||
|
// <a href={item.link} target="_blank" rel="noopener noreferrer">
|
||||||
|
// <Text dangerouslySetInnerHTML={{ __html: item.deskripsi }} truncate="end" fz={"sm"} />
|
||||||
|
// </a>
|
||||||
|
// </Box>
|
||||||
|
// </TableTd>
|
||||||
|
// <TableTd>
|
||||||
|
// <Text>
|
||||||
|
// <Button onClick={() => router.push(`/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/${item.id}`)}>
|
||||||
|
// <IconDeviceImac size={20} />
|
||||||
|
// </Button>
|
||||||
|
// </Text>
|
||||||
|
// </TableTd>
|
||||||
|
// </TableTr>
|
||||||
|
// ))}
|
||||||
|
// </TableTbody>
|
||||||
|
// </Table>
|
||||||
|
// </Paper>
|
||||||
|
// <Center>
|
||||||
|
// <Pagination
|
||||||
|
// value={page}
|
||||||
|
// onChange={(newPage) => {
|
||||||
|
// load(newPage, 10);
|
||||||
|
// window.scrollTo(0, 0);
|
||||||
|
// }}
|
||||||
|
// total={totalPages}
|
||||||
|
// mt="md"
|
||||||
|
// mb="md"
|
||||||
|
// />
|
||||||
|
// </Center>
|
||||||
|
// </Box>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export default PelayananTelunjukSakti;
|
||||||
|
|
||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
import {
|
||||||
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
|
Box,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Group,
|
||||||
|
Pagination,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
TableTbody,
|
||||||
|
TableTd,
|
||||||
|
TableTh,
|
||||||
|
TableThead,
|
||||||
|
TableTr,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import HeaderSearch from '../../../_com/header';
|
import HeaderSearch from '../../../_com/header';
|
||||||
import JudulList from '../../../_com/judulList';
|
|
||||||
import stateLayananDesa from '../../../_state/desa/layananDesa';
|
import stateLayananDesa from '../../../_state/desa/layananDesa';
|
||||||
|
|
||||||
function PelayananTelunjukSakti() {
|
function PelayananTelunjukSakti() {
|
||||||
@@ -15,8 +189,8 @@ function PelayananTelunjukSakti() {
|
|||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
title='Posisi Organisasi'
|
title="Pelayanan Telunjuk Sakti"
|
||||||
placeholder='pencarian'
|
placeholder="Cari layanan..."
|
||||||
searchIcon={<IconSearch size={20} />}
|
searchIcon={<IconSearch size={20} />}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
@@ -27,125 +201,113 @@ function PelayananTelunjukSakti() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ListPelayananTelunjukSakti({ search }: { search: string }) {
|
function ListPelayananTelunjukSakti({ search }: { search: string }) {
|
||||||
const telunjukSaktiState = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa)
|
const telunjukSaktiState = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa);
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
const {
|
const { data, page, totalPages, loading, load } = telunjukSaktiState.findMany;
|
||||||
data,
|
|
||||||
page,
|
|
||||||
totalPages,
|
|
||||||
loading,
|
|
||||||
load,
|
|
||||||
} = telunjukSaktiState.findMany;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
load(page, 10)
|
load(page, 10, search);
|
||||||
}, [])
|
}, [page, search]);
|
||||||
|
|
||||||
const filteredData = useMemo(() => {
|
const filteredData = data || [];
|
||||||
if (!data) return [];
|
|
||||||
return data.filter(item => {
|
|
||||||
const keyword = search.toLowerCase();
|
|
||||||
return (
|
|
||||||
item.name?.toLowerCase().includes(keyword) ||
|
|
||||||
item.link?.toLowerCase().includes(keyword) ||
|
|
||||||
item.deskripsi?.toLowerCase().includes(keyword)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.sort((a, b) => a.posisi?.hierarki - b.posisi?.hierarki);
|
|
||||||
}, [data, search]);
|
|
||||||
|
|
||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton height={300} />
|
<Skeleton height={400} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.length === 0) {
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={10}>
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||||
<JudulList
|
<Group justify="space-between" mb="md">
|
||||||
title='List Pelayanan Telunjuk Sakti Desa'
|
<Title order={4}>Daftar Pelayanan Telunjuk Sakti</Title>
|
||||||
href='/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/create'
|
<Tooltip label="Tambah Layanan" withArrow>
|
||||||
/>
|
<Button
|
||||||
<Table striped withTableBorder withRowBorders>
|
leftSection={<IconPlus size={18} />}
|
||||||
<TableThead>
|
color="blue"
|
||||||
<TableTr>
|
variant="light"
|
||||||
<TableTh>Nama</TableTh>
|
onClick={() =>
|
||||||
<TableTh>Link</TableTh>
|
router.push('/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/create')
|
||||||
<TableTh>Detail</TableTh>
|
|
||||||
</TableTr>
|
|
||||||
</TableThead>
|
|
||||||
<TableTbody>
|
|
||||||
<TableTr>
|
|
||||||
<TableTd colSpan={3}>
|
|
||||||
<Text fz={"sm"} color="gray.5">
|
|
||||||
Tidak ada data
|
|
||||||
</Text>
|
|
||||||
</TableTd>
|
|
||||||
</TableTr>
|
|
||||||
</TableTbody>
|
|
||||||
</Table>
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
>
|
||||||
return (
|
Tambah Baru
|
||||||
<Box py={10}>
|
</Button>
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
</Tooltip>
|
||||||
<JudulList
|
</Group>
|
||||||
title='List Pelayanan Telunjuk Sakti Desa'
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
href='/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/create'
|
<Table highlightOnHover style={{ minWidth: '700px' }}>
|
||||||
/>
|
|
||||||
<Table striped withTableBorder withRowBorders>
|
|
||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh>Nama</TableTh>
|
<TableTh style={{ width: '30%' }}>Nama</TableTh>
|
||||||
<TableTh>Link</TableTh>
|
<TableTh style={{ width: '40%' }}>Link</TableTh>
|
||||||
<TableTh>Detail</TableTh>
|
<TableTh style={{ width: '30%' }}>Detail</TableTh>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
</TableThead>
|
</TableThead>
|
||||||
<TableTbody>
|
<TableTbody>
|
||||||
{filteredData.map((item) => (
|
{filteredData.length > 0 ? (
|
||||||
|
filteredData.map((item) => (
|
||||||
<TableTr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Box w={100}>
|
<Box w={200}>
|
||||||
<Text truncate="end" lineClamp={1} fz={"sm"} dangerouslySetInnerHTML={{ __html: item.name }} />
|
<Text fw={500} truncate="end" lineClamp={1}>
|
||||||
</Box>
|
{item.name}
|
||||||
|
</Text></Box>
|
||||||
|
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Box w={100}>
|
<Box w={200}>
|
||||||
<a href={item.link} target="_blank" rel="noopener noreferrer">
|
<a href={item.link} target="_blank" rel="noopener noreferrer">
|
||||||
<Text dangerouslySetInnerHTML={{ __html: item.deskripsi }} truncate="end" fz={"sm"} />
|
<Text lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} truncate="end" fz={"sm"} />
|
||||||
</a>
|
</a>
|
||||||
</Box>
|
</Box>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Text>
|
<Button
|
||||||
<Button onClick={() => router.push(`/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/${item.id}`)}>
|
variant="light"
|
||||||
<IconDeviceImac size={20} />
|
color="blue"
|
||||||
|
onClick={() =>
|
||||||
|
router.push(
|
||||||
|
`/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/${item.id}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<IconDeviceImacCog size={20} />
|
||||||
|
<Text ml={5}>Detail</Text>
|
||||||
</Button>
|
</Button>
|
||||||
</Text>
|
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))}
|
))
|
||||||
|
) : (
|
||||||
|
<TableTr>
|
||||||
|
<TableTd colSpan={3}>
|
||||||
|
<Center py={20}>
|
||||||
|
<Text color="dimmed">
|
||||||
|
Tidak ada data layanan yang cocok
|
||||||
|
</Text>
|
||||||
|
</Center>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
)}
|
||||||
</TableTbody>
|
</TableTbody>
|
||||||
</Table>
|
</Table>
|
||||||
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
<Center>
|
<Center>
|
||||||
<Pagination
|
<Pagination
|
||||||
value={page}
|
value={page}
|
||||||
onChange={(newPage) => {
|
onChange={(newPage) => {
|
||||||
load(newPage, 10);
|
load(newPage, 10, search);
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
}}
|
}}
|
||||||
total={totalPages}
|
total={totalPages}
|
||||||
mt="md"
|
mt="md"
|
||||||
mb="md"
|
mb="md"
|
||||||
|
color="blue"
|
||||||
|
radius="md"
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -153,3 +315,4 @@ function ListPelayananTelunjukSakti({ search }: { search: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default PelayananTelunjukSakti;
|
export default PelayananTelunjukSakti;
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,18 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
|||||||
import penghargaanState from '@/app/admin/(dashboard)/_state/desa/penghargaan';
|
import penghargaanState from '@/app/admin/(dashboard)/_state/desa/penghargaan';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import ApiFetch from '@/lib/api-fetch';
|
import ApiFetch from '@/lib/api-fetch';
|
||||||
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Image,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
import { Dropzone } from '@mantine/dropzone';
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
@@ -84,87 +95,104 @@ function EditPenghargaan() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Tombol Back + Title */}
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors["blue-button"]} size={30} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Tooltip>
|
||||||
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
|
<Title order={4} ml="sm" c="dark">
|
||||||
<Stack gap={"xs"}>
|
Edit Penghargaan
|
||||||
<Title order={3}>Edit Penghargaan</Title>
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{/* Card Form */}
|
||||||
|
<Paper
|
||||||
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
{/* Input Judul */}
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label="Judul"
|
||||||
|
placeholder="Masukkan judul penghargaan"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
|
required
|
||||||
placeholder="masukkan judul"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Input Juara */}
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label="Juara"
|
||||||
|
placeholder="Masukkan juara"
|
||||||
value={formData.juara}
|
value={formData.juara}
|
||||||
onChange={(e) => setFormData({ ...formData, juara: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, juara: e.target.value })}
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Juara</Text>}
|
required
|
||||||
placeholder="masukkan juara"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Upload Gambar */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
<Box>
|
Gambar Penghargaan
|
||||||
|
</Text>
|
||||||
<Dropzone
|
<Dropzone
|
||||||
onDrop={(files) => {
|
onDrop={(files) => {
|
||||||
const selectedFile = files[0]; // Ambil file pertama
|
const selectedFile = files[0];
|
||||||
if (selectedFile) {
|
if (selectedFile) {
|
||||||
setFile(selectedFile);
|
setFile(selectedFile);
|
||||||
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
|
setPreviewImage(URL.createObjectURL(selectedFile));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onReject={() => toast.error('File tidak valid.')}
|
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
||||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
maxSize={5 * 1024 ** 2}
|
||||||
accept={{ 'image/*': [] }}
|
accept={{ 'image/*': [] }}
|
||||||
|
radius="md"
|
||||||
|
p="xl"
|
||||||
>
|
>
|
||||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
<Group justify="center" gap="xl" mih={180}>
|
||||||
<Dropzone.Accept>
|
<Dropzone.Accept>
|
||||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
|
||||||
</Dropzone.Accept>
|
</Dropzone.Accept>
|
||||||
<Dropzone.Reject>
|
<Dropzone.Reject>
|
||||||
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
<IconX size={48} color="red" stroke={1.5} />
|
||||||
</Dropzone.Reject>
|
</Dropzone.Reject>
|
||||||
<Dropzone.Idle>
|
<Dropzone.Idle>
|
||||||
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
<IconPhoto size={48} color="#868e96" stroke={1.5} />
|
||||||
</Dropzone.Idle>
|
</Dropzone.Idle>
|
||||||
|
<Stack gap="xs" align="center">
|
||||||
<div>
|
<Text size="md" fw={500}>
|
||||||
<Text size="xl" inline>
|
Seret gambar atau klik untuk memilih file
|
||||||
Drag gambar ke sini atau klik untuk pilih file
|
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="sm" c="dimmed" inline mt={7}>
|
<Text size="sm" c="dimmed">
|
||||||
Maksimal 5MB dan harus format gambar
|
Maksimal 5MB, format gambar wajib
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</Stack>
|
||||||
</Group>
|
</Group>
|
||||||
</Dropzone>
|
</Dropzone>
|
||||||
|
|
||||||
{/* Tampilkan preview kalau ada */}
|
|
||||||
{previewImage && (
|
{previewImage && (
|
||||||
<Box mt="sm">
|
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
|
||||||
<Image
|
<Image
|
||||||
src={previewImage}
|
src={previewImage}
|
||||||
alt="Preview"
|
alt="Preview Gambar"
|
||||||
style={{
|
radius="md"
|
||||||
maxWidth: '100%',
|
style={{ maxHeight: 220, objectFit: 'contain', border: `1px solid ${colors['blue-button']}` }}
|
||||||
maxHeight: '200px',
|
|
||||||
objectFit: 'contain',
|
|
||||||
borderRadius: '8px',
|
|
||||||
border: '1px solid #ddd',
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Deskripsi */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
|
<Text fz="sm" fw="bold" mb={6}>
|
||||||
|
Deskripsi
|
||||||
|
</Text>
|
||||||
<EditEditor
|
<EditEditor
|
||||||
value={formData.deskripsi}
|
value={formData.deskripsi}
|
||||||
onChange={(htmlContent) => {
|
onChange={(htmlContent) => {
|
||||||
@@ -174,7 +202,21 @@ function EditPenghargaan() {
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Button onClick={handleSubmit}>Edit Penghargaan</Button>
|
{/* Tombol Simpan */}
|
||||||
|
<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>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -4,105 +4,166 @@ import penghargaanState from '../../../_state/desa/penghargaan';
|
|||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
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 colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { IconArrowBack, IconX, IconEdit } from '@tabler/icons-react';
|
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
|
||||||
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||||
|
|
||||||
function DetailPenghargaan() {
|
function DetailPenghargaan() {
|
||||||
const statePenghargaan = useProxy(penghargaanState)
|
const statePenghargaan = useProxy(penghargaanState);
|
||||||
const [modalHapus, setModalHapus] = useState(false);
|
const [modalHapus, setModalHapus] = useState(false);
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
const params = useParams()
|
const params = useParams();
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
statePenghargaan.findUnique.load(params?.id as string)
|
statePenghargaan.findUnique.load(params?.id as string);
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
const handleHapus = () => {
|
const handleHapus = () => {
|
||||||
if (selectedId) {
|
if (selectedId) {
|
||||||
statePenghargaan.delete.byId(selectedId)
|
statePenghargaan.delete.byId(selectedId);
|
||||||
setModalHapus(false)
|
setModalHapus(false);
|
||||||
setSelectedId(null)
|
setSelectedId(null);
|
||||||
router.push("/admin/desa/penghargaan")
|
router.push('/admin/desa/penghargaan');
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (!statePenghargaan.findUnique.data) {
|
if (!statePenghargaan.findUnique.data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton h={500} />
|
<Skeleton height={500} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = statePenghargaan.findUnique.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box py={10}>
|
||||||
<Box mb={10}>
|
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
|
|
||||||
<Stack>
|
|
||||||
<Text fz={"xl"} fw={"bold"}>Detail Penghargaan</Text>
|
|
||||||
{statePenghargaan.findUnique.data ? (
|
|
||||||
<Paper key={statePenghargaan.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
|
||||||
<Stack gap={"xs"}>
|
|
||||||
<Box>
|
|
||||||
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
|
|
||||||
<Image w={{ base: 400, md: 400, lg: 400 }} src={statePenghargaan.findUnique.data?.image?.link} alt="gambar" />
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text fw={"bold"} fz={"lg"}>Judul</Text>
|
|
||||||
<Text fz={"lg"}>{statePenghargaan.findUnique.data?.name}</Text>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text fw={"bold"} fz={"lg"}>Juara</Text>
|
|
||||||
<Text fz={"lg"}>{statePenghargaan.findUnique.data?.juara}</Text>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
|
|
||||||
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: statePenghargaan.findUnique.data?.deskripsi }} />
|
|
||||||
</Box>
|
|
||||||
<Flex gap={"xs"} mt={10}>
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
variant="subtle"
|
||||||
if (statePenghargaan.findUnique.data) {
|
onClick={() => router.back()}
|
||||||
setSelectedId(statePenghargaan.findUnique.data.id);
|
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
|
||||||
setModalHapus(true);
|
mb={15}
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={statePenghargaan.delete.loading || !statePenghargaan.findUnique.data}
|
|
||||||
color={"red"}
|
|
||||||
>
|
>
|
||||||
<IconX size={20} />
|
Kembali
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<Paper
|
||||||
|
withBorder
|
||||||
|
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 Penghargaan
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">
|
||||||
|
Gambar
|
||||||
|
</Text>
|
||||||
|
{data.image?.link ? (
|
||||||
|
<Image
|
||||||
|
src={data.image.link}
|
||||||
|
alt={data.name || 'Gambar Penghargaan'}
|
||||||
|
w={200}
|
||||||
|
h={200}
|
||||||
|
radius="md"
|
||||||
|
fit="cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Text fz="sm" c="dimmed">
|
||||||
|
Tidak ada gambar
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">
|
||||||
|
Judul
|
||||||
|
</Text>
|
||||||
|
<Text fz="md" c="dimmed">
|
||||||
|
{data.name || '-'}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">
|
||||||
|
Juara
|
||||||
|
</Text>
|
||||||
|
<Text fz="md" c="dimmed">
|
||||||
|
{data.juara || '-'}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">
|
||||||
|
Deskripsi
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
fz="md"
|
||||||
|
c="dimmed"
|
||||||
|
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Group gap="sm" mt={10}>
|
||||||
|
<Tooltip label="Hapus Penghargaan" withArrow position="top">
|
||||||
<Button
|
<Button
|
||||||
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (statePenghargaan.findUnique.data) {
|
setSelectedId(data.id);
|
||||||
router.push(`/admin/desa/penghargaan/${statePenghargaan.findUnique.data.id}/edit`);
|
setModalHapus(true);
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
disabled={!statePenghargaan.findUnique.data}
|
variant="light"
|
||||||
color={"green"}
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
>
|
||||||
|
<IconTrash size={20} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip label="Edit Penghargaan" withArrow position="top">
|
||||||
|
<Button
|
||||||
|
color="green"
|
||||||
|
onClick={() =>
|
||||||
|
router.push(`/admin/desa/penghargaan/${data.id}/edit`)
|
||||||
|
}
|
||||||
|
variant="light"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
>
|
>
|
||||||
<IconEdit size={20} />
|
<IconEdit size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
) : null}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{/* Modal Konfirmasi Hapus */}
|
|
||||||
<ModalKonfirmasiHapus
|
<ModalKonfirmasiHapus
|
||||||
opened={modalHapus}
|
opened={modalHapus}
|
||||||
onClose={() => setModalHapus(false)}
|
onClose={() => setModalHapus(false)}
|
||||||
onConfirm={handleHapus}
|
onConfirm={handleHapus}
|
||||||
text='Apakah anda yakin ingin menghapus penghargaan ini?'
|
text="Apakah Anda yakin ingin menghapus penghargaan ini?"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,18 @@
|
|||||||
'use client'
|
'use client';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import ApiFetch from '@/lib/api-fetch';
|
import ApiFetch from '@/lib/api-fetch';
|
||||||
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Image,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
import { Dropzone } from '@mantine/dropzone';
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
@@ -11,74 +22,88 @@ import { useProxy } from 'valtio/utils';
|
|||||||
import CreateEditor from '../../../_com/createEditor';
|
import CreateEditor from '../../../_com/createEditor';
|
||||||
import penghargaanState from '../../../_state/desa/penghargaan';
|
import penghargaanState from '../../../_state/desa/penghargaan';
|
||||||
|
|
||||||
|
|
||||||
function CreatePenghargaan() {
|
function CreatePenghargaan() {
|
||||||
const statePenghargaan = useProxy(penghargaanState)
|
const statePenghargaan = useProxy(penghargaanState);
|
||||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
statePenghargaan.create.form = {
|
statePenghargaan.create.form = {
|
||||||
name: "",
|
name: '',
|
||||||
juara: "",
|
juara: '',
|
||||||
deskripsi: "",
|
deskripsi: '',
|
||||||
imageId: "",
|
imageId: '',
|
||||||
}
|
};
|
||||||
setPreviewImage(null)
|
setPreviewImage(null);
|
||||||
setFile(null)
|
setFile(null);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return toast.error("Silahkan pilih file gambar terlebih dahulu")
|
return toast.warn('Silakan pilih file gambar terlebih dahulu');
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await ApiFetch.api.fileStorage.create.post({
|
const res = await ApiFetch.api.fileStorage.create.post({
|
||||||
file: file,
|
file,
|
||||||
name: file.name
|
name: file.name,
|
||||||
})
|
});
|
||||||
|
|
||||||
|
const uploaded = res.data?.data;
|
||||||
|
|
||||||
const uploaded = res.data?.data
|
|
||||||
if (!uploaded?.id) {
|
if (!uploaded?.id) {
|
||||||
return toast.error("Gagal upload gambar")
|
return toast.error('Gagal mengunggah gambar, silakan coba lagi');
|
||||||
}
|
}
|
||||||
|
|
||||||
statePenghargaan.create.form.imageId = uploaded.id
|
statePenghargaan.create.form.imageId = uploaded.id;
|
||||||
|
|
||||||
await statePenghargaan.create.create()
|
await statePenghargaan.create.create();
|
||||||
resetForm()
|
resetForm();
|
||||||
router.push("/admin/desa/penghargaan")
|
router.push('/admin/desa/penghargaan');
|
||||||
|
};
|
||||||
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Header */}
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Tooltip>
|
||||||
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
|
<Title order={4} ml="sm" c="dark">
|
||||||
<Stack gap={"xs"}>
|
Tambah Penghargaan
|
||||||
<Title order={3}>Create Penghargaan</Title>
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{/* Form */}
|
||||||
|
<Paper
|
||||||
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
value={statePenghargaan.create.form.name}
|
value={statePenghargaan.create.form.name}
|
||||||
onChange={(val) => {
|
onChange={(val) => (statePenghargaan.create.form.name = val.target.value)}
|
||||||
statePenghargaan.create.form.name = val.target.value;
|
label={<Text fz="sm" fw="bold">Nama Penghargaan</Text>}
|
||||||
}}
|
placeholder="Masukkan nama penghargaan"
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Nama Penghargaan</Text>}
|
required
|
||||||
placeholder="masukkan nama penghargaan"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
value={statePenghargaan.create.form.juara}
|
value={statePenghargaan.create.form.juara}
|
||||||
onChange={(val) => {
|
onChange={(val) => (statePenghargaan.create.form.juara = val.target.value)}
|
||||||
statePenghargaan.create.form.juara = val.target.value;
|
label={<Text fz="sm" fw="bold">Juara</Text>}
|
||||||
}}
|
placeholder="Masukkan juara"
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Juara</Text>}
|
required
|
||||||
placeholder="masukkan juara"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
|
<Text fz="sm" fw="bold" mb={6}>Deskripsi</Text>
|
||||||
<CreateEditor
|
<CreateEditor
|
||||||
value={statePenghargaan.create.form.deskripsi}
|
value={statePenghargaan.create.form.deskripsi}
|
||||||
onChange={(htmlContent) => {
|
onChange={(htmlContent) => {
|
||||||
@@ -86,63 +111,67 @@ function CreatePenghargaan() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Dropzone Upload */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
<Text fz="sm" fw="bold" mb={6}>Gambar</Text>
|
||||||
<Box>
|
|
||||||
<Dropzone
|
<Dropzone
|
||||||
onDrop={(files) => {
|
onDrop={(files) => {
|
||||||
const selectedFile = files[0]; // Ambil file pertama
|
const selectedFile = files[0];
|
||||||
if (selectedFile) {
|
if (selectedFile) {
|
||||||
setFile(selectedFile);
|
setFile(selectedFile);
|
||||||
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
|
setPreviewImage(URL.createObjectURL(selectedFile));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onReject={() => toast.error('File tidak valid.')}
|
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
||||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
maxSize={5 * 1024 ** 2}
|
||||||
accept={{ 'image/*': [] }}
|
accept={{ 'image/*': [] }}
|
||||||
|
radius="md"
|
||||||
|
p="xl"
|
||||||
>
|
>
|
||||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
<Group justify="center" gap="xl" mih={180}>
|
||||||
<Dropzone.Accept>
|
<Dropzone.Accept>
|
||||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
<IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||||
</Dropzone.Accept>
|
</Dropzone.Accept>
|
||||||
<Dropzone.Reject>
|
<Dropzone.Reject>
|
||||||
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
<IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||||
</Dropzone.Reject>
|
</Dropzone.Reject>
|
||||||
<Dropzone.Idle>
|
<Dropzone.Idle>
|
||||||
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
<IconPhoto size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||||
</Dropzone.Idle>
|
</Dropzone.Idle>
|
||||||
|
|
||||||
<div>
|
|
||||||
<Text size="xl" inline>
|
|
||||||
Drag gambar ke sini atau klik untuk pilih file
|
|
||||||
</Text>
|
|
||||||
<Text size="sm" c="dimmed" inline mt={7}>
|
|
||||||
Maksimal 5MB dan harus format gambar
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</Group>
|
</Group>
|
||||||
|
<Text ta="center" mt="sm" size="sm" color="dimmed">
|
||||||
|
Seret gambar atau klik untuk memilih file (maks 5MB)
|
||||||
|
</Text>
|
||||||
</Dropzone>
|
</Dropzone>
|
||||||
|
|
||||||
{/* Tampilkan preview kalau ada */}
|
|
||||||
{previewImage && (
|
{previewImage && (
|
||||||
<Box mt="sm">
|
<Box mt="sm" style={{ textAlign: 'center' }}>
|
||||||
<Image
|
<Image
|
||||||
src={previewImage}
|
src={previewImage}
|
||||||
alt="Preview"
|
alt="Preview Gambar"
|
||||||
style={{
|
radius="md"
|
||||||
maxWidth: '100%',
|
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
|
||||||
maxHeight: '200px',
|
|
||||||
objectFit: 'contain',
|
|
||||||
borderRadius: '8px',
|
|
||||||
border: '1px solid #ddd',
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
</Box>
|
{/* Button Submit */}
|
||||||
</Box>
|
<Group justify="right">
|
||||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
|
<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>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -2,21 +2,38 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import penghargaanState from '@/app/admin/(dashboard)/_state/desa/penghargaan';
|
import penghargaanState from '@/app/admin/(dashboard)/_state/desa/penghargaan';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Image, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
import {
|
||||||
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
|
Box,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Group,
|
||||||
|
Pagination,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
TableTbody,
|
||||||
|
TableTd,
|
||||||
|
TableTh,
|
||||||
|
TableThead,
|
||||||
|
TableTr,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
Tooltip
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import HeaderSearch from '../../_com/header';
|
import HeaderSearch from '../../_com/header';
|
||||||
import JudulList from '../../_com/judulList';
|
|
||||||
|
|
||||||
function Penghargaan() {
|
function Penghargaan() {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
title='Penghargaan'
|
title="Penghargaan"
|
||||||
placeholder='pencarian'
|
placeholder="Cari nama atau deskripsi..."
|
||||||
searchIcon={<IconSearch size={20} />}
|
searchIcon={<IconSearch size={20} />}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
@@ -27,125 +44,114 @@ function Penghargaan() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ListPenghargaan({ search }: { search: string }) {
|
function ListPenghargaan({ search }: { search: string }) {
|
||||||
const state = useProxy(penghargaanState)
|
const state = useProxy(penghargaanState);
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
const {
|
const { data, page, totalPages, loading, load } = state.findMany;
|
||||||
data,
|
|
||||||
page,
|
|
||||||
totalPages,
|
|
||||||
loading,
|
|
||||||
load,
|
|
||||||
} = state.findMany;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
load(page, 10)
|
load(page, 10, search);
|
||||||
}, [])
|
}, [page, search]);
|
||||||
|
|
||||||
const filteredData = useMemo(() => {
|
const filteredData = data || []
|
||||||
if (!data) return [];
|
|
||||||
return data.filter(item => {
|
|
||||||
const keyword = search.toLowerCase();
|
|
||||||
return (
|
|
||||||
item.name?.toLowerCase().includes(keyword) ||
|
|
||||||
item.deskripsi?.toLowerCase().includes(keyword)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}, [data, search]);
|
|
||||||
|
|
||||||
// Handle loading state
|
// Loading state
|
||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton height={300} />
|
<Skeleton height={600} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.length === 0) {
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={10}>
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||||
<JudulList
|
<Group justify="space-between" mb="md">
|
||||||
title='List Penghargaan'
|
<Title order={4}>List Penghargaan</Title>
|
||||||
href='/admin/desa/penghargaan/create'
|
<Tooltip label="Tambah Penghargaan" withArrow>
|
||||||
/>
|
<Button
|
||||||
<Table striped withTableBorder withRowBorders>
|
leftSection={<IconPlus size={18} />}
|
||||||
|
color="blue"
|
||||||
|
variant="light"
|
||||||
|
onClick={() => router.push('/admin/desa/penghargaan/create')}
|
||||||
|
>
|
||||||
|
Tambah Baru
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
|
<Table highlightOnHover>
|
||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh>Nama</TableTh>
|
<TableTh style={{ width: '35%' }}>Nama</TableTh>
|
||||||
<TableTh>Deskripsi</TableTh>
|
<TableTh style={{ width: '35%' }}>Deskripsi</TableTh>
|
||||||
<TableTh>Image</TableTh>
|
<TableTh style={{ width: '30%' }}>Aksi</TableTh>
|
||||||
<TableTh>Detail</TableTh>
|
|
||||||
</TableTr>
|
</TableTr>
|
||||||
</TableThead>
|
</TableThead>
|
||||||
<TableTbody>
|
<TableTbody>
|
||||||
<TableTr>
|
{filteredData.length > 0 ? (
|
||||||
<TableTd colSpan={4}>
|
filteredData.map((item) => (
|
||||||
<Text ta="center">Tidak ada data</Text>
|
|
||||||
</TableTd>
|
|
||||||
</TableTr>
|
|
||||||
</TableTbody>
|
|
||||||
</Table>
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box py={10}>
|
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
|
||||||
<JudulList
|
|
||||||
title='List Penghargaan'
|
|
||||||
href='/admin/desa/penghargaan/create'
|
|
||||||
/>
|
|
||||||
<Table striped withTableBorder withRowBorders>
|
|
||||||
<TableThead>
|
|
||||||
<TableTr>
|
|
||||||
<TableTh>Nama</TableTh>
|
|
||||||
<TableTh>Deskripsi</TableTh>
|
|
||||||
<TableTh>Image</TableTh>
|
|
||||||
<TableTh>Detail</TableTh>
|
|
||||||
</TableTr>
|
|
||||||
</TableThead>
|
|
||||||
<TableTbody>
|
|
||||||
{filteredData.map((item) => (
|
|
||||||
<TableTr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Box w={100}>
|
<Box w={200}>
|
||||||
<Text lineClamp={1} truncate="end" fz={"sm"}>{item.name}</Text>
|
<Text fw={500} truncate="end" lineClamp={1}>
|
||||||
</Box>
|
{item.name}
|
||||||
</TableTd>
|
|
||||||
<TableTd>
|
|
||||||
<Box w={100}>
|
|
||||||
<Text lineClamp={1} truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
|
||||||
</Box>
|
|
||||||
</TableTd>
|
|
||||||
<TableTd>
|
|
||||||
<Image w={100} src={item.image?.link} alt="gambar" />
|
|
||||||
</TableTd>
|
|
||||||
<TableTd>
|
|
||||||
<Text>
|
|
||||||
<Button onClick={() => router.push(`/admin/desa/penghargaan/${item.id}`)}>
|
|
||||||
<IconDeviceImac size={20} />
|
|
||||||
</Button>
|
|
||||||
</Text>
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Box w={200}>
|
||||||
|
<Text
|
||||||
|
truncate="end"
|
||||||
|
lineClamp={1}
|
||||||
|
fz="sm"
|
||||||
|
c="dimmed"
|
||||||
|
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
onClick={() =>
|
||||||
|
router.push(`/admin/desa/penghargaan/${item.id}`)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<IconDeviceImac size={20} />
|
||||||
|
<Text ml={5}>Detail</Text>
|
||||||
|
</Button>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))}
|
))
|
||||||
|
) : (
|
||||||
|
<TableTr>
|
||||||
|
<TableTd colSpan={4}>
|
||||||
|
<Center py={20}>
|
||||||
|
<Text color="dimmed">
|
||||||
|
Tidak ada data penghargaan yang cocok
|
||||||
|
</Text>
|
||||||
|
</Center>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
)}
|
||||||
</TableTbody>
|
</TableTbody>
|
||||||
</Table>
|
</Table>
|
||||||
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
<Center>
|
<Center>
|
||||||
<Pagination
|
<Pagination
|
||||||
value={page}
|
value={page}
|
||||||
onChange={(newPage) => {
|
onChange={(newPage) => {
|
||||||
load(newPage, 10);
|
load(newPage, 10, search);
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
}}
|
}}
|
||||||
total={totalPages}
|
total={totalPages}
|
||||||
mt="md"
|
mt="md"
|
||||||
mb="md"
|
mb="md"
|
||||||
|
color="blue"
|
||||||
|
radius="md"
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
|
||||||
import { usePathname, useRouter } from 'next/navigation';
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { IconListDetails, IconCategory } from '@tabler/icons-react';
|
||||||
|
|
||||||
function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
|
function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -12,16 +13,21 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
|
|||||||
{
|
{
|
||||||
label: "List Pengumuman",
|
label: "List Pengumuman",
|
||||||
value: "listpengumuman",
|
value: "listpengumuman",
|
||||||
href: "/admin/desa/pengumuman/list-pengumuman"
|
href: "/admin/desa/pengumuman/list-pengumuman",
|
||||||
|
icon: <IconListDetails size={18} stroke={1.8} />,
|
||||||
|
tooltip: "Lihat semua daftar pengumuman"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Kategori Pengumuman",
|
label: "Kategori Pengumuman",
|
||||||
value: "kategoripengumuman",
|
value: "kategoripengumuman",
|
||||||
href: "/admin/desa/pengumuman/kategori-pengumuman"
|
href: "/admin/desa/pengumuman/kategori-pengumuman",
|
||||||
|
icon: <IconCategory size={18} stroke={1.8} />,
|
||||||
|
tooltip: "Kelola kategori pengumuman"
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const curentTab = tabs.find(tab => tab.href === pathname)
|
|
||||||
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
|
const currentTab = tabs.find(tab => tab.href === pathname)
|
||||||
|
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
|
||||||
|
|
||||||
const handleTabChange = (value: string | null) => {
|
const handleTabChange = (value: string | null) => {
|
||||||
const tab = tabs.find(t => t.value === value)
|
const tab = tabs.find(t => t.value === value)
|
||||||
@@ -39,22 +45,57 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
|
|||||||
}, [pathname])
|
}, [pathname])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack gap="lg">
|
||||||
<Title order={3}>Pengumuman</Title>
|
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Pengumuman</Title>
|
||||||
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
|
<Tabs
|
||||||
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
|
color={colors['blue-button']}
|
||||||
{tabs.map((e, i) => (
|
variant='pills'
|
||||||
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
|
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 transitionProps={{ transition: 'pop', duration: 200 }}>
|
||||||
|
<TabsTab
|
||||||
|
value={tab.value}
|
||||||
|
leftSection={tab.icon}
|
||||||
|
style={{
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
transition: "all 0.2s ease",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</TabsTab>
|
||||||
|
</Tooltip>
|
||||||
))}
|
))}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
{tabs.map((e, i) => (
|
|
||||||
<TabsPanel key={i} value={e.value}>
|
{tabs.map((tab, i) => (
|
||||||
{/* Konten dummy, bisa diganti tergantung routing */}
|
<TabsPanel
|
||||||
<></>
|
key={i}
|
||||||
|
value={tab.value}
|
||||||
|
style={{
|
||||||
|
padding: "1.5rem",
|
||||||
|
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
|
||||||
|
borderRadius: "1rem",
|
||||||
|
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Konten dummy, bisa diganti sesuai routing */}
|
||||||
|
<>{children}</>
|
||||||
</TabsPanel>
|
</TabsPanel>
|
||||||
))}
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
{children}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,18 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client';
|
||||||
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
|
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
Text,
|
||||||
|
} from '@mantine/core';
|
||||||
import { IconArrowBack } from '@tabler/icons-react';
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
@@ -10,9 +20,10 @@ import { toast } from 'react-toastify';
|
|||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
function EditKategoriPengumuman() {
|
function EditKategoriPengumuman() {
|
||||||
const editState = useProxy(stateDesaPengumuman.category)
|
const editState = useProxy(stateDesaPengumuman.category);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: editState.update.form.name || '',
|
name: editState.update.form.name || '',
|
||||||
});
|
});
|
||||||
@@ -23,15 +34,15 @@ function EditKategoriPengumuman() {
|
|||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await editState.update.load(id); // akses langsung, bukan dari proxy
|
const data = await editState.update.load(id);
|
||||||
if (data) {
|
if (data) {
|
||||||
setFormData({
|
setFormData({
|
||||||
name: data.name || '',
|
name: data.name || '',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading kategori Pengumuman:", error);
|
console.error('Error loading kategori Pengumuman:', error);
|
||||||
toast.error("Gagal memuat data kategori Pengumuman");
|
toast.error('Gagal memuat data kategori Pengumuman');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -44,6 +55,7 @@ function EditKategoriPengumuman() {
|
|||||||
...editState.update.form,
|
...editState.update.form,
|
||||||
name: formData.name,
|
name: formData.name,
|
||||||
};
|
};
|
||||||
|
|
||||||
await editState.update.update();
|
await editState.update.update();
|
||||||
toast.success('Kategori Pengumuman berhasil diperbarui!');
|
toast.success('Kategori Pengumuman berhasil diperbarui!');
|
||||||
router.push('/admin/desa/pengumuman/kategori-pengumuman');
|
router.push('/admin/desa/pengumuman/kategori-pengumuman');
|
||||||
@@ -54,23 +66,62 @@ function EditKategoriPengumuman() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Header */}
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors["blue-button"]} size={30} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
p="xs"
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Tooltip>
|
||||||
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
|
<Title order={4} ml="sm" c="dark">
|
||||||
<Stack gap={"xs"}>
|
Edit Kategori Pengumuman
|
||||||
<Title order={3}>Edit Kategori Pengumuman</Title>
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{/* Form */}
|
||||||
|
<Paper
|
||||||
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label={
|
||||||
|
<Text fz="sm" fw="bold">
|
||||||
|
Nama Kategori Pengumuman
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
placeholder="Masukkan nama kategori Pengumuman"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
onChange={(e) =>
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Nama Kategori Pengumuman</Text>}
|
setFormData({ ...formData, name: e.target.value })
|
||||||
placeholder="masukkan nama kategori Pengumuman"
|
}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button onClick={handleSubmit}>Simpan</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>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,50 +1,87 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
|
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
import { IconArrowBack } from '@tabler/icons-react';
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function CreateKategoriPengumuman() {
|
function CreateKategoriPengumuman() {
|
||||||
const createState = useProxy(stateDesaPengumuman.category)
|
const createState = useProxy(stateDesaPengumuman.category);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
createState.create.form = {
|
createState.create.form = {
|
||||||
name: "",
|
name: '',
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
await createState.create.create();
|
await createState.create.create();
|
||||||
resetForm();
|
resetForm();
|
||||||
router.push("/admin/desa/pengumuman/kategori-pengumuman")
|
router.push('/admin/desa/pengumuman/kategori-pengumuman');
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Header dengan back button */}
|
||||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
p="xs"
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Tooltip>
|
||||||
|
<Title order={4} ml="sm" c="dark">
|
||||||
|
Tambah Kategori Pengumuman
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
{/* Form utama */}
|
||||||
<Stack gap={"xs"}>
|
<Paper
|
||||||
<Title order={4}>Create Kategori Pengumuman</Title>
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Pengumuman</Text>}
|
label={<Text fw="bold" fz="sm">Nama Kategori Pengumuman</Text>}
|
||||||
placeholder='Masukkan nama kategori Pengumuman'
|
placeholder="Masukkan nama kategori pengumuman"
|
||||||
value={createState.create.form.name}
|
value={createState.create.form.name || ''}
|
||||||
onChange={(val) => {
|
onChange={(e) => (createState.create.form.name = e.target.value)}
|
||||||
createState.create.form.name = val.target.value;
|
required
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<Group>
|
|
||||||
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
|
color: '#fff',
|
||||||
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -1,25 +1,26 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
import {
|
||||||
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
|
Box, Button, Center, Paper, Skeleton, Stack,
|
||||||
|
Table, TableTbody, TableTd, TableTh, TableThead, TableTr,
|
||||||
|
Text, Title, Tooltip, Pagination
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { IconEdit, IconSearch, IconTrash, IconPlus } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import HeaderSearch from '../../../_com/header';
|
import HeaderSearch from '../../../_com/header';
|
||||||
import JudulList from '../../../_com/judulList';
|
|
||||||
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||||
import stateDesaPengumuman from '../../../_state/desa/pengumuman';
|
import stateDesaPengumuman from '../../../_state/desa/pengumuman';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function KategoriPengumuman() {
|
function KategoriPengumuman() {
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
title='Kategori Pengumuman'
|
title='Kategori Pengumuman'
|
||||||
placeholder='pencarian'
|
placeholder='Cari nama kategori...'
|
||||||
searchIcon={<IconSearch size={20} />}
|
searchIcon={<IconSearch size={20} />}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
@@ -34,69 +35,84 @@ function ListKategoriPengumuman({ search }: { search: string }) {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [modalHapus, setModalHapus] = useState(false)
|
const [modalHapus, setModalHapus] = useState(false)
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||||
|
const { data, page, totalPages, loading, load } = listDataState.findMany;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
listDataState.findMany.load()
|
load(1, 10, search)
|
||||||
}, [])
|
}, [search])
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
if (selectedId) {
|
if (selectedId) {
|
||||||
listDataState.delete.delete(selectedId)
|
listDataState.delete.delete(selectedId)
|
||||||
setModalHapus(false)
|
setModalHapus(false)
|
||||||
setSelectedId(null)
|
setSelectedId(null)
|
||||||
|
load(page, 10, search)
|
||||||
listDataState.findMany.load()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredData = (listDataState.findMany.data || []).filter(item => {
|
const filteredData = data || []
|
||||||
const keyword = search.toLowerCase();
|
|
||||||
return (
|
|
||||||
item.name.toLowerCase().includes(keyword)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!listDataState.findMany.data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton h={500} />
|
<Skeleton height={500} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={10}>
|
||||||
<Paper bg={colors['white-1']} p="md">
|
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||||
<Stack>
|
<Stack>
|
||||||
<JudulList
|
<Box style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 15 }}>
|
||||||
title='List Kategori Pengumuman'
|
<Title order={4}>List Kategori Pengumuman</Title>
|
||||||
href='/admin/desa/pengumuman/kategori-pengumuman/create'
|
<Tooltip label="Tambah Kategori Pengumuman" withArrow>
|
||||||
/>
|
<Button
|
||||||
|
leftSection={<IconPlus size={18} />}
|
||||||
|
color="blue"
|
||||||
|
variant="light"
|
||||||
|
onClick={() => router.push('/admin/desa/pengumuman/kategori-pengumuman/create')}
|
||||||
|
>
|
||||||
|
Tambah Baru
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Box style={{ overflowX: 'auto' }}>
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
|
<Table highlightOnHover striped withRowBorders style={{ minWidth: '700px' }}>
|
||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh>No</TableTh>
|
<TableTh style={{ width: '10%' }}>No</TableTh>
|
||||||
<TableTh>Nama</TableTh>
|
<TableTh style={{ width: '60%' }}>Nama</TableTh>
|
||||||
<TableTh>Edit</TableTh>
|
<TableTh style={{ width: '15%' }}>Edit</TableTh>
|
||||||
<TableTh>Hapus</TableTh>
|
<TableTh style={{ width: '15%' }}>Hapus</TableTh>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
</TableThead>
|
</TableThead>
|
||||||
<TableTbody>
|
<TableTbody>
|
||||||
{filteredData.map((item, index) => (
|
{filteredData.length > 0 ? (
|
||||||
|
filteredData.map((item, index) => (
|
||||||
<TableTr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Box w={100}>
|
<Text fz="sm">{(page - 1) * 10 + index + 1}</Text>
|
||||||
<Text truncate="end" fz={"sm"}>{index + 1}</Text>
|
|
||||||
</Box>
|
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd>{item.name}</TableTd>
|
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Button color='green' onClick={() => router.push(`/admin/desa/pengumuman/kategori-pengumuman/${item.id}`)}>
|
<Text truncate lineClamp={1}>{item.name}</Text>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Tooltip label="Edit Kategori Pengumuman" withArrow>
|
||||||
|
<Button
|
||||||
|
variant='light'
|
||||||
|
color='green'
|
||||||
|
onClick={() => router.push(`/admin/desa/pengumuman/kategori-pengumuman/${item.id}`)}
|
||||||
|
>
|
||||||
<IconEdit size={20} />
|
<IconEdit size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
|
<Tooltip label="Hapus Kategori Pengumuman" withArrow>
|
||||||
<Button
|
<Button
|
||||||
|
variant='light'
|
||||||
color='red'
|
color='red'
|
||||||
disabled={listDataState.delete.loading}
|
disabled={listDataState.delete.loading}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -105,16 +121,35 @@ function ListKategoriPengumuman({ search }: { search: string }) {
|
|||||||
}}>
|
}}>
|
||||||
<IconTrash size={20} />
|
<IconTrash size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))}
|
))
|
||||||
|
) : (
|
||||||
|
<TableTr>
|
||||||
|
<TableTd colSpan={4}>
|
||||||
|
<Center py={20}>
|
||||||
|
<Text c="dimmed">Tidak ada data kategori pengumuman yang cocok</Text>
|
||||||
|
</Center>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
)}
|
||||||
</TableTbody>
|
</TableTbody>
|
||||||
</Table>
|
</Table>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{/* Modal Konfirmasi Hapus */}
|
<Center mt="md">
|
||||||
|
<Pagination
|
||||||
|
value={page}
|
||||||
|
onChange={(newPage) => load(newPage, 10, search)}
|
||||||
|
total={totalPages}
|
||||||
|
color="blue"
|
||||||
|
radius="md"
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
|
||||||
<ModalKonfirmasiHapus
|
<ModalKonfirmasiHapus
|
||||||
opened={modalHapus}
|
opened={modalHapus}
|
||||||
onClose={() => setModalHapus(false)}
|
onClose={() => setModalHapus(false)}
|
||||||
|
|||||||
@@ -7,12 +7,14 @@ import colors from "@/con/colors";
|
|||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
|
Group,
|
||||||
Paper,
|
Paper,
|
||||||
Select,
|
Select,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title
|
Title,
|
||||||
|
Tooltip,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { IconArrowBack } from "@tabler/icons-react";
|
import { IconArrowBack } from "@tabler/icons-react";
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
@@ -20,34 +22,34 @@ import { useEffect, useState } from "react";
|
|||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { useProxy } from "valtio/utils";
|
import { useProxy } from "valtio/utils";
|
||||||
|
|
||||||
|
|
||||||
function EditPengumuman() {
|
function EditPengumuman() {
|
||||||
const editState = useProxy(stateDesaPengumuman);
|
const editState = useProxy(stateDesaPengumuman);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
judul: editState.pengumuman.edit.form.judul || '',
|
judul: editState.pengumuman.edit.form.judul || "",
|
||||||
deskripsi: editState.pengumuman.edit.form.deskripsi || '',
|
deskripsi: editState.pengumuman.edit.form.deskripsi || "",
|
||||||
categoryPengumumanId: editState.pengumuman.edit.form.categoryPengumumanId || '',
|
categoryPengumumanId:
|
||||||
content: editState.pengumuman.edit.form.content || ''
|
editState.pengumuman.edit.form.categoryPengumumanId || "",
|
||||||
|
content: editState.pengumuman.edit.form.content || "",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load pengumuman by id saat pertama kali
|
// Load pengumuman by id saat pertama kali
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
editState.category.findMany.load()
|
editState.category.findMany.load();
|
||||||
const loadpengumuman = async () => {
|
const loadpengumuman = async () => {
|
||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await stateDesaPengumuman.pengumuman.edit.load(id); // akses langsung, bukan dari proxy
|
const data = await stateDesaPengumuman.pengumuman.edit.load(id);
|
||||||
if (data) {
|
if (data) {
|
||||||
setFormData({
|
setFormData({
|
||||||
judul: data.judul || '',
|
judul: data.judul || "",
|
||||||
deskripsi: data.deskripsi || '',
|
deskripsi: data.deskripsi || "",
|
||||||
categoryPengumumanId: data.categoryPengumumanId || '',
|
categoryPengumumanId: data.categoryPengumumanId || "",
|
||||||
content: data.content || '',
|
content: data.content || "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -57,21 +59,18 @@ function EditPengumuman() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
loadpengumuman();
|
loadpengumuman();
|
||||||
}, [params?.id]); // ✅ hapus editState dari dependency
|
}, [params?.id]);
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// edit global state with form data
|
// update global state
|
||||||
editState.pengumuman.edit.form = {
|
editState.pengumuman.edit.form = {
|
||||||
...editState.pengumuman.edit.form,
|
...editState.pengumuman.edit.form,
|
||||||
judul: formData.judul,
|
...formData,
|
||||||
deskripsi: formData.deskripsi,
|
|
||||||
content: formData.content,
|
|
||||||
categoryPengumumanId: formData.categoryPengumumanId || ''
|
|
||||||
};
|
};
|
||||||
|
|
||||||
await editState.pengumuman.edit.update();
|
await editState.pengumuman.edit.update();
|
||||||
toast.success("pengumuman berhasil diperbarui!");
|
toast.success("Pengumuman berhasil diperbarui!");
|
||||||
router.push("/admin/desa/pengumuman/list-pengumuman");
|
router.push("/admin/desa/pengumuman/list-pengumuman");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating pengumuman:", error);
|
console.error("Error updating pengumuman:", error);
|
||||||
@@ -80,57 +79,97 @@ function EditPengumuman() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: "sm", md: "lg" }} py="md">
|
||||||
<Box mb={10}>
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
<IconArrowBack color={colors["blue-button"]} size={30} />
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
p="xs"
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
|
<IconArrowBack color={colors["blue-button"]} size={24} />
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Tooltip>
|
||||||
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
|
<Title order={4} ml="sm" c="dark">
|
||||||
<Stack gap={"xs"}>
|
Edit Pengumuman
|
||||||
<Title order={3}>Edit pengumuman</Title>
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Paper
|
||||||
|
w={{ base: "100%", md: "50%" }}
|
||||||
|
bg={colors["white-1"]}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: "1px solid #e0e0e0" }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label="Judul Pengumuman"
|
||||||
|
placeholder="Masukkan judul"
|
||||||
value={formData.judul}
|
value={formData.judul}
|
||||||
onChange={(e) => setFormData({ ...formData, judul: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, judul: e.target.value })}
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
|
required
|
||||||
placeholder="masukkan judul"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label="Deskripsi Singkat"
|
||||||
|
placeholder="Masukkan deskripsi"
|
||||||
value={formData.deskripsi}
|
value={formData.deskripsi}
|
||||||
onChange={(e) => setFormData({ ...formData, deskripsi: e.target.value })}
|
onChange={(e) =>
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>}
|
setFormData({ ...formData, deskripsi: e.target.value })
|
||||||
placeholder="masukkan deskripsi"
|
}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<Box>
|
|
||||||
<Text fz={"sm"} fw={"bold"}>Konten</Text>
|
|
||||||
<EditEditor
|
|
||||||
value={formData.content}
|
|
||||||
onChange={(htmlContent) => {
|
|
||||||
setFormData((prev) => ({ ...prev, content: htmlContent }));
|
|
||||||
editState.pengumuman.edit.form.content = htmlContent;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
value={formData.categoryPengumumanId}
|
value={formData.categoryPengumumanId}
|
||||||
onChange={(val) => setFormData({ ...formData, categoryPengumumanId: val || "" })}
|
onChange={(val) =>
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>}
|
setFormData({ ...formData, categoryPengumumanId: val || "" })
|
||||||
placeholder='Pilih kategori'
|
}
|
||||||
|
label="Kategori"
|
||||||
|
placeholder="Pilih kategori"
|
||||||
data={
|
data={
|
||||||
editState.category.findMany.data?.map((v) => ({
|
editState.category.findMany.data?.map((v) => ({
|
||||||
value: v.id,
|
value: v.id,
|
||||||
label: v.name
|
label: v.name,
|
||||||
})) || []
|
})) || []
|
||||||
}
|
}
|
||||||
clearable
|
clearable
|
||||||
searchable
|
searchable
|
||||||
required
|
required
|
||||||
error={!formData.categoryPengumumanId ? "Pilih kategori" : undefined}
|
error={
|
||||||
|
!formData.categoryPengumumanId ? "Pilih kategori" : undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button onClick={handleSubmit}>Edit pengumuman</Button>
|
<Box>
|
||||||
|
<Text fz="sm" fw="bold" mb={6}>
|
||||||
|
Konten Lengkap
|
||||||
|
</Text>
|
||||||
|
<EditEditor
|
||||||
|
value={formData.content}
|
||||||
|
onChange={(htmlContent) =>
|
||||||
|
setFormData({ ...formData, content: htmlContent })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<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>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,116 +1,163 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
|
||||||
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { useProxy } from 'valtio/utils';
|
import {
|
||||||
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
|
||||||
|
import { useRouter, useParams } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { useParams } from 'next/navigation';
|
|
||||||
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||||
|
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
|
||||||
|
|
||||||
|
export default function DetailPengumuman() {
|
||||||
function DetailPengumuman() {
|
const pengumumanState = useProxy(stateDesaPengumuman);
|
||||||
const pengumumanState = useProxy(stateDesaPengumuman)
|
const [modalHapus, setModalHapus] = useState(false);
|
||||||
const [modalHapus, setModalHapus] = useState(false)
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
const params = useParams();
|
||||||
const params = useParams()
|
const router = useRouter();
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
pengumumanState.pengumuman.findUnique.load(params?.id as string)
|
pengumumanState.pengumuman.findUnique.load(params?.id as string);
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
const handleHapus = () => {
|
const handleHapus = () => {
|
||||||
if (selectedId) {
|
if (selectedId) {
|
||||||
pengumumanState.pengumuman.delete.byId(selectedId)
|
pengumumanState.pengumuman.delete.byId(selectedId);
|
||||||
setModalHapus(false)
|
setModalHapus(false);
|
||||||
setSelectedId(null)
|
setSelectedId(null);
|
||||||
router.push("/admin/desa/pengumuman/list-pengumuman")
|
router.push('/admin/desa/pengumuman/list-pengumuman');
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (!pengumumanState.pengumuman.findUnique.data) {
|
if (!pengumumanState.pengumuman.findUnique.data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton h={400} />
|
<Skeleton height={500} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = pengumumanState.pengumuman.findUnique.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box py={10}>
|
||||||
<Box mb={10}>
|
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
|
|
||||||
<Stack>
|
|
||||||
<Text fz={"xl"} fw={"bold"}>Detail Pengumuman</Text>
|
|
||||||
{pengumumanState.pengumuman.findUnique.data ? (
|
|
||||||
<Paper key={pengumumanState.pengumuman.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
|
||||||
<Stack gap={"xs"}>
|
|
||||||
<Box>
|
|
||||||
<Text fw={"bold"} fz={"lg"}>Kategori</Text>
|
|
||||||
<Text fz={"lg"}>{pengumumanState.pengumuman.findUnique.data?.CategoryPengumuman?.name}</Text>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text fw={"bold"} fz={"lg"}>Judul</Text>
|
|
||||||
<Text fz={"lg"}>{pengumumanState.pengumuman.findUnique.data?.judul}</Text>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
|
|
||||||
<Text fz={"lg"}>{pengumumanState.pengumuman.findUnique.data?.deskripsi}</Text>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text fw={"bold"} fz={"lg"}>Konten</Text>
|
|
||||||
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: pengumumanState.pengumuman.findUnique.data?.content }} />
|
|
||||||
</Box>
|
|
||||||
<Flex gap={"xs"} mt={10}>
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
variant="subtle"
|
||||||
if (pengumumanState.pengumuman.findUnique.data) {
|
onClick={() => router.back()}
|
||||||
setSelectedId(pengumumanState.pengumuman.findUnique.data.id);
|
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
|
||||||
setModalHapus(true);
|
mb={15}
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={pengumumanState.pengumuman.delete.loading || !pengumumanState.pengumuman.findUnique.data}
|
|
||||||
color={"red"}
|
|
||||||
>
|
>
|
||||||
<IconX size={20} />
|
Kembali
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
|
||||||
onClick={() => {
|
<Paper
|
||||||
if (pengumumanState.pengumuman.findUnique.data) {
|
withBorder
|
||||||
router.push(`/admin/desa/pengumuman/list-pengumuman/${pengumumanState.pengumuman.findUnique.data.id}/edit`);
|
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 Pengumuman
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">
|
||||||
|
Kategori
|
||||||
|
</Text>
|
||||||
|
<Text fz="md" c="dimmed">
|
||||||
|
{data?.CategoryPengumuman?.name || '-'}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">
|
||||||
|
Judul
|
||||||
|
</Text>
|
||||||
|
<Text fz="md" c="dimmed">
|
||||||
|
{data?.judul || '-'}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">
|
||||||
|
Deskripsi
|
||||||
|
</Text>
|
||||||
|
<Text fz="md" c="dimmed">
|
||||||
|
{data?.deskripsi || '-'}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">
|
||||||
|
Konten
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
fz="md"
|
||||||
|
c="dimmed"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: data?.content || '-',
|
||||||
}}
|
}}
|
||||||
disabled={!pengumumanState.pengumuman.findUnique.data}
|
/>
|
||||||
color={"green"}
|
</Box>
|
||||||
|
|
||||||
|
<Group gap="sm">
|
||||||
|
<Tooltip label="Hapus Pengumuman" 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 Pengumuman" withArrow position="top">
|
||||||
|
<Button
|
||||||
|
color="green"
|
||||||
|
onClick={() =>
|
||||||
|
router.push(
|
||||||
|
`/admin/desa/pengumuman/list-pengumuman/${data.id}/edit`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
variant="light"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
>
|
>
|
||||||
<IconEdit size={20} />
|
<IconEdit size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
) : null}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{/* Modal Konfirmasi Hapus */}
|
|
||||||
<ModalKonfirmasiHapus
|
<ModalKonfirmasiHapus
|
||||||
opened={modalHapus}
|
opened={modalHapus}
|
||||||
onClose={() => setModalHapus(false)}
|
onClose={() => setModalHapus(false)}
|
||||||
onConfirm={handleHapus}
|
onConfirm={handleHapus}
|
||||||
text='Apakah anda yakin ingin menghapus pengumuman ini?'
|
text="Apakah anda yakin ingin menghapus pengumuman ini?"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DetailPengumuman;
|
|
||||||
@@ -1,79 +1,110 @@
|
|||||||
'use client'
|
'use client';
|
||||||
|
|
||||||
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
||||||
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
|
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
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 { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconArrowBack } from '@tabler/icons-react';
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
|
|
||||||
function CreatePengumuman() {
|
function CreatePengumuman() {
|
||||||
const pengumumanState = useProxy(stateDesaPengumuman)
|
const pengumumanState = useProxy(stateDesaPengumuman);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
pengumumanState.category.findMany.load()
|
pengumumanState.category.findMany.load();
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
await pengumumanState.pengumuman.create.create()
|
await pengumumanState.pengumuman.create.create();
|
||||||
resetForm()
|
resetForm();
|
||||||
router.push("/admin/desa/pengumuman/list-pengumuman")
|
router.push('/admin/desa/pengumuman/list-pengumuman');
|
||||||
}
|
};
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
pengumumanState.pengumuman.create.form = {
|
pengumumanState.pengumuman.create.form = {
|
||||||
judul: "",
|
judul: '',
|
||||||
deskripsi: "",
|
deskripsi: '',
|
||||||
content: "",
|
content: '',
|
||||||
categoryPengumumanId: "",
|
categoryPengumumanId: '',
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Box mb={10}>
|
|
||||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
return (
|
||||||
<Stack gap={"xs"}>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Title order={4}>Create Pengumuman</Title>
|
{/* Header */}
|
||||||
|
<Group mb="md">
|
||||||
|
<Tooltip label="Kembali ke halaman sebelumnya" 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 Pengumuman
|
||||||
|
</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">
|
||||||
|
{/* Judul */}
|
||||||
<TextInput
|
<TextInput
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>}
|
value={pengumumanState.pengumuman.create.form.judul}
|
||||||
placeholder='Masukkan judul'
|
onChange={(val) => (pengumumanState.pengumuman.create.form.judul = val.target.value)}
|
||||||
onChange={(val) => {
|
label={<Text fz="sm" fw="bold">Judul</Text>}
|
||||||
pengumumanState.pengumuman.create.form.judul = val.target.value
|
placeholder="Masukkan judul pengumuman"
|
||||||
}}
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Kategori */}
|
||||||
<Select
|
<Select
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>}
|
label={<Text fz="sm" fw="bold">Kategori</Text>}
|
||||||
placeholder='Pilih kategori'
|
placeholder="Pilih kategori"
|
||||||
|
value={pengumumanState.pengumuman.create.form.categoryPengumumanId || ""}
|
||||||
|
onChange={(val) => {
|
||||||
|
pengumumanState.pengumuman.create.form.categoryPengumumanId = val ?? "";
|
||||||
|
}}
|
||||||
data={pengumumanState.category.findMany.data?.map((item) => ({
|
data={pengumumanState.category.findMany.data?.map((item) => ({
|
||||||
label: item.name,
|
label: item.name,
|
||||||
value: item.id,
|
value: item.id,
|
||||||
}))}
|
}))}
|
||||||
onChange={(val) => {
|
|
||||||
const selected = pengumumanState.category.findMany.data?.find((item) => item.id === val);
|
|
||||||
if (selected) {
|
|
||||||
pengumumanState.pengumuman.create.form.categoryPengumumanId = selected.id;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
searchable
|
searchable
|
||||||
nothingFoundMessage="Tidak ditemukan"
|
nothingFoundMessage="Tidak ditemukan"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Deskripsi Singkat */}
|
||||||
<TextInput
|
<TextInput
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Deskripsi Singkat</Text>}
|
value={pengumumanState.pengumuman.create.form.deskripsi}
|
||||||
placeholder='Masukkan deskripsi singkat'
|
onChange={(val) => (pengumumanState.pengumuman.create.form.deskripsi = val.target.value)}
|
||||||
onChange={(val) => {
|
label={<Text fz="sm" fw="bold">Deskripsi Singkat</Text>}
|
||||||
pengumumanState.pengumuman.create.form.deskripsi = val.target.value
|
placeholder="Masukkan deskripsi singkat"
|
||||||
}}
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Konten Editor */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
|
<Text fz="sm" fw="bold" mb={6}>
|
||||||
|
Konten Lengkap
|
||||||
|
</Text>
|
||||||
<CreateEditor
|
<CreateEditor
|
||||||
value={pengumumanState.pengumuman.create.form.content}
|
value={pengumumanState.pengumuman.create.form.content}
|
||||||
onChange={(htmlContent) => {
|
onChange={(htmlContent) => {
|
||||||
@@ -82,8 +113,20 @@ function CreatePengumuman() {
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Group>
|
{/* Tombol Submit */}
|
||||||
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
|
color: '#fff',
|
||||||
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -1,6 +1,25 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Grid, GridCol, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Group,
|
||||||
|
Pagination,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
TableTbody,
|
||||||
|
TableTd,
|
||||||
|
TableTh,
|
||||||
|
TableThead,
|
||||||
|
TableTr,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
Tooltip
|
||||||
|
} from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
|
import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
@@ -9,14 +28,13 @@ import { useProxy } from 'valtio/utils';
|
|||||||
import HeaderSearch from '../../../_com/header';
|
import HeaderSearch from '../../../_com/header';
|
||||||
import stateDesaPengumuman from '../../../_state/desa/pengumuman';
|
import stateDesaPengumuman from '../../../_state/desa/pengumuman';
|
||||||
|
|
||||||
|
|
||||||
function Pengumuman() {
|
function Pengumuman() {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
title='List Pengumuman'
|
title="Pengumuman Desa"
|
||||||
placeholder='pencarian'
|
placeholder="Cari judul atau kategori..."
|
||||||
searchIcon={<IconSearch size={20} />}
|
searchIcon={<IconSearch size={20} />}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
@@ -27,86 +45,107 @@ function Pengumuman() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ListPengumuman({ search }: { search: string }) {
|
function ListPengumuman({ search }: { search: string }) {
|
||||||
const pengumumanState = useProxy(stateDesaPengumuman)
|
const pengumumanState = useProxy(stateDesaPengumuman);
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
const {
|
|
||||||
data,
|
const { data, page, totalPages, loading, load } = pengumumanState.pengumuman.findMany;
|
||||||
page,
|
|
||||||
totalPages,
|
|
||||||
loading,
|
|
||||||
load,
|
|
||||||
} = pengumumanState.pengumuman.findMany;
|
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
load(page, 10, search)
|
load(page, 10, search);
|
||||||
}, [page, search])
|
}, [page, search]);
|
||||||
|
|
||||||
const filteredData = (data || [])
|
const filteredData = data || [];
|
||||||
|
|
||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton h={500} />
|
<Skeleton height={600} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={10}>
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||||
<Stack>
|
<Group justify="space-between" mb="md">
|
||||||
<Grid>
|
<Title order={4}>Daftar Pengumuman</Title>
|
||||||
<GridCol span={{ base: 12, md: 11 }}>
|
<Tooltip label="Tambah Pengumuman" withArrow>
|
||||||
<Text fz={"xl"} fw={"bold"}>List Pengumuman</Text>
|
<Button
|
||||||
</GridCol>
|
leftSection={<IconCircleDashedPlus size={18} />}
|
||||||
<GridCol span={{ base: 12, md: 1 }}>
|
color="blue"
|
||||||
<Button onClick={() => router.push("/admin/desa/pengumuman/list-pengumuman/create")} bg={colors['blue-button']}>
|
variant="light"
|
||||||
<IconCircleDashedPlus size={25} />
|
onClick={() => router.push('/admin/desa/pengumuman/list-pengumuman/create')}
|
||||||
|
>
|
||||||
|
Tambah Baru
|
||||||
</Button>
|
</Button>
|
||||||
</GridCol>
|
</Tooltip>
|
||||||
</Grid>
|
</Group>
|
||||||
<Box style={{ overflowX: "auto" }}>
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
|
<Table highlightOnHover style={{ minWidth: '700px' }}>
|
||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh w={250}>Judul</TableTh>
|
<TableTh style={{ width: '40%' }}>Judul</TableTh>
|
||||||
<TableTh w={250}>Kategori</TableTh>
|
<TableTh style={{ width: '30%' }}>Kategori</TableTh>
|
||||||
<TableTh w={200}>Detail</TableTh>
|
<TableTh style={{ width: '20%' }}>Detail</TableTh>
|
||||||
|
|
||||||
</TableTr>
|
</TableTr>
|
||||||
</TableThead>
|
</TableThead>
|
||||||
<TableTbody>
|
<TableTbody>
|
||||||
{filteredData.map((item) => (
|
{filteredData.length > 0 ? (
|
||||||
|
filteredData.map((item) => (
|
||||||
<TableTr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Box w={100}>
|
<Text fw={500} truncate="end" lineClamp={1}>
|
||||||
<Text truncate="end" fz={"sm"}>{item.judul}</Text>
|
{item.judul}
|
||||||
</Box>
|
</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd >{item.CategoryPengumuman?.name}</TableTd>
|
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Button bg={"green"} onClick={() => router.push(`/admin/desa/pengumuman/list-pengumuman/${item.id}`)}>
|
<Text fz="sm" c="dimmed">
|
||||||
<IconDeviceImacCog size={25} />
|
{item.CategoryPengumuman?.name || '-'}
|
||||||
|
</Text>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
onClick={() =>
|
||||||
|
router.push(`/admin/desa/pengumuman/list-pengumuman/${item.id}`)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<IconDeviceImacCog size={20} />
|
||||||
|
<Text ml={5}>Detail</Text>
|
||||||
</Button>
|
</Button>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))}
|
))
|
||||||
|
) : (
|
||||||
|
<TableTr>
|
||||||
|
<TableTd colSpan={3}>
|
||||||
|
<Center py={20}>
|
||||||
|
<Text color="dimmed">Tidak ada pengumuman yang cocok</Text>
|
||||||
|
</Center>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
)}
|
||||||
</TableTbody>
|
</TableTbody>
|
||||||
</Table>
|
</Table>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
|
||||||
</Paper>
|
</Paper>
|
||||||
<Center>
|
<Center>
|
||||||
<Pagination
|
<Pagination
|
||||||
value={page}
|
value={page}
|
||||||
onChange={(newPage) => load(newPage)}
|
onChange={(newPage) => {
|
||||||
|
load(newPage, 10);
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
}}
|
||||||
total={totalPages}
|
total={totalPages}
|
||||||
mt="md"
|
mt="md"
|
||||||
mb="md"
|
mb="md"
|
||||||
|
color="blue"
|
||||||
|
radius="md"
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Pengumuman;
|
export default Pengumuman;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
|
||||||
|
import { IconCategory, IconListCheck } from '@tabler/icons-react';
|
||||||
import { usePathname, useRouter } from 'next/navigation';
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
@@ -12,17 +13,21 @@ function LayoutTabsPotensi({ children }: { children: React.ReactNode }) {
|
|||||||
{
|
{
|
||||||
label: "List Potensi",
|
label: "List Potensi",
|
||||||
value: "list_potensi",
|
value: "list_potensi",
|
||||||
href: "/admin/desa/potensi/list-potensi"
|
href: "/admin/desa/potensi/list-potensi",
|
||||||
|
icon: <IconListCheck size={18} stroke={1.8} />,
|
||||||
|
tooltip: "Lihat semua potensi desa"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Kategori Potensi",
|
label: "Kategori Potensi",
|
||||||
value: "kategori_potensi",
|
value: "kategori_potensi",
|
||||||
href: "/admin/desa/potensi/kategori-potensi"
|
href: "/admin/desa/potensi/kategori-potensi",
|
||||||
|
icon: <IconCategory size={18} stroke={1.8} />,
|
||||||
|
tooltip: "Kelola kategori potensi"
|
||||||
},
|
},
|
||||||
|
|
||||||
];
|
];
|
||||||
const curentTab = tabs.find(tab => tab.href === pathname)
|
|
||||||
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
|
const currentTab = tabs.find(tab => tab.href === pathname)
|
||||||
|
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
|
||||||
|
|
||||||
const handleTabChange = (value: string | null) => {
|
const handleTabChange = (value: string | null) => {
|
||||||
const tab = tabs.find(t => t.value === value)
|
const tab = tabs.find(t => t.value === value)
|
||||||
@@ -40,22 +45,57 @@ function LayoutTabsPotensi({ children }: { children: React.ReactNode }) {
|
|||||||
}, [pathname])
|
}, [pathname])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack gap="lg">
|
||||||
<Title order={3}>Potensi</Title>
|
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Potensi</Title>
|
||||||
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
|
<Tabs
|
||||||
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
|
color={colors['blue-button']}
|
||||||
{tabs.map((e, i) => (
|
variant='pills'
|
||||||
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
|
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 transitionProps={{ transition: 'pop', duration: 200 }}>
|
||||||
|
<TabsTab
|
||||||
|
value={tab.value}
|
||||||
|
leftSection={tab.icon}
|
||||||
|
style={{
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
transition: "all 0.2s ease",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</TabsTab>
|
||||||
|
</Tooltip>
|
||||||
))}
|
))}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
{tabs.map((e, i) => (
|
|
||||||
<TabsPanel key={i} value={e.value}>
|
{tabs.map((tab, i) => (
|
||||||
{/* Konten dummy, bisa diganti tergantung routing */}
|
<TabsPanel
|
||||||
<></>
|
key={i}
|
||||||
|
value={tab.value}
|
||||||
|
style={{
|
||||||
|
padding: "1.5rem",
|
||||||
|
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
|
||||||
|
borderRadius: "1rem",
|
||||||
|
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Konten dummy, bisa diganti sesuai routing */}
|
||||||
|
<>{children}</>
|
||||||
</TabsPanel>
|
</TabsPanel>
|
||||||
))}
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
{children}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client';
|
||||||
import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi';
|
import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Tooltip
|
||||||
|
} from '@mantine/core';
|
||||||
import { IconArrowBack } from '@tabler/icons-react';
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
@@ -10,9 +19,10 @@ import { toast } from 'react-toastify';
|
|||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
function EditKategoriPotensi() {
|
function EditKategoriPotensi() {
|
||||||
const editState = useProxy(potensiDesaState.kategoriPotensi)
|
const editState = useProxy(potensiDesaState.kategoriPotensi);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
nama: editState.update.form.nama || '',
|
nama: editState.update.form.nama || '',
|
||||||
});
|
});
|
||||||
@@ -23,15 +33,15 @@ function EditKategoriPotensi() {
|
|||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await editState.update.load(id); // akses langsung, bukan dari proxy
|
const data = await editState.update.load(id);
|
||||||
if (data) {
|
if (data) {
|
||||||
setFormData({
|
setFormData({
|
||||||
nama: data.nama || '',
|
nama: data.nama || '',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading kategori potensi:", error);
|
console.error('Error loading kategori potensi:', error);
|
||||||
toast.error("Gagal memuat data kategori potensi");
|
toast.error('Gagal memuat data kategori potensi');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -44,6 +54,7 @@ function EditKategoriPotensi() {
|
|||||||
...editState.update.form,
|
...editState.update.form,
|
||||||
nama: formData.nama,
|
nama: formData.nama,
|
||||||
};
|
};
|
||||||
|
|
||||||
await editState.update.update();
|
await editState.update.update();
|
||||||
toast.success('Kategori Potensi berhasil diperbarui!');
|
toast.success('Kategori Potensi berhasil diperbarui!');
|
||||||
router.push('/admin/desa/potensi/kategori-potensi');
|
router.push('/admin/desa/potensi/kategori-potensi');
|
||||||
@@ -54,23 +65,49 @@ function EditKategoriPotensi() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
<IconArrowBack color={colors["blue-button"]} size={30} />
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Tooltip>
|
||||||
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
|
<Title order={4} ml="sm" c="dark">
|
||||||
<Stack gap={"xs"}>
|
Edit Kategori Potensi
|
||||||
<Title order={3}>Edit Kategori Potensi</Title>
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Paper
|
||||||
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label="Nama Kategori Potensi"
|
||||||
|
placeholder="Masukkan nama kategori potensi"
|
||||||
value={formData.nama}
|
value={formData.nama}
|
||||||
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Nama Kategori Potensi</Text>}
|
required
|
||||||
placeholder="masukkan nama kategori potensi"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button onClick={handleSubmit}>Simpan</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>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,50 +1,87 @@
|
|||||||
'use client'
|
'use client';
|
||||||
import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi';
|
import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
import { IconArrowBack } from '@tabler/icons-react';
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function CreateKategoriPotensi() {
|
function CreateKategoriPotensi() {
|
||||||
const createState = useProxy(potensiDesaState.kategoriPotensi)
|
const createState = useProxy(potensiDesaState.kategoriPotensi);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
createState.create.form = {
|
createState.create.form = {
|
||||||
nama: "",
|
nama: '',
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
await createState.create.create();
|
await createState.create.create();
|
||||||
resetForm();
|
resetForm();
|
||||||
router.push("/admin/desa/potensi/kategori-potensi")
|
router.push('/admin/desa/potensi/kategori-potensi');
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Header dengan back button */}
|
||||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
p="xs"
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Tooltip>
|
||||||
|
<Title order={4} ml="sm" c="dark">
|
||||||
|
Tambah Kategori Potensi
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
{/* Form utama */}
|
||||||
<Stack gap={"xs"}>
|
<Paper
|
||||||
<Title order={4}>Create Kategori Potensi</Title>
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Potensi</Text>}
|
label={<Text fw="bold" fz="sm">Nama Kategori Potensi</Text>}
|
||||||
placeholder='Masukkan nama kategori Potensi'
|
placeholder="Masukkan nama kategori potensi"
|
||||||
value={createState.create.form.nama}
|
value={createState.create.form.nama || ''}
|
||||||
onChange={(val) => {
|
onChange={(e) => (createState.create.form.nama = e.target.value)}
|
||||||
createState.create.form.nama = val.target.value;
|
required
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<Group>
|
|
||||||
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
|
color: '#fff',
|
||||||
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
import { Box, Button, Center, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip, Pagination } from '@mantine/core';
|
||||||
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
|
import { IconEdit, IconSearch, IconTrash, IconPlus } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import HeaderSearch from '../../../_com/header';
|
import HeaderSearch from '../../../_com/header';
|
||||||
import JudulList from '../../../_com/judulList';
|
|
||||||
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
|
||||||
import potensiDesaState from '../../../_state/desa/potensi';
|
import potensiDesaState from '../../../_state/desa/potensi';
|
||||||
|
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||||
|
|
||||||
|
|
||||||
function KategoriPotensi() {
|
function KategoriPotensi() {
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
@@ -19,7 +16,7 @@ function KategoriPotensi() {
|
|||||||
<Box>
|
<Box>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
title='Kategori Potensi'
|
title='Kategori Potensi'
|
||||||
placeholder='pencarian'
|
placeholder='Cari nama kategori...'
|
||||||
searchIcon={<IconSearch size={20} />}
|
searchIcon={<IconSearch size={20} />}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
@@ -34,69 +31,77 @@ function ListKategoriPotensi({ search }: { search: string }) {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [modalHapus, setModalHapus] = useState(false)
|
const [modalHapus, setModalHapus] = useState(false)
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||||
|
const { data, page, totalPages, loading, load } = listDataState.findMany;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
listDataState.findMany.load()
|
load(1, 10, search)
|
||||||
}, [])
|
}, [search])
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
if (selectedId) {
|
if (selectedId) {
|
||||||
listDataState.delete.delete(selectedId)
|
listDataState.delete.delete(selectedId)
|
||||||
setModalHapus(false)
|
setModalHapus(false)
|
||||||
setSelectedId(null)
|
setSelectedId(null)
|
||||||
|
load(page, 10, search)
|
||||||
listDataState.findMany.load()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredData = (listDataState.findMany.data || []).filter(item => {
|
const filteredData = data || []
|
||||||
const keyword = search.toLowerCase();
|
|
||||||
return (
|
|
||||||
item.nama.toLowerCase().includes(keyword)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!listDataState.findMany.data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton h={500} />
|
<Skeleton height={500} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={10}>
|
||||||
<Paper bg={colors['white-1']} p="md">
|
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||||
<Stack>
|
<Stack>
|
||||||
<JudulList
|
<Box style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 15 }}>
|
||||||
title='List Kategori Potensi'
|
<Title order={4}>List Kategori Potensi</Title>
|
||||||
href='/admin/desa/potensi/kategori-potensi/create'
|
<Tooltip label="Tambah Kategori Potensi" withArrow>
|
||||||
/>
|
<Button
|
||||||
|
leftSection={<IconPlus size={18} />}
|
||||||
|
color="blue"
|
||||||
|
variant="light"
|
||||||
|
onClick={() => router.push('/admin/desa/potensi/kategori-potensi/create')}
|
||||||
|
>
|
||||||
|
Tambah Baru
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Box style={{ overflowX: 'auto' }}>
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
|
<Table highlightOnHover striped withRowBorders style={{ minWidth: '700px' }}>
|
||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh>No</TableTh>
|
<TableTh style={{ width: '10%' }}>No</TableTh>
|
||||||
<TableTh>Nama</TableTh>
|
<TableTh style={{ width: '60%' }}>Nama</TableTh>
|
||||||
<TableTh>Edit</TableTh>
|
<TableTh style={{ width: '15%' }}>Edit</TableTh>
|
||||||
<TableTh>Hapus</TableTh>
|
<TableTh style={{ width: '15%' }}>Hapus</TableTh>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
</TableThead>
|
</TableThead>
|
||||||
<TableTbody>
|
<TableTbody>
|
||||||
{filteredData.map((item, index) => (
|
{filteredData.length > 0 ? (
|
||||||
|
filteredData.map((item, index) => (
|
||||||
<TableTr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Box w={100}>
|
<Text fz="sm">{(page - 1) * 10 + index + 1}</Text>
|
||||||
<Text truncate="end" fz={"sm"}>{index + 1}</Text>
|
|
||||||
</Box>
|
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd>{item.nama}</TableTd>
|
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Button color='green' onClick={() => router.push(`/admin/desa/potensi/kategori-potensi/${item.id}`)}>
|
<Text truncate lineClamp={1}>{item.nama}</Text>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Button variant='light' color='green' onClick={() => router.push(`/admin/desa/potensi/kategori-potensi/${item.id}`)}>
|
||||||
<IconEdit size={20} />
|
<IconEdit size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Button
|
<Button
|
||||||
|
variant='light'
|
||||||
color='red'
|
color='red'
|
||||||
disabled={listDataState.delete.loading}
|
disabled={listDataState.delete.loading}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -107,14 +112,32 @@ function ListKategoriPotensi({ search }: { search: string }) {
|
|||||||
</Button>
|
</Button>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))}
|
))
|
||||||
|
) : (
|
||||||
|
<TableTr>
|
||||||
|
<TableTd colSpan={4}>
|
||||||
|
<Center py={20}>
|
||||||
|
<Text color="dimmed">Tidak ada data kategori potensi yang cocok</Text>
|
||||||
|
</Center>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
)}
|
||||||
</TableTbody>
|
</TableTbody>
|
||||||
</Table>
|
</Table>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{/* Modal Konfirmasi Hapus */}
|
<Center mt="md">
|
||||||
|
<Pagination
|
||||||
|
value={page}
|
||||||
|
onChange={(newPage) => load(newPage, 10, search)}
|
||||||
|
total={totalPages}
|
||||||
|
color="blue"
|
||||||
|
radius="md"
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
|
||||||
<ModalKonfirmasiHapus
|
<ModalKonfirmasiHapus
|
||||||
opened={modalHapus}
|
opened={modalHapus}
|
||||||
onClose={() => setModalHapus(false)}
|
onClose={() => setModalHapus(false)}
|
||||||
|
|||||||
@@ -5,7 +5,19 @@ import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
|
|||||||
import potensiDesaState from "@/app/admin/(dashboard)/_state/desa/potensi";
|
import potensiDesaState from "@/app/admin/(dashboard)/_state/desa/potensi";
|
||||||
import colors from "@/con/colors";
|
import colors from "@/con/colors";
|
||||||
import ApiFetch from "@/lib/api-fetch";
|
import ApiFetch from "@/lib/api-fetch";
|
||||||
import { Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput, Title } from "@mantine/core";
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Image,
|
||||||
|
Paper,
|
||||||
|
Select,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from "@mantine/core";
|
||||||
import { Dropzone } from "@mantine/dropzone";
|
import { Dropzone } from "@mantine/dropzone";
|
||||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from "@tabler/icons-react";
|
import { IconArrowBack, IconPhoto, IconUpload, IconX } from "@tabler/icons-react";
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
@@ -13,38 +25,36 @@ import { useEffect, useState } from "react";
|
|||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { useProxy } from "valtio/utils";
|
import { useProxy } from "valtio/utils";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function EditPotensi() {
|
function EditPotensi() {
|
||||||
const potensiState = useProxy(potensiDesaState.potensiDesa)
|
const potensiState = useProxy(potensiDesaState.potensiDesa);
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
const params = useParams()
|
const params = useParams();
|
||||||
|
|
||||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: '',
|
name: "",
|
||||||
deskripsi: '',
|
deskripsi: "",
|
||||||
kategoriId: '',
|
kategoriId: "",
|
||||||
content: '',
|
content: "",
|
||||||
imageId: ''
|
imageId: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
potensiDesaState.kategoriPotensi.findMany.load()
|
potensiDesaState.kategoriPotensi.findMany.load();
|
||||||
const loadPotensi = async () => {
|
const loadPotensi = async () => {
|
||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await potensiState.edit.load(id); // ambil data dari API
|
const data = await potensiState.edit.load(id);
|
||||||
if (data) {
|
if (data) {
|
||||||
setFormData({
|
setFormData({
|
||||||
name: data.name || '',
|
name: data.name || "",
|
||||||
deskripsi: data.deskripsi || '',
|
deskripsi: data.deskripsi || "",
|
||||||
kategoriId: data.kategoriId || '',
|
kategoriId: data.kategoriId || "",
|
||||||
content: data.content || '',
|
content: data.content || "",
|
||||||
imageId: data.imageId || '',
|
imageId: data.imageId || "",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data?.image?.link) {
|
if (data?.image?.link) {
|
||||||
@@ -62,13 +72,9 @@ function EditPotensi() {
|
|||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
// Sinkronkan semua data dari formData ke state global
|
|
||||||
potensiState.edit.form = {
|
potensiState.edit.form = {
|
||||||
...potensiState.edit.form,
|
...potensiState.edit.form,
|
||||||
name: formData.name,
|
...formData,
|
||||||
deskripsi: formData.deskripsi,
|
|
||||||
kategoriId: formData.kategoriId,
|
|
||||||
content: formData.content,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (file) {
|
if (file) {
|
||||||
@@ -92,44 +98,52 @@ function EditPotensi() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: "sm", md: "lg" }} py="md">
|
||||||
<Box mb={10}>
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
|
<IconArrowBack color={colors["blue-button"]} size={24} />
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Tooltip>
|
||||||
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
|
<Title order={4} ml="sm" c="dark">
|
||||||
<Stack gap={"xs"}>
|
Edit Potensi Desa
|
||||||
<Title order={3}>Edit Potensi</Title>
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Paper
|
||||||
|
w={{ base: "100%", md: "50%" }}
|
||||||
|
bg={colors["white-1"]}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: "1px solid #e0e0e0" }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label="Judul Potensi"
|
||||||
|
placeholder="Masukkan judul"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(e) => {
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
const val = e.target.value;
|
required
|
||||||
setFormData((prev) => ({ ...prev, name: val }));
|
|
||||||
potensiState.edit.form.name = val;
|
|
||||||
}}
|
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
|
|
||||||
placeholder="masukkan judul"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label="Deskripsi Singkat"
|
||||||
|
placeholder="Masukkan deskripsi"
|
||||||
value={formData.deskripsi}
|
value={formData.deskripsi}
|
||||||
onChange={(e) => {
|
onChange={(e) => setFormData({ ...formData, deskripsi: e.target.value })}
|
||||||
const val = e.target.value;
|
required
|
||||||
setFormData((prev) => ({ ...prev, deskripsi: val }));
|
|
||||||
potensiState.edit.form.deskripsi = val;
|
|
||||||
}}
|
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>}
|
|
||||||
placeholder="masukkan deskripsi"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
value={formData.kategoriId}
|
value={formData.kategoriId}
|
||||||
onChange={(val) => setFormData({ ...formData, kategoriId: val || "" })}
|
onChange={(val) => setFormData({ ...formData, kategoriId: val || "" })}
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>}
|
label="Kategori"
|
||||||
placeholder='Pilih kategori'
|
placeholder="Pilih kategori"
|
||||||
data={
|
data={
|
||||||
potensiDesaState.kategoriPotensi.findMany.data?.map((v) => ({
|
potensiDesaState.kategoriPotensi.findMany.data?.map((v) => ({
|
||||||
value: v.id,
|
value: v.id,
|
||||||
label: v.nama
|
label: v.nama,
|
||||||
})) || []
|
})) || []
|
||||||
}
|
}
|
||||||
clearable
|
clearable
|
||||||
@@ -137,73 +151,86 @@ function EditPotensi() {
|
|||||||
required
|
required
|
||||||
error={!formData.kategoriId ? "Pilih kategori" : undefined}
|
error={!formData.kategoriId ? "Pilih kategori" : undefined}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
<Box>
|
Gambar Potensi
|
||||||
|
</Text>
|
||||||
<Dropzone
|
<Dropzone
|
||||||
onDrop={(files) => {
|
onDrop={(files) => {
|
||||||
const selectedFile = files[0]; // Ambil file pertama
|
const selectedFile = files[0];
|
||||||
if (selectedFile) {
|
if (selectedFile) {
|
||||||
setFile(selectedFile);
|
setFile(selectedFile);
|
||||||
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
|
setPreviewImage(URL.createObjectURL(selectedFile));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onReject={() => toast.error('File tidak valid.')}
|
onReject={() => toast.error("File tidak valid, gunakan format gambar")}
|
||||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
maxSize={5 * 1024 ** 2}
|
||||||
accept={{ 'image/*': [] }}
|
accept={{ "image/*": [] }}
|
||||||
|
radius="md"
|
||||||
|
p="xl"
|
||||||
>
|
>
|
||||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
<Group justify="center" gap="xl" mih={180}>
|
||||||
<Dropzone.Accept>
|
<Dropzone.Accept>
|
||||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
<IconUpload size={48} color={colors["blue-button"]} stroke={1.5} />
|
||||||
</Dropzone.Accept>
|
</Dropzone.Accept>
|
||||||
<Dropzone.Reject>
|
<Dropzone.Reject>
|
||||||
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
<IconX size={48} color="red" stroke={1.5} />
|
||||||
</Dropzone.Reject>
|
</Dropzone.Reject>
|
||||||
<Dropzone.Idle>
|
<Dropzone.Idle>
|
||||||
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
<IconPhoto size={48} color="#868e96" stroke={1.5} />
|
||||||
</Dropzone.Idle>
|
</Dropzone.Idle>
|
||||||
|
<Stack gap="xs" align="center">
|
||||||
<div>
|
<Text size="md" fw={500}>
|
||||||
<Text size="xl" inline>
|
Seret gambar atau klik untuk memilih file
|
||||||
Drag gambar ke sini atau klik untuk pilih file
|
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="sm" c="dimmed" inline mt={7}>
|
<Text size="sm" c="dimmed">
|
||||||
Maksimal 5MB dan harus format gambar
|
Maksimal 5MB, format gambar wajib
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</Stack>
|
||||||
</Group>
|
</Group>
|
||||||
</Dropzone>
|
</Dropzone>
|
||||||
|
|
||||||
{/* Tampilkan preview kalau ada */}
|
|
||||||
{previewImage && (
|
{previewImage && (
|
||||||
<Box mt="sm">
|
<Box mt="sm" style={{ display: "flex", justifyContent: "center" }}>
|
||||||
<Image
|
<Image
|
||||||
src={previewImage}
|
src={previewImage}
|
||||||
alt="Preview"
|
alt="Preview Gambar"
|
||||||
|
radius="md"
|
||||||
style={{
|
style={{
|
||||||
maxWidth: '100%',
|
maxHeight: 220,
|
||||||
maxHeight: '200px',
|
objectFit: "contain",
|
||||||
objectFit: 'contain',
|
border: `1px solid ${colors["blue-button"]}`,
|
||||||
borderRadius: '8px',
|
|
||||||
border: '1px solid #ddd',
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"sm"} fw={"bold"}>Konten</Text>
|
<Text fz="sm" fw="bold" mb={6}>
|
||||||
|
Konten Lengkap
|
||||||
|
</Text>
|
||||||
<EditEditor
|
<EditEditor
|
||||||
value={formData.content}
|
value={formData.content}
|
||||||
onChange={(htmlContent) => {
|
onChange={(htmlContent) => setFormData({ ...formData, content: htmlContent })}
|
||||||
setFormData((prev) => ({ ...prev, content: htmlContent }));
|
|
||||||
potensiState.edit.form.content = htmlContent;
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Edit Potensi</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>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,122 +1,151 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
|
||||||
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
|
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter, useParams } from 'next/navigation';
|
||||||
import React from 'react';
|
import { useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { useParams } from 'next/navigation';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||||
import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi';
|
import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi';
|
||||||
|
|
||||||
|
|
||||||
export default function DetailPotensi() {
|
export default function DetailPotensi() {
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
const params = useParams()
|
const params = useParams();
|
||||||
const [modalHapus, setModalHapus] = useState(false)
|
const [modalHapus, setModalHapus] = useState(false);
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
const potensiState = useProxy(potensiDesaState.potensiDesa)
|
const potensiState = useProxy(potensiDesaState.potensiDesa);
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
potensiState.findUnique.load(params?.id as string)
|
potensiState.findUnique.load(params?.id as string);
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
const handleHapus = () => {
|
const handleHapus = () => {
|
||||||
if (selectedId) {
|
if (selectedId) {
|
||||||
potensiState.delete.byId(selectedId)
|
potensiState.delete.byId(selectedId);
|
||||||
setModalHapus(false)
|
setModalHapus(false);
|
||||||
setSelectedId(null)
|
setSelectedId(null);
|
||||||
router.push("/admin/desa/potensi")
|
router.push("/admin/desa/potensi");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (!potensiState.findUnique.data) {
|
if (!potensiState.findUnique.data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
{Array.from({ length: 10 }).map((_, k) => (
|
<Skeleton height={500} radius="md" />
|
||||||
<Skeleton key={k} h={40} />
|
|
||||||
))}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = potensiState.findUnique.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box py={10}>
|
||||||
<Box mb={10}>
|
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
|
|
||||||
<Stack>
|
|
||||||
<Text fz={"xl"} fw={"bold"}>Detail Potensi</Text>
|
|
||||||
{potensiState.findUnique.data ? (
|
|
||||||
<Paper key={potensiState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
|
||||||
<Stack gap={"xs"}>
|
|
||||||
<Box>
|
|
||||||
<Text fz={"lg"} fw={"bold"}>Judul</Text>
|
|
||||||
<Text fz={"lg"}>{potensiState.findUnique.data.name}</Text>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text fz={"lg"} fw={"bold"}>Kategori</Text>
|
|
||||||
<Text fz={"lg"}>{potensiState.findUnique.data.kategori?.nama}</Text>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text>
|
|
||||||
<Text fz={"lg"}>{potensiState.findUnique.data.deskripsi}</Text>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
|
|
||||||
<Image src={potensiState.findUnique.data.image?.link} alt="gambar" />
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text fz={"lg"} fw={"bold"}>Konten</Text>
|
|
||||||
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: potensiState.findUnique.data.content }} />
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Flex gap={"xs"}>
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
variant="subtle"
|
||||||
if (potensiState.findUnique.data) {
|
onClick={() => router.back()}
|
||||||
setSelectedId(potensiState.findUnique.data.id)
|
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
|
||||||
setModalHapus(true)
|
mb={15}
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={potensiState.delete.loading || !potensiState.findUnique.data}
|
|
||||||
color="red"
|
|
||||||
>
|
>
|
||||||
<IconX size={20} />
|
Kembali
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<Paper
|
||||||
|
withBorder
|
||||||
|
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 Potensi
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
|
||||||
|
<Stack gap="sm">
|
||||||
|
<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?.nama || '-'}</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">Deskripsi</Text>
|
||||||
|
<Text fz="md" c="dimmed">{data.deskripsi || '-'}</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">Gambar</Text>
|
||||||
|
{data.image?.link ? (
|
||||||
|
<Image
|
||||||
|
src={data.image.link}
|
||||||
|
alt={data.name || 'Gambar Potensi'}
|
||||||
|
w={200}
|
||||||
|
h={200}
|
||||||
|
radius="md"
|
||||||
|
fit="cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">Konten</Text>
|
||||||
|
<Text
|
||||||
|
fz="md"
|
||||||
|
c="dimmed"
|
||||||
|
dangerouslySetInnerHTML={{ __html: data.content || '-' }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Group gap="sm">
|
||||||
|
<Tooltip label="Hapus Potensi" withArrow position="top">
|
||||||
<Button
|
<Button
|
||||||
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (potensiState.findUnique.data) {
|
setSelectedId(data.id);
|
||||||
router.push(`/admin/desa/potensi/list-potensi/${potensiState.findUnique.data.id}/edit`)
|
setModalHapus(true);
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
disabled={!potensiState.findUnique.data}
|
variant="light"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
>
|
||||||
|
<IconTrash size={20} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip label="Edit Potensi" withArrow position="top">
|
||||||
|
<Button
|
||||||
color="green"
|
color="green"
|
||||||
|
onClick={() =>
|
||||||
|
router.push(`/admin/desa/potensi/list-potensi/${data.id}/edit`)
|
||||||
|
}
|
||||||
|
variant="light"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
>
|
>
|
||||||
<IconEdit size={20} />
|
<IconEdit size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Tooltip>
|
||||||
</Box>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
) : null}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{/* Modal Hapus */}
|
|
||||||
<ModalKonfirmasiHapus
|
<ModalKonfirmasiHapus
|
||||||
opened={modalHapus}
|
opened={modalHapus}
|
||||||
onClose={() => setModalHapus(false)}
|
onClose={() => setModalHapus(false)}
|
||||||
onConfirm={handleHapus}
|
onConfirm={handleHapus}
|
||||||
text="Apakah anda yakin ingin menghapus potensi ini?"
|
text="Apakah Anda yakin ingin menghapus potensi ini?"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,19 @@ import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
|||||||
import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi';
|
import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import ApiFetch from '@/lib/api-fetch';
|
import ApiFetch from '@/lib/api-fetch';
|
||||||
import { Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Image,
|
||||||
|
Paper,
|
||||||
|
Select,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
import { Dropzone } from '@mantine/dropzone';
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
@@ -12,8 +24,6 @@ import { useEffect, useState } from 'react';
|
|||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function CreatePotensi() {
|
function CreatePotensi() {
|
||||||
const potensiState = useProxy(potensiDesaState.potensiDesa);
|
const potensiState = useProxy(potensiDesaState.potensiDesa);
|
||||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
@@ -21,8 +31,8 @@ function CreatePotensi() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
potensiDesaState.kategoriPotensi.findMany.load()
|
potensiDesaState.kategoriPotensi.findMany.load();
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!file) return toast.warn('Pilih file gambar terlebih dahulu');
|
if (!file) return toast.warn('Pilih file gambar terlebih dahulu');
|
||||||
@@ -59,34 +69,50 @@ function CreatePotensi() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Header */}
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Tooltip>
|
||||||
|
<Title order={4} ml="sm" c="dark">
|
||||||
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}>
|
Tambah Potensi Desa
|
||||||
<Stack gap="xs">
|
</Title>
|
||||||
<Title order={3}>Create Potensi</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">
|
||||||
|
{/* Judul */}
|
||||||
<TextInput
|
<TextInput
|
||||||
value={potensiState.create.form.name}
|
value={potensiState.create.form.name}
|
||||||
onChange={(val) => (potensiState.create.form.name = val.target.value)}
|
onChange={(val) => (potensiState.create.form.name = val.target.value)}
|
||||||
label={<Text fz="sm" fw="bold">Judul</Text>}
|
label={<Text fz="sm" fw="bold">Judul</Text>}
|
||||||
placeholder="masukkan judul"
|
placeholder="Masukkan judul potensi"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Deskripsi */}
|
||||||
<TextInput
|
<TextInput
|
||||||
value={potensiState.create.form.deskripsi}
|
value={potensiState.create.form.deskripsi}
|
||||||
onChange={(val) => (potensiState.create.form.deskripsi = val.target.value)}
|
onChange={(val) => (potensiState.create.form.deskripsi = val.target.value)}
|
||||||
label={<Text fz="sm" fw="bold">Deskripsi</Text>}
|
label={<Text fz="sm" fw="bold">Deskripsi</Text>}
|
||||||
placeholder="masukkan deskripsi"
|
placeholder="Masukkan deskripsi singkat"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Kategori */}
|
||||||
<Select
|
<Select
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>}
|
label={<Text fz="sm" fw="bold">Kategori</Text>}
|
||||||
placeholder='Pilih kategori'
|
placeholder="Pilih kategori"
|
||||||
value={potensiState.create.form.kategoriId || ""}
|
value={potensiState.create.form.kategoriId || ""}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
potensiState.create.form.kategoriId = val ?? "";
|
potensiState.create.form.kategoriId = val ?? "";
|
||||||
@@ -97,65 +123,58 @@ function CreatePotensi() {
|
|||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Upload Gambar */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
<Box>
|
Gambar Potensi
|
||||||
|
</Text>
|
||||||
<Dropzone
|
<Dropzone
|
||||||
onDrop={(files) => {
|
onDrop={(files) => {
|
||||||
const selectedFile = files[0]; // Ambil file pertama
|
const selectedFile = files[0];
|
||||||
if (selectedFile) {
|
if (selectedFile) {
|
||||||
setFile(selectedFile);
|
setFile(selectedFile);
|
||||||
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
|
setPreviewImage(URL.createObjectURL(selectedFile));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onReject={() => toast.error('File tidak valid.')}
|
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
||||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
maxSize={5 * 1024 ** 2}
|
||||||
accept={{ 'image/*': [] }}
|
accept={{ 'image/*': [] }}
|
||||||
|
radius="md"
|
||||||
|
p="xl"
|
||||||
>
|
>
|
||||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
<Group justify="center" gap="xl" mih={180}>
|
||||||
<Dropzone.Accept>
|
<Dropzone.Accept>
|
||||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
<IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||||
</Dropzone.Accept>
|
</Dropzone.Accept>
|
||||||
<Dropzone.Reject>
|
<Dropzone.Reject>
|
||||||
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
<IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||||
</Dropzone.Reject>
|
</Dropzone.Reject>
|
||||||
<Dropzone.Idle>
|
<Dropzone.Idle>
|
||||||
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
<IconPhoto size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||||
</Dropzone.Idle>
|
</Dropzone.Idle>
|
||||||
|
|
||||||
<div>
|
|
||||||
<Text size="xl" inline>
|
|
||||||
Drag gambar ke sini atau klik untuk pilih file
|
|
||||||
</Text>
|
|
||||||
<Text size="sm" c="dimmed" inline mt={7}>
|
|
||||||
Maksimal 5MB dan harus format gambar
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</Group>
|
</Group>
|
||||||
|
<Text ta="center" mt="sm" size="sm" color="dimmed">
|
||||||
|
Seret gambar atau klik untuk memilih file (maks 5MB)
|
||||||
|
</Text>
|
||||||
</Dropzone>
|
</Dropzone>
|
||||||
|
|
||||||
{/* Tampilkan preview kalau ada */}
|
|
||||||
{previewImage && (
|
{previewImage && (
|
||||||
<Box mt="sm">
|
<Box mt="sm" style={{ textAlign: 'center' }}>
|
||||||
<Image
|
<Image
|
||||||
src={previewImage}
|
src={previewImage}
|
||||||
alt="Preview"
|
alt="Preview Gambar"
|
||||||
style={{
|
radius="md"
|
||||||
maxWidth: '100%',
|
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
|
||||||
maxHeight: '200px',
|
|
||||||
objectFit: 'contain',
|
|
||||||
borderRadius: '8px',
|
|
||||||
border: '1px solid #ddd',
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Konten Editor */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz="sm" fw="bold">Konten</Text>
|
<Text fz="sm" fw="bold" mb={6}>
|
||||||
|
Konten Lengkap
|
||||||
|
</Text>
|
||||||
<CreateEditor
|
<CreateEditor
|
||||||
value={potensiState.create.form.content}
|
value={potensiState.create.form.content}
|
||||||
onChange={(htmlContent) => {
|
onChange={(htmlContent) => {
|
||||||
@@ -164,9 +183,21 @@ function CreatePotensi() {
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>
|
{/* Tombol Simpan */}
|
||||||
|
<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 Potensi
|
Simpan Potensi
|
||||||
</Button>
|
</Button>
|
||||||
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,25 +1,40 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
import {
|
||||||
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
|
Box,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Group,
|
||||||
|
Pagination,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
TableTbody,
|
||||||
|
TableTd,
|
||||||
|
TableTh,
|
||||||
|
TableThead,
|
||||||
|
TableTr,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
Tooltip
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import HeaderSearch from '../../../_com/header';
|
import HeaderSearch from '../../../_com/header';
|
||||||
import JudulList from '../../../_com/judulList';
|
|
||||||
import potensiDesaState from '../../../_state/desa/potensi';
|
import potensiDesaState from '../../../_state/desa/potensi';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function Potensi() {
|
function Potensi() {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
title='Posisi Organisasi'
|
title='Potensi Desa'
|
||||||
placeholder='pencarian'
|
placeholder='Cari potensi atau kategori...'
|
||||||
searchIcon={<IconSearch size={20} />}
|
searchIcon={<IconSearch size={20} />}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
@@ -30,8 +45,8 @@ function Potensi() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ListPotensi({ search }: { search: string }) {
|
function ListPotensi({ search }: { search: string }) {
|
||||||
const potensiState = useProxy(potensiDesaState)
|
const potensiState = useProxy(potensiDesaState);
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
@@ -42,117 +57,108 @@ function ListPotensi({ search }: { search: string }) {
|
|||||||
} = potensiState.potensiDesa.findMany;
|
} = potensiState.potensiDesa.findMany;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
potensiState.kategoriPotensi.findMany.load()
|
potensiState.kategoriPotensi.findMany.load();
|
||||||
load(page, 10)
|
load(page, 10, search);
|
||||||
}, [])
|
}, [page, search]);
|
||||||
|
|
||||||
const filteredData = (potensiState.potensiDesa.findMany.data || []).filter(item => {
|
const filteredData = data || []
|
||||||
const keyword = search.toLowerCase();
|
|
||||||
return (
|
|
||||||
item.name.toLowerCase().includes(keyword) ||
|
|
||||||
item.kategori?.nama.toLowerCase().includes(keyword) ||
|
|
||||||
item.deskripsi.toLowerCase().includes(keyword)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle loading state
|
|
||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton height={300} />
|
<Skeleton height={600} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.length === 0) {
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={10}>
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||||
<Stack>
|
<Group justify="space-between" mb="md">
|
||||||
<JudulList
|
<Title order={4}>Daftar Potensi Desa</Title>
|
||||||
title='List Potensi'
|
<Tooltip label="Tambah Potensi" withArrow>
|
||||||
href='/admin/desa/potensi/list-potensi/create'
|
<Button
|
||||||
/>
|
leftSection={<IconPlus size={18} />}
|
||||||
|
color="blue"
|
||||||
|
variant="light"
|
||||||
|
onClick={() => router.push('/admin/desa/potensi/list-potensi/create')}
|
||||||
|
>
|
||||||
|
Tambah Baru
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
<Box style={{ overflowX: "auto" }}>
|
<Box style={{ overflowX: "auto" }}>
|
||||||
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
|
<Table highlightOnHover style={{ minWidth: '700px' }}>
|
||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh>Judul</TableTh>
|
<TableTh style={{ width: '20%' }}>Judul</TableTh>
|
||||||
<TableTh>Kategori</TableTh>
|
<TableTh style={{ width: '20%' }}>Kategori</TableTh>
|
||||||
<TableTh>Deskripsi</TableTh>
|
<TableTh style={{ width: '35%' }}>Deskripsi</TableTh>
|
||||||
<TableTh>Detail</TableTh>
|
<TableTh style={{ width: '15%' }}>Detail</TableTh>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
</TableThead>
|
</TableThead>
|
||||||
<TableTbody>
|
<TableTbody>
|
||||||
<TableTr>
|
{filteredData.length > 0 ? (
|
||||||
<TableTd colSpan={4}>Tidak Ada Data</TableTd>
|
filteredData.map((item) => (
|
||||||
</TableTr>
|
|
||||||
</TableTbody>
|
|
||||||
</Table>
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box py={10}>
|
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
|
||||||
<Stack>
|
|
||||||
<JudulList
|
|
||||||
title='List Potensi'
|
|
||||||
href='/admin/desa/potensi/list-potensi/create'
|
|
||||||
/>
|
|
||||||
<Box style={{ overflowX: "auto" }}>
|
|
||||||
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
|
|
||||||
<TableThead>
|
|
||||||
<TableTr>
|
|
||||||
<TableTh>Judul</TableTh>
|
|
||||||
<TableTh>Kategori</TableTh>
|
|
||||||
<TableTh>Deskripsi</TableTh>
|
|
||||||
<TableTh>Detail</TableTh>
|
|
||||||
</TableTr>
|
|
||||||
</TableThead>
|
|
||||||
<TableTbody>
|
|
||||||
{filteredData.map((item) => (
|
|
||||||
<TableTr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Box w={100}>
|
<Text fw={500} truncate="end" lineClamp={1}>
|
||||||
<Text truncate="end" fz={"sm"}>{item.name}</Text>
|
{item.name}
|
||||||
</Box></TableTd>
|
</Text>
|
||||||
<TableTd>{item.kategori?.nama}</TableTd>
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Text fz="sm" c="dimmed">{item.kategori?.nama || '-'}</Text>
|
||||||
|
</TableTd>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Box w={300}>
|
<Box w={300}>
|
||||||
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
<Text
|
||||||
|
truncate
|
||||||
|
fz="sm"
|
||||||
|
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Button onClick={() => router.push(`/admin/desa/potensi/list-potensi/${item.id}`)}>
|
<Button
|
||||||
<IconDeviceImacCog size={25} />
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
onClick={() => router.push(`/admin/desa/potensi/list-potensi/${item.id}`)}
|
||||||
|
>
|
||||||
|
<IconDeviceImacCog size={20} />
|
||||||
|
<Text ml={5}>Detail</Text>
|
||||||
</Button>
|
</Button>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))}
|
))
|
||||||
|
) : (
|
||||||
|
<TableTr>
|
||||||
|
<TableTd colSpan={4}>
|
||||||
|
<Center py={20}>
|
||||||
|
<Text color="dimmed">Tidak ada data potensi yang cocok</Text>
|
||||||
|
</Center>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
)}
|
||||||
</TableTbody>
|
</TableTbody>
|
||||||
</Table>
|
</Table>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
|
||||||
</Paper>
|
</Paper>
|
||||||
<Center>
|
<Center>
|
||||||
<Pagination
|
<Pagination
|
||||||
value={page}
|
value={page}
|
||||||
onChange={(newPage) => {
|
onChange={(newPage) => {
|
||||||
load(newPage, 10);
|
load(newPage, 10);
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
}}
|
}}
|
||||||
total={totalPages}
|
total={totalPages}
|
||||||
mt="md"
|
mt="md"
|
||||||
mb="md"
|
mb="md"
|
||||||
|
color="blue"
|
||||||
|
radius="md"
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Potensi;
|
export default Potensi;
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
|
||||||
import { usePathname, useRouter } from 'next/navigation';
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { IconUser, IconUsers, IconCalendar } from '@tabler/icons-react';
|
||||||
|
|
||||||
function LayoutTabsDetail({ children }: { children: React.ReactNode }) {
|
function LayoutTabsDetail({ children }: { children: React.ReactNode }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -12,21 +13,28 @@ function LayoutTabsDetail({ children }: { children: React.ReactNode }) {
|
|||||||
{
|
{
|
||||||
label: "Profile Desa",
|
label: "Profile Desa",
|
||||||
value: "profiledesa",
|
value: "profiledesa",
|
||||||
href: "/admin/desa/profile/profile-desa"
|
href: "/admin/desa/profile/profile-desa",
|
||||||
|
icon: <IconUser size={18} stroke={1.8} />,
|
||||||
|
tooltip: "Lihat dan kelola profil desa"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Profile Perbekel",
|
label: "Profile Perbekel",
|
||||||
value: "profileperbekel",
|
value: "profileperbekel",
|
||||||
href: "/admin/desa/profile/profile-perbekel"
|
href: "/admin/desa/profile/profile-perbekel",
|
||||||
|
icon: <IconUsers size={18} stroke={1.8} />,
|
||||||
|
tooltip: "Kelola data Perbekel"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Profile Perbekel Dari Masa Ke Masa",
|
label: "Profile Perbekel Dari Masa Ke Masa",
|
||||||
value: "profile-perbekel-dari-masa-ke-masa",
|
value: "profile-perbekel-dari-masa-ke-masa",
|
||||||
href: "/admin/desa/profile/profile-perbekel-dari-masa-ke-masa"
|
href: "/admin/desa/profile/profile-perbekel-dari-masa-ke-masa",
|
||||||
|
icon: <IconCalendar size={18} stroke={1.8} />,
|
||||||
|
tooltip: "Riwayat Perbekel dari masa ke masa"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
const curentTab = tabs.find(tab => tab.href === pathname)
|
|
||||||
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
|
const currentTab = tabs.find(tab => tab.href === pathname)
|
||||||
|
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
|
||||||
|
|
||||||
const handleTabChange = (value: string | null) => {
|
const handleTabChange = (value: string | null) => {
|
||||||
const tab = tabs.find(t => t.value === value)
|
const tab = tabs.find(t => t.value === value)
|
||||||
@@ -44,22 +52,57 @@ function LayoutTabsDetail({ children }: { children: React.ReactNode }) {
|
|||||||
}, [pathname])
|
}, [pathname])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack gap="lg">
|
||||||
<Title order={3}>Profile Desa</Title>
|
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Profile Desa</Title>
|
||||||
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
|
<Tabs
|
||||||
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
|
color={colors['blue-button']}
|
||||||
{tabs.map((e, i) => (
|
variant='pills'
|
||||||
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
|
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 transitionProps={{ transition: 'pop', duration: 200 }}>
|
||||||
|
<TabsTab
|
||||||
|
value={tab.value}
|
||||||
|
leftSection={tab.icon}
|
||||||
|
style={{
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
transition: "all 0.2s ease",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</TabsTab>
|
||||||
|
</Tooltip>
|
||||||
))}
|
))}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
{tabs.map((e, i) => (
|
|
||||||
<TabsPanel key={i} value={e.value}>
|
{tabs.map((tab, i) => (
|
||||||
{/* Konten dummy, bisa diganti tergantung routing */}
|
<TabsPanel
|
||||||
<></>
|
key={i}
|
||||||
|
value={tab.value}
|
||||||
|
style={{
|
||||||
|
padding: "1.5rem",
|
||||||
|
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
|
||||||
|
borderRadius: "1rem",
|
||||||
|
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Konten dummy, bisa diganti sesuai routing */}
|
||||||
|
<>{children}</>
|
||||||
</TabsPanel>
|
</TabsPanel>
|
||||||
))}
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
{children}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
||||||
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
|
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import { Alert, Box, Button, Center, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
|
||||||
import { IconArrowBack } from '@tabler/icons-react';
|
import { IconAlertCircle, IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
@@ -15,7 +15,7 @@ function Page() {
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
// Load data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
@@ -25,9 +25,12 @@ function Page() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
const data = await lambangState.findUnique.load(id);
|
const data = await lambangState.findUnique.load(id);
|
||||||
if (data) {
|
|
||||||
lambangState.update.initialize(data);
|
lambangState.update.initialize(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading lambang:", error);
|
||||||
|
toast.error("Gagal memuat data lambang desa");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -35,19 +38,21 @@ function Page() {
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
lambangState.update.reset();
|
lambangState.update.reset();
|
||||||
lambangState.findUnique.reset(); // opsional: reset juga data lama
|
lambangState.findUnique.reset();
|
||||||
};
|
};
|
||||||
}, [params?.id, router]);
|
}, [params?.id, router]);
|
||||||
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (isSubmitting || !lambangState.update.form.judul.trim()) {
|
if (isSubmitting || !lambangState.update.form.judul.trim()) {
|
||||||
toast.error("Judul wajib diisi");
|
toast.error("Judul wajib diisi");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIsSubmitting(true)
|
|
||||||
|
setIsSubmitting(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const success = await lambangState.update.submit()
|
const success = await lambangState.update.submit();
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
toast.success("Data berhasil disimpan");
|
toast.success("Data berhasil disimpan");
|
||||||
router.push("/admin/desa/profile/profile-desa");
|
router.push("/admin/desa/profile/profile-desa");
|
||||||
@@ -58,17 +63,12 @@ function Page() {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleBack = () => {
|
const handleBack = () => router.back();
|
||||||
router.back()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
// Loading state
|
||||||
lambangState.findUnique.loading ||
|
if (lambangState.findUnique.loading || lambangState.update.loading) {
|
||||||
!lambangState.findUnique.data ||
|
|
||||||
lambangState.update.loading
|
|
||||||
) {
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Center h={400}>
|
<Center h={400}>
|
||||||
@@ -77,27 +77,50 @@ function Page() {
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Error state
|
||||||
|
if (lambangState.findUnique.error) {
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Stack gap={'xs'}>
|
<Stack gap="md">
|
||||||
<Group>
|
|
||||||
<Button variant="subtle" onClick={handleBack}>
|
<Button variant="subtle" onClick={handleBack}>
|
||||||
<IconArrowBack color={colors['blue-button']} size={20} />
|
<IconArrowBack color={colors['blue-button']} size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
|
<Alert icon={<IconAlertCircle size={16} />} color="red">
|
||||||
|
<Text fw="bold">Error</Text>
|
||||||
|
<Text>{lambangState.findUnique.error}</Text>
|
||||||
|
</Alert>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Group mb="md">
|
||||||
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
|
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Title order={4} ml="sm" c="dark">Edit Lambang Desa</Title>
|
||||||
</Group>
|
</Group>
|
||||||
<Paper bg={colors['white-1']} p={'xs'} w={{ base: '100%', md: '50%' }}>
|
|
||||||
<Stack gap={'xs'}>
|
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p="md" radius="md" shadow="sm" style={{ border: '1px solid #e0e0e0' }}>
|
||||||
<Box>
|
<Stack gap="xs">
|
||||||
<Box>
|
|
||||||
<Stack>
|
|
||||||
<Title order={3}>Edit Lambang Desa</Title>
|
<Title order={3}>Edit Lambang Desa</Title>
|
||||||
|
|
||||||
|
{/* Judul */}
|
||||||
<TextInput
|
<TextInput
|
||||||
label={<Text fz={"md"} fw={"bold"}>Judul</Text>}
|
label={<Text fw="bold">Judul</Text>}
|
||||||
placeholder="Judul"
|
placeholder="Judul lambang"
|
||||||
value={lambangState.update.form.judul}
|
value={lambangState.update.form.judul}
|
||||||
onChange={(e) => lambangState.update.form.judul = e.currentTarget.value}
|
onChange={(e) => lambangState.update.form.judul = e.currentTarget.value}
|
||||||
error={!lambangState.update.form.judul && "Judul wajib diisi"}
|
error={!lambangState.update.form.judul && "Judul wajib diisi"}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Deskripsi */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"md"} fw={"bold"}>Deskripsi</Text>
|
<Text fz={"md"} fw={"bold"}>Deskripsi</Text>
|
||||||
<EditEditor
|
<EditEditor
|
||||||
@@ -105,18 +128,23 @@ function Page() {
|
|||||||
onChange={(val) => lambangState.update.form.deskripsi = val}
|
onChange={(val) => lambangState.update.form.deskripsi = val}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Buttons */}
|
||||||
<Group>
|
<Group>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSubmit}
|
|
||||||
bg={colors['blue-button']}
|
bg={colors['blue-button']}
|
||||||
|
onClick={handleSubmit}
|
||||||
|
loading={isSubmitting || lambangState.update.loading}
|
||||||
|
disabled={!lambangState.update.form.judul}
|
||||||
>
|
>
|
||||||
Submit
|
{isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button variant="outline" onClick={handleBack} disabled={isSubmitting || lambangState.update.loading}>
|
||||||
|
Batal
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
</Paper>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -5,38 +5,40 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
|||||||
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
|
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import ApiFetch from '@/lib/api-fetch';
|
import ApiFetch from '@/lib/api-fetch';
|
||||||
import { Box, Button, Group, Image, Paper, SimpleGrid, Stack, Text, TextInput, Title } from '@mantine/core';
|
import { Box, Button, Group, Image, Paper, SimpleGrid, Stack, Text, TextInput, Title, Tooltip, Center, Alert } from '@mantine/core';
|
||||||
import { Dropzone } from '@mantine/dropzone';
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
import { IconArrowBack, IconPhoto, IconUpload, IconX, IconAlertCircle } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
function Page() {
|
function Page() {
|
||||||
const maskotState = useProxy(stateProfileDesa.maskotDesa)
|
const maskotState = useProxy(stateProfileDesa.maskotDesa);
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
const params = useParams()
|
const params = useParams();
|
||||||
|
|
||||||
const [images, setImages] = useState<
|
|
||||||
Array<{ file: File; preview: string; label: string }>
|
|
||||||
>([]);
|
|
||||||
|
|
||||||
|
const [images, setImages] = useState<Array<{ file: File | null; preview: string; label: string }>>([]);
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
judul: maskotState.update.form.judul || '',
|
judul: '',
|
||||||
deskripsi: maskotState.update.form.deskripsi || '',
|
deskripsi: '',
|
||||||
images: [] as Array<{ label: string; imageId: string }>
|
images: [] as Array<{ label: string; imageId: string }>,
|
||||||
})
|
});
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Load data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
if (!id) return;
|
if (!id) {
|
||||||
|
toast.error("ID tidak valid");
|
||||||
|
router.push("/admin/desa/profile/profile-desa");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await maskotState.findUnique.load(id);
|
const data = await maskotState.findUnique.load(id);
|
||||||
if (data) {
|
if (data) {
|
||||||
// 🔥 INI YANG KURANG!
|
|
||||||
maskotState.update.initialize(data);
|
maskotState.update.initialize(data);
|
||||||
|
|
||||||
setFormData({
|
setFormData({
|
||||||
@@ -57,28 +59,39 @@ function Page() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading berita:", error);
|
console.error("Error loading maskot:", error);
|
||||||
toast.error("Gagal memuat data berita");
|
toast.error("Gagal memuat data maskot");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadData();
|
loadData();
|
||||||
}, [params?.id]);
|
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
maskotState.update.reset();
|
||||||
|
maskotState.findUnique.reset();
|
||||||
|
};
|
||||||
|
}, [params?.id, router]);
|
||||||
|
|
||||||
const handleBack = () => {
|
const handleBack = () => router.back();
|
||||||
router.back()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
|
if (isSubmitting || !formData.judul.trim()) {
|
||||||
|
toast.error("Judul wajib diisi");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const uploadedImages = [];
|
const uploadedImages = [];
|
||||||
|
|
||||||
// Upload semua gambar baru
|
// Upload semua gambar baru
|
||||||
for (const img of images) {
|
for (const img of images) {
|
||||||
if (!img.file || !(img.file instanceof File)) {
|
if (!img.file) {
|
||||||
toast.error("File tidak valid untuk di-upload");
|
// Kalau gambar lama, skip upload
|
||||||
continue; // atau return kalau kamu mau hentikan semua
|
if (!img.preview) continue;
|
||||||
|
uploadedImages.push({ imageId: '', label: img.label });
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await ApiFetch.api.fileStorage.create.post({
|
const res = await ApiFetch.api.fileStorage.create.post({
|
||||||
@@ -92,10 +105,7 @@ function Page() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadedImages.push({
|
uploadedImages.push({ imageId: uploaded.id, label: img.label || 'main' });
|
||||||
imageId: uploaded.id,
|
|
||||||
label: img.label || 'main',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update ke global state
|
// Update ke global state
|
||||||
@@ -109,44 +119,79 @@ function Page() {
|
|||||||
toast.success("Maskot berhasil diperbarui!");
|
toast.success("Maskot berhasil diperbarui!");
|
||||||
router.push("/admin/desa/profile/profile-desa");
|
router.push("/admin/desa/profile/profile-desa");
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error update maskot:", error);
|
console.error("Error update maskot:", error);
|
||||||
toast.error("Gagal update maskot");
|
toast.error("Gagal update maskot");
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Loading state
|
||||||
|
if (maskotState.findUnique.loading || maskotState.update.loading) {
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Stack gap={'xs'}>
|
<Center h={400}>
|
||||||
<Group>
|
<Text>Memuat data...</Text>
|
||||||
|
</Center>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error state
|
||||||
|
if (maskotState.findUnique.error) {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Stack gap="md">
|
||||||
<Button variant="subtle" onClick={handleBack}>
|
<Button variant="subtle" onClick={handleBack}>
|
||||||
<IconArrowBack color={colors['blue-button']} size={20} />
|
<IconArrowBack color={colors['blue-button']} size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
|
<Alert icon={<IconAlertCircle size={16} />} color="red">
|
||||||
|
<Text fw="bold">Error</Text>
|
||||||
|
<Text>{maskotState.findUnique.error}</Text>
|
||||||
|
</Alert>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Group mb="md">
|
||||||
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
|
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Title order={4} ml="sm" c="dark">Edit Maskot Desa</Title>
|
||||||
</Group>
|
</Group>
|
||||||
<Paper bg={colors['white-1']} p={'xs'} w={{ base: '100%', md: '100%' }}>
|
|
||||||
<Stack gap={'xs'}>
|
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p="md" radius="md" shadow="sm" style={{ border: '1px solid #e0e0e0' }}>
|
||||||
<Box>
|
<Stack gap="xs">
|
||||||
<Box>
|
|
||||||
<Stack>
|
|
||||||
<Title order={3}>Edit Maskot Desa</Title>
|
<Title order={3}>Edit Maskot Desa</Title>
|
||||||
|
|
||||||
|
{/* Judul */}
|
||||||
<TextInput
|
<TextInput
|
||||||
w={{ base: '100%', md: '50%' }}
|
label={<Text fw="bold">Judul</Text>}
|
||||||
label={<Text fz={"md"} fw={"bold"}>Judul</Text>}
|
placeholder="Masukkan judul maskot"
|
||||||
placeholder="Masukkan judul"
|
|
||||||
value={formData.judul}
|
value={formData.judul}
|
||||||
onChange={(val) => setFormData({ ...formData, judul: val.currentTarget.value })}
|
onChange={(e) => setFormData({ ...formData, judul: e.currentTarget.value })}
|
||||||
|
error={!formData.judul && "Judul wajib diisi"}
|
||||||
/>
|
/>
|
||||||
<Box w={{ base: '100%', md: '50%' }}>
|
|
||||||
|
{/* Deskripsi */}
|
||||||
|
<Box>
|
||||||
<Text fz={"md"} fw={"bold"}>Deskripsi</Text>
|
<Text fz={"md"} fw={"bold"}>Deskripsi</Text>
|
||||||
<EditEditor
|
<EditEditor
|
||||||
value={formData.deskripsi}
|
value={formData.deskripsi}
|
||||||
onChange={(val) => setFormData({ ...formData, deskripsi: val })}
|
onChange={(val) => setFormData({ ...formData, deskripsi: val })}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Upload Gambar */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
||||||
<Box w={{ base: '100%', md: '50%' }}>
|
|
||||||
<Dropzone
|
<Dropzone
|
||||||
onDrop={(files) => {
|
onDrop={(files) => {
|
||||||
const newImages = files.map((file) => ({
|
const newImages = files.map((file) => ({
|
||||||
@@ -158,34 +203,24 @@ function Page() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
||||||
<Dropzone.Accept>
|
<Dropzone.Accept><IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /></Dropzone.Accept>
|
||||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
<Dropzone.Reject><IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} /></Dropzone.Reject>
|
||||||
</Dropzone.Accept>
|
<Dropzone.Idle><IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} /></Dropzone.Idle>
|
||||||
<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>
|
<div>
|
||||||
<Text size="xl" inline>
|
<Text size="xl" inline>Drag images here or click to select files</Text>
|
||||||
Drag images here or click to select files
|
<Text size="sm" c="dimmed" inline mt={7}>Attach as many files as you like, each file max 5mb</Text>
|
||||||
</Text>
|
|
||||||
<Text size="sm" c="dimmed" inline mt={7}>
|
|
||||||
Attach as many files as you like, each file should not exceed 5mb
|
|
||||||
</Text>
|
|
||||||
</div>
|
</div>
|
||||||
</Group>
|
</Group>
|
||||||
</Dropzone>
|
</Dropzone>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
|
||||||
|
{/* Preview Gambar */}
|
||||||
<SimpleGrid cols={{ base: 2, md: 4 }}>
|
<SimpleGrid cols={{ base: 2, md: 4 }}>
|
||||||
{images.map((img, index) => (
|
{images.map((img, index) => (
|
||||||
<Box key={index} mb="md">
|
<Box key={index} mb="md">
|
||||||
<Paper p="sm" radius="md" withBorder style={{ position: 'relative', maxWidth: 300 }}>
|
<Paper p="sm" radius="md" withBorder style={{ position: 'relative', maxWidth: 300 }}>
|
||||||
<Stack gap={'xs'}>
|
<Stack gap="xs">
|
||||||
<Group>
|
<Group justify="space-between">
|
||||||
<Button
|
<Button
|
||||||
size="xs"
|
size="xs"
|
||||||
color="red"
|
color="red"
|
||||||
@@ -222,18 +257,22 @@ function Page() {
|
|||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
|
||||||
|
{/* Buttons */}
|
||||||
<Group>
|
<Group>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSubmit}
|
|
||||||
bg={colors['blue-button']}
|
bg={colors['blue-button']}
|
||||||
|
onClick={handleSubmit}
|
||||||
|
loading={isSubmitting || maskotState.update.loading}
|
||||||
|
disabled={!formData.judul}
|
||||||
>
|
>
|
||||||
Submit
|
{isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" onClick={handleBack} disabled={isSubmitting || maskotState.update.loading}>
|
||||||
|
Batal
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
</Paper>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
||||||
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
|
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import { Alert, Box, Button, Center, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
|
||||||
import { IconArrowBack } from '@tabler/icons-react';
|
import { IconAlertCircle, IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
@@ -16,6 +16,7 @@ function Page() {
|
|||||||
const params = useParams()
|
const params = useParams()
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Load data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
@@ -25,29 +26,34 @@ function Page() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
const data = await sejarahState.findUnique.load(id);
|
const data = await sejarahState.findUnique.load(id);
|
||||||
if (data) {
|
if (data) {
|
||||||
sejarahState.update.initialize(data);
|
sejarahState.update.initialize(data);
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading sejarah:", error);
|
||||||
|
toast.error("Gagal memuat data sejarah desa");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadData();
|
loadData();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
sejarahState.update.reset();
|
sejarahState.update.reset();
|
||||||
sejarahState.findUnique.reset(); // opsional: reset juga data lama
|
sejarahState.findUnique.reset();
|
||||||
};
|
};
|
||||||
}, [params?.id, router]);
|
}, [params?.id, router]);
|
||||||
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (isSubmitting || !sejarahState.update.form.judul.trim()) {
|
if (isSubmitting || !sejarahState.update.form.judul.trim()) {
|
||||||
toast.error("Judul wajib diisi");
|
toast.error("Judul wajib diisi");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIsSubmitting(true)
|
|
||||||
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
const success = await sejarahState.update.submit()
|
const success = await sejarahState.update.submit();
|
||||||
if (success) {
|
if (success) {
|
||||||
toast.success("Data berhasil disimpan");
|
toast.success("Data berhasil disimpan");
|
||||||
router.push("/admin/desa/profile/profile-desa");
|
router.push("/admin/desa/profile/profile-desa");
|
||||||
@@ -58,17 +64,12 @@ function Page() {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleBack = () => {
|
const handleBack = () => router.back();
|
||||||
router.back()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
// Loading state
|
||||||
sejarahState.findUnique.loading ||
|
if (sejarahState.findUnique.loading || sejarahState.update.loading) {
|
||||||
!sejarahState.findUnique.data ||
|
|
||||||
sejarahState.update.loading
|
|
||||||
) {
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Center h={400}>
|
<Center h={400}>
|
||||||
@@ -77,27 +78,50 @@ function Page() {
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Error state
|
||||||
|
if (sejarahState.findUnique.error) {
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Stack gap={'xs'}>
|
<Stack gap="md">
|
||||||
<Group>
|
|
||||||
<Button variant="subtle" onClick={handleBack}>
|
<Button variant="subtle" onClick={handleBack}>
|
||||||
<IconArrowBack color={colors['blue-button']} size={20} />
|
<IconArrowBack color={colors['blue-button']} size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
|
<Alert icon={<IconAlertCircle size={16} />} color="red">
|
||||||
|
<Text fw="bold">Error</Text>
|
||||||
|
<Text>{sejarahState.findUnique.error}</Text>
|
||||||
|
</Alert>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Group mb="md">
|
||||||
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
|
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Title order={4} ml="sm" c="dark">Edit Sejarah Desa</Title>
|
||||||
</Group>
|
</Group>
|
||||||
<Paper bg={colors['white-1']} p={'xs'} w={{ base: '100%', md: '50%' }}>
|
|
||||||
<Stack gap={'xs'}>
|
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p="md" radius="md" shadow="sm" style={{ border: '1px solid #e0e0e0' }}>
|
||||||
<Box>
|
<Stack gap="xs">
|
||||||
<Box>
|
|
||||||
<Stack>
|
|
||||||
<Title order={3}>Edit Sejarah Desa</Title>
|
<Title order={3}>Edit Sejarah Desa</Title>
|
||||||
|
|
||||||
|
{/* Judul */}
|
||||||
<TextInput
|
<TextInput
|
||||||
label={<Text fz={"md"} fw={"bold"}>Judul</Text>}
|
label={<Text fw="bold">Judul</Text>}
|
||||||
placeholder="Judul"
|
placeholder="Judul sejarah"
|
||||||
value={sejarahState.update.form.judul}
|
value={sejarahState.update.form.judul}
|
||||||
onChange={(e) => sejarahState.update.form.judul = e.currentTarget.value}
|
onChange={(e) => sejarahState.update.form.judul = e.currentTarget.value}
|
||||||
error={!sejarahState.update.form.judul && "Judul wajib diisi"}
|
error={!sejarahState.update.form.judul && "Judul wajib diisi"}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Deskripsi */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"md"} fw={"bold"}>Deskripsi</Text>
|
<Text fz={"md"} fw={"bold"}>Deskripsi</Text>
|
||||||
<EditEditor
|
<EditEditor
|
||||||
@@ -105,18 +129,23 @@ function Page() {
|
|||||||
onChange={(val) => sejarahState.update.form.deskripsi = val}
|
onChange={(val) => sejarahState.update.form.deskripsi = val}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Buttons */}
|
||||||
<Group>
|
<Group>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSubmit}
|
|
||||||
bg={colors['blue-button']}
|
bg={colors['blue-button']}
|
||||||
|
onClick={handleSubmit}
|
||||||
|
loading={isSubmitting || sejarahState.update.loading}
|
||||||
|
disabled={!sejarahState.update.form.judul}
|
||||||
>
|
>
|
||||||
Submit
|
{isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button variant="outline" onClick={handleBack} disabled={isSubmitting || sejarahState.update.loading}>
|
||||||
|
Batal
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
</Paper>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
||||||
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
|
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Group, Paper, Stack, Text, Title } from '@mantine/core';
|
import { Alert, Box, Button, Center, Group, Paper, Stack, Text, Title, Tooltip } from '@mantine/core';
|
||||||
import { IconArrowBack } from '@tabler/icons-react';
|
import { IconAlertCircle, IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
@@ -16,6 +16,7 @@ function Page() {
|
|||||||
const params = useParams()
|
const params = useParams()
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Load data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
@@ -25,9 +26,12 @@ function Page() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
const data = await visiMisiState.findUnique.load(id);
|
const data = await visiMisiState.findUnique.load(id);
|
||||||
if (data) {
|
|
||||||
visiMisiState.update.initialize(data);
|
visiMisiState.update.initialize(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading visi misi:", error);
|
||||||
|
toast.error("Gagal memuat data visi misi desa");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -35,40 +39,37 @@ function Page() {
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
visiMisiState.update.reset();
|
visiMisiState.update.reset();
|
||||||
visiMisiState.findUnique.reset(); // opsional: reset juga data lama
|
visiMisiState.findUnique.reset();
|
||||||
};
|
};
|
||||||
}, [params?.id, router]);
|
}, [params?.id, router]);
|
||||||
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (isSubmitting || !visiMisiState.update.form.visi.trim()) {
|
if (isSubmitting || !visiMisiState.update.form.visi.trim()) {
|
||||||
toast.error("Visi wajib diisi");
|
toast.error("Visi wajib diisi");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIsSubmitting(true)
|
|
||||||
|
setIsSubmitting(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const success = await visiMisiState.update.submit()
|
const success = await visiMisiState.update.submit();
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
toast.success("Data berhasil disimpan");
|
toast.success("Data berhasil disimpan");
|
||||||
router.push("/admin/desa/profile/profile-desa");
|
router.push("/admin/desa/profile/profile-desa");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error update sejarah desa:", error);
|
console.error("Error update visi misi desa:", error);
|
||||||
toast.error("Terjadi kesalahan saat update sejarah desa");
|
toast.error("Terjadi kesalahan saat update visi misi desa");
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleBack = () => {
|
const handleBack = () => router.back();
|
||||||
router.back()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
// Loading state
|
||||||
visiMisiState.findUnique.loading ||
|
if (visiMisiState.findUnique.loading || visiMisiState.update.loading) {
|
||||||
!visiMisiState.findUnique.data ||
|
|
||||||
visiMisiState.update.loading
|
|
||||||
) {
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Center h={400}>
|
<Center h={400}>
|
||||||
@@ -77,25 +78,50 @@ function Page() {
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Error state
|
||||||
|
if (visiMisiState.findUnique.error) {
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Stack gap={'xs'}>
|
<Stack gap="md">
|
||||||
<Group>
|
|
||||||
<Button variant="subtle" onClick={handleBack}>
|
<Button variant="subtle" onClick={handleBack}>
|
||||||
<IconArrowBack color={colors['blue-button']} size={20} />
|
<IconArrowBack color={colors['blue-button']} size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
|
<Alert icon={<IconAlertCircle size={16} />} color="red">
|
||||||
|
<Text fw="bold">Error</Text>
|
||||||
|
<Text>{visiMisiState.findUnique.error}</Text>
|
||||||
|
</Alert>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Group mb="md">
|
||||||
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
|
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Title order={4} ml="sm" c="dark">Edit Visi Misi Desa</Title>
|
||||||
</Group>
|
</Group>
|
||||||
<Paper bg={colors['white-1']} p={'xs'} w={{ base: '100%', md: '50%' }}>
|
|
||||||
<Stack gap={'xs'}>
|
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p="md" radius="md" shadow="sm" style={{ border: '1px solid #e0e0e0' }}>
|
||||||
<Box>
|
<Stack gap="xs">
|
||||||
<Box>
|
|
||||||
<Stack>
|
|
||||||
<Title order={3}>Edit Visi Misi Desa</Title>
|
<Title order={3}>Edit Visi Misi Desa</Title>
|
||||||
|
|
||||||
|
{/* Visi */}
|
||||||
|
<Box>
|
||||||
<Text fz={"md"} fw={"bold"}>Visi</Text>
|
<Text fz={"md"} fw={"bold"}>Visi</Text>
|
||||||
<EditEditor
|
<EditEditor
|
||||||
value={visiMisiState.update.form.visi}
|
value={visiMisiState.update.form.visi}
|
||||||
onChange={(val) => visiMisiState.update.form.visi = val}
|
onChange={(val) => visiMisiState.update.form.visi = val}
|
||||||
/>
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Misi */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"md"} fw={"bold"}>Misi</Text>
|
<Text fz={"md"} fw={"bold"}>Misi</Text>
|
||||||
<EditEditor
|
<EditEditor
|
||||||
@@ -103,18 +129,23 @@ function Page() {
|
|||||||
onChange={(val) => visiMisiState.update.form.misi = val}
|
onChange={(val) => visiMisiState.update.form.misi = val}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Buttons */}
|
||||||
<Group>
|
<Group>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSubmit}
|
|
||||||
bg={colors['blue-button']}
|
bg={colors['blue-button']}
|
||||||
|
onClick={handleSubmit}
|
||||||
|
loading={isSubmitting || visiMisiState.update.loading}
|
||||||
|
disabled={!visiMisiState.update.form.visi}
|
||||||
>
|
>
|
||||||
Submit
|
{isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button variant="outline" onClick={handleBack} disabled={isSubmitting || visiMisiState.update.loading}>
|
||||||
|
Batal
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
</Paper>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Card, Center, Grid, GridCol, Group, Image, Paper, Stack, Text, Title } from '@mantine/core';
|
import { Box, Button, Card, Center, Divider, Grid, GridCol, Image, Paper, SimpleGrid, Stack, Text, Title, Tooltip } from '@mantine/core';
|
||||||
import { useSnapshot } from 'valtio';
|
import { useSnapshot } from 'valtio';
|
||||||
import stateProfileDesa from '../../../_state/desa/profile';
|
import stateProfileDesa from '../../../_state/desa/profile';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
@@ -12,7 +12,6 @@ function Page() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const snap = useSnapshot(stateProfileDesa);
|
const snap = useSnapshot(stateProfileDesa);
|
||||||
|
|
||||||
// Panggil load data sekali saat komponen mount
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
stateProfileDesa.sejarahDesa.findUnique.load("edit");
|
stateProfileDesa.sejarahDesa.findUnique.load("edit");
|
||||||
stateProfileDesa.visiMisiDesa.findUnique.load("edit");
|
stateProfileDesa.visiMisiDesa.findUnique.load("edit");
|
||||||
@@ -26,142 +25,219 @@ function Page() {
|
|||||||
const maskot = snap.maskotDesa.findUnique.data;
|
const maskot = snap.maskotDesa.findUnique.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
|
||||||
<Stack gap={"lg"}>
|
<Stack gap="lg">
|
||||||
<Title order={2}>Preview Profile Desa</Title>
|
<Title order={2} c={colors['blue-button']}>Preview Profile Desa</Title>
|
||||||
|
|
||||||
{/* Sejarah Desa */}
|
{/* Sejarah Desa */}
|
||||||
{sejarah && (
|
{sejarah && (
|
||||||
<Box>
|
<Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">
|
||||||
<Stack gap={'lg'}>
|
<Grid align="center">
|
||||||
<Paper p={"md"} bg={colors['BG-trans']}>
|
|
||||||
<Grid>
|
|
||||||
<GridCol span={{ base: 12, md: 11 }}>
|
<GridCol span={{ base: 12, md: 11 }}>
|
||||||
<Title order={2}>Preview Sejarah Desa</Title>
|
<Title order={3} c={colors['blue-button']}>Preview Sejarah Desa</Title>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
<GridCol span={{ base: 12, md: 1 }}>
|
<GridCol span={{ base: 12, md: 1 }}>
|
||||||
<Button bg={colors['blue-button']} onClick={() => router.push(`/admin/desa/profile/profile-desa/${sejarah.id}/sejarah_desa`)}>
|
<Tooltip label="Edit Sejarah Desa" withArrow>
|
||||||
<IconEdit size={20} />
|
<Button
|
||||||
|
c="green"
|
||||||
|
variant="light"
|
||||||
|
leftSection={<IconEdit size={18} stroke={2} />}
|
||||||
|
radius="md"
|
||||||
|
onClick={() => router.push(`/admin/desa/profile/profile-desa/${sejarah.id}/sejarah_desa`)}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Box pb={30}>
|
|
||||||
|
<Box px={{ base: 0, md: 50 }} py="xl">
|
||||||
|
<Paper bg={colors['white-1']} withBorder radius="md" shadow="xs" p="lg">
|
||||||
|
<Stack gap={0}>
|
||||||
<Center>
|
<Center>
|
||||||
<Image src={"/darmasaba-icon.png"} alt="" w={{ base: 200, md: 300 }} />
|
<Image src="/darmasaba-icon.png" w={{ base: 150, md: 250 }} alt="Logo Desa" />
|
||||||
</Center>
|
</Center>
|
||||||
<Text c={colors['blue-button']} ta={"center"} fw={"bold"} fz={"2.5rem"}>{sejarah.judul}</Text>
|
<Paper
|
||||||
</Box>
|
bg={colors['blue-button']}
|
||||||
<Paper p={"xl"} bg={colors['white-trans-1']}>
|
py="md"
|
||||||
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: sejarah.deskripsi }} />
|
px="sm"
|
||||||
</Paper>
|
radius="md"
|
||||||
|
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" }}>
|
||||||
|
{sejarah.judul}
|
||||||
|
</Text>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
<Divider my="md" color={colors['blue-button']} />
|
||||||
|
<Text fz={{ base: "md", md: "h3" }} ta="justify" dangerouslySetInnerHTML={{ __html: sejarah.deskripsi }} />
|
||||||
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
</Paper>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Visi Misi Desa */}
|
{/* Visi Misi Desa */}
|
||||||
{visiMisi && (
|
{visiMisi && (
|
||||||
<Box>
|
<Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">
|
||||||
<Stack gap={'lg'}>
|
<Grid align="center">
|
||||||
<Paper p={"md"} bg={colors['BG-trans']}>
|
|
||||||
<Grid>
|
|
||||||
<GridCol span={{ base: 12, md: 11 }}>
|
<GridCol span={{ base: 12, md: 11 }}>
|
||||||
<Title order={2}>Preview Visi Misi Desa</Title>
|
<Title order={3} c={colors['blue-button']}>Preview Visi Misi Desa</Title>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
<GridCol span={{ base: 12, md: 1 }}>
|
<GridCol span={{ base: 12, md: 1 }}>
|
||||||
<Button onClick={() => router.push(`/admin/desa/profile/profile-desa/${visiMisi.id}/visi_misi_desa`)} bg={colors['blue-button']}>
|
<Tooltip label="Edit Visi Misi Desa" withArrow>
|
||||||
<IconEdit size={20} />
|
<Button
|
||||||
|
c="green"
|
||||||
|
variant="light"
|
||||||
|
leftSection={<IconEdit size={18} stroke={2} />}
|
||||||
|
radius="md"
|
||||||
|
onClick={() => router.push(`/admin/desa/profile/profile-desa/${visiMisi.id}/visi_misi_desa`)}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Box pb={30}>
|
|
||||||
|
<Box px={{ base: 0, md: 50 }} py="xl">
|
||||||
|
<Paper bg={colors['white-1']} withBorder radius="md" shadow="xs" p="lg">
|
||||||
|
<Stack gap={0}>
|
||||||
<Center>
|
<Center>
|
||||||
<Image src={"/darmasaba-icon.png"} alt="" w={{ base: 200, md: 300 }} />
|
<Image src="/darmasaba-icon.png" w={{ base: 150, md: 250 }} alt="Logo Desa" />
|
||||||
</Center>
|
</Center>
|
||||||
<Text c={colors['blue-button']} ta={"center"} fw={"bold"} fz={"2.5rem"}>Visi Misi Desa</Text>
|
<Paper
|
||||||
</Box>
|
bg={colors['blue-button']}
|
||||||
<Paper p={"xl"} bg={colors['white-trans-1']}>
|
py="md"
|
||||||
<Text fw={"bold"} fz={{ base: "lg", md: "h2" }}>Visi Desa</Text>
|
px="sm"
|
||||||
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: visiMisi.visi }} />
|
radius="md"
|
||||||
<Text fw={"bold"} fz={{ base: "lg", md: "h2" }}>Misi Desa</Text>
|
style={{ mt: -30, boxShadow: '0 4px 20px rgba(0,0,0,0.15)' }}
|
||||||
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: visiMisi.misi }} />
|
>
|
||||||
</Paper>
|
<Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "1.2rem", md: "1.6rem" }}>
|
||||||
|
Visi Misi Desa
|
||||||
|
</Text>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
<Divider my="md" color={colors['blue-button']} />
|
||||||
|
<Text fw="bold" fz={{ base: "lg", md: "h2" }}>Visi Desa</Text>
|
||||||
|
<Text fz={{ base: "md", md: "h3" }} ta="justify" dangerouslySetInnerHTML={{ __html: visiMisi.visi }} />
|
||||||
|
<Text fw="bold" fz={{ base: "lg", md: "h2" }}>Misi Desa</Text>
|
||||||
|
<Text fz={{ base: "md", md: "h3" }} ta="justify" dangerouslySetInnerHTML={{ __html: visiMisi.misi }} />
|
||||||
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
</Paper>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Lambang Desa */}
|
{/* Lambang Desa */}
|
||||||
{lambang && (
|
{lambang && (
|
||||||
<Box>
|
<Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">
|
||||||
<Stack gap={'lg'}>
|
<Grid align="center">
|
||||||
<Paper p={"md"} bg={colors['BG-trans']}>
|
|
||||||
<Grid>
|
|
||||||
<GridCol span={{ base: 12, md: 11 }}>
|
<GridCol span={{ base: 12, md: 11 }}>
|
||||||
<Title order={2}>Preview Lambang Desa</Title>
|
<Title order={3} c={colors['blue-button']}>Preview Lambang Desa</Title>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
<GridCol span={{ base: 12, md: 1 }}>
|
<GridCol span={{ base: 12, md: 1 }}>
|
||||||
<Button onClick={() => router.push(`/admin/desa/profile/profile-desa/${lambang.id}/lambang_desa`)} bg={colors['blue-button']}>
|
<Tooltip label="Edit Lambang Desa" withArrow>
|
||||||
<IconEdit size={20} />
|
<Button
|
||||||
|
c="green"
|
||||||
|
variant="light"
|
||||||
|
leftSection={<IconEdit size={18} stroke={2} />}
|
||||||
|
radius="md"
|
||||||
|
onClick={() => router.push(`/admin/desa/profile/profile-desa/${lambang.id}/lambang_desa`)}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Box pb={30}>
|
|
||||||
|
<Box px={{ base: 0, md: 50 }} py="xl">
|
||||||
|
<Paper bg={colors['white-1']} withBorder radius="md" shadow="xs" p="lg">
|
||||||
|
<Stack gap={0}>
|
||||||
<Center>
|
<Center>
|
||||||
<Image src={"/darmasaba-icon.png"} alt="" w={{ base: 200, md: 300 }} />
|
<Image src="/darmasaba-icon.png" w={{ base: 150, md: 250 }} alt="Logo Desa" />
|
||||||
</Center>
|
</Center>
|
||||||
<Text c={colors['blue-button']} ta={"center"} fw={"bold"} fz={"2.5rem"}>Lambang Desa</Text>
|
<Paper
|
||||||
</Box>
|
bg={colors['blue-button']}
|
||||||
<Paper p={"xl"} bg={colors['white-trans-1']}>
|
py="md"
|
||||||
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: lambang.deskripsi }} />
|
px="sm"
|
||||||
</Paper>
|
radius="md"
|
||||||
|
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" }}>
|
||||||
|
Lambang Desa
|
||||||
|
</Text>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
<Divider my="md" color={colors['blue-button']} />
|
||||||
|
<Text fz={{ base: "md", md: "h3" }} ta="justify" dangerouslySetInnerHTML={{ __html: lambang.deskripsi }} />
|
||||||
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
</Paper>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Maskot Desa */}
|
{/* Maskot Desa */}
|
||||||
{maskot && (
|
{maskot && (
|
||||||
<Box>
|
<Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">
|
||||||
<Stack gap={'lg'}>
|
<Grid align="center">
|
||||||
<Paper p={"md"} bg={colors['BG-trans']}>
|
|
||||||
<Grid>
|
|
||||||
<GridCol span={{ base: 12, md: 11 }}>
|
<GridCol span={{ base: 12, md: 11 }}>
|
||||||
<Title order={2}>Preview Maskot Desa</Title>
|
<Title order={3} c={colors['blue-button']}>Preview Maskot Desa</Title>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
<GridCol span={{ base: 12, md: 1 }}>
|
<GridCol span={{ base: 12, md: 1 }}>
|
||||||
<Button onClick={() => router.push(`/admin/desa/profile/profile-desa/${maskot.id}/maskot_desa`)} bg={colors['blue-button']}>
|
<Tooltip label="Edit Maskot Desa" withArrow>
|
||||||
<IconEdit size={20} />
|
<Button
|
||||||
|
c="green"
|
||||||
|
variant="light"
|
||||||
|
leftSection={<IconEdit size={18} stroke={2} />}
|
||||||
|
radius="md"
|
||||||
|
onClick={() => router.push(`/admin/desa/profile/profile-desa/${maskot.id}/maskot_desa`)}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Box pb={30}>
|
|
||||||
|
<Box px={{ base: 0, md: 50 }} py="xl">
|
||||||
|
<Paper bg={colors['white-1']} withBorder radius="md" shadow="xs" p="lg">
|
||||||
|
<Stack gap={0}>
|
||||||
<Center>
|
<Center>
|
||||||
<Image src={"/pudak-icon.png"} alt="" w={{ base: 200, md: 300 }} />
|
<Image src="/pudak-icon.png" w={{ base: 150, md: 250 }} alt="Maskot Desa" />
|
||||||
</Center>
|
</Center>
|
||||||
<Text c={colors['blue-button']} ta={"center"} fw={"bold"} fz={"2.5rem"}>Maskot Desa</Text>
|
<Paper
|
||||||
</Box>
|
bg={colors['blue-button']}
|
||||||
<Paper p={"xl"} bg={colors['white-trans-1']}>
|
py="md"
|
||||||
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: maskot.deskripsi }} />
|
px="sm"
|
||||||
<Group wrap="wrap" gap="md">
|
radius="md"
|
||||||
{maskot.images.map((img, index) => (
|
style={{ mt: -30, boxShadow: '0 4px 20px rgba(0,0,0,0.15)' }}
|
||||||
<Card key={index} p="xs" w={220}>
|
>
|
||||||
|
<Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "1.2rem", md: "1.6rem" }}>
|
||||||
|
Maskot Desa
|
||||||
|
</Text>
|
||||||
|
</Paper>
|
||||||
|
</Stack>
|
||||||
|
<Divider my="md" color={colors['blue-button']} />
|
||||||
|
<Text fz={{ base: "md", md: "h3" }} ta="justify" dangerouslySetInnerHTML={{ __html: maskot.deskripsi }} />
|
||||||
|
<Stack mt="md" gap="sm">
|
||||||
|
<SimpleGrid cols={{ base: 2, md: 4 }} spacing="md">
|
||||||
|
{maskot.images.map((img, idx) => (
|
||||||
|
<Card withBorder key={idx} p="xs" w={{ base: '100%', md: 180 }}>
|
||||||
|
<Center>
|
||||||
<Image
|
<Image
|
||||||
src={img.image.link}
|
src={img.image.link}
|
||||||
alt={img.label}
|
alt={img.label}
|
||||||
w={200}
|
w={150}
|
||||||
h={200}
|
h={150}
|
||||||
fit="cover"
|
fit="cover"
|
||||||
radius="md"
|
radius="md"
|
||||||
style={{ border: '1px solid #ccc', objectFit: 'cover' }}
|
style={{ border: '1px solid #ccc' }}
|
||||||
/>
|
/>
|
||||||
|
</Center>
|
||||||
<Text ta="center" mt="xs" fw="bold">{img.label}</Text>
|
<Text ta="center" mt="xs" fw="bold">{img.label}</Text>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
</Group>
|
</SimpleGrid>
|
||||||
</Paper>
|
|
||||||
</Paper>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
</Paper>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -3,17 +3,27 @@
|
|||||||
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
|
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import ApiFetch from '@/lib/api-fetch';
|
import ApiFetch from '@/lib/api-fetch';
|
||||||
import { Box, Button, Center, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Image,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Tooltip
|
||||||
|
} from '@mantine/core';
|
||||||
import { Dropzone } from '@mantine/dropzone';
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
import { IconArrowBack, IconImageInPicture, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
|
|
||||||
function EditPerbekelDariMasaKeMasa() {
|
function EditPerbekelDariMasaKeMasa() {
|
||||||
const state = useProxy(stateProfileDesa.mantanPerbekel)
|
const state = useProxy(stateProfileDesa.mantanPerbekel);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
@@ -38,9 +48,7 @@ function EditPerbekelDariMasaKeMasa() {
|
|||||||
periode: data.periode || '',
|
periode: data.periode || '',
|
||||||
imageId: data.imageId || ''
|
imageId: data.imageId || ''
|
||||||
});
|
});
|
||||||
if (data?.imageGalleryFoto?.link) {
|
if (data?.imageGalleryFoto?.link) setPreviewImage(data.imageGalleryFoto.link);
|
||||||
setPreviewImage(data.imageGalleryFoto.link);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading foto:', error);
|
console.error('Error loading foto:', error);
|
||||||
@@ -52,24 +60,18 @@ function EditPerbekelDariMasaKeMasa() {
|
|||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
state.update.form = {
|
state.update.form = { ...state.update.form, ...formData };
|
||||||
...state.update.form,
|
|
||||||
nama: formData.nama,
|
|
||||||
daerah: formData.daerah,
|
|
||||||
periode: formData.periode,
|
|
||||||
imageId: formData.imageId
|
|
||||||
};
|
|
||||||
if (file) {
|
if (file) {
|
||||||
const res = await ApiFetch.api.fileStorage.create.post({
|
const res = await ApiFetch.api.fileStorage.create.post({
|
||||||
file,
|
file,
|
||||||
name: file.name,
|
name: file.name,
|
||||||
});
|
});
|
||||||
const uploaded = res.data?.data;
|
const uploaded = res.data?.data;
|
||||||
if (!uploaded?.id) {
|
if (!uploaded?.id) return toast.error("Gagal upload gambar");
|
||||||
return toast.error("Gagal upload gambar");
|
|
||||||
}
|
|
||||||
state.update.form.imageId = uploaded.id;
|
state.update.form.imageId = uploaded.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
await state.update.update();
|
await state.update.update();
|
||||||
toast.success('Perbekel dari masa ke masa berhasil diperbarui!');
|
toast.success('Perbekel dari masa ke masa berhasil diperbarui!');
|
||||||
router.push('/admin/desa/profile/profile-perbekel-dari-masa-ke-masa');
|
router.push('/admin/desa/profile/profile-perbekel-dari-masa-ke-masa');
|
||||||
@@ -80,86 +82,119 @@ function EditPerbekelDariMasaKeMasa() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
<Group mb="md">
|
||||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Tooltip>
|
||||||
|
<Title order={4} ml="sm" c="dark">
|
||||||
|
Edit Perbekel Dari Masa Ke Masa
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
<Paper
|
||||||
<Stack gap={"xs"}>
|
w={{ base: '100%', md: '50%' }}
|
||||||
<Title order={4}>Edit Perbekel Dari Masa Ke Masa</Title>
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Nama</Text>}
|
label="Nama"
|
||||||
placeholder='Masukkan nama'
|
placeholder="Masukkan nama"
|
||||||
value={formData.nama}
|
value={formData.nama}
|
||||||
onChange={(e) =>
|
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
|
||||||
(formData.nama = e.target.value)
|
required
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text>Upload Foto</Text>
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
|
Foto Perbekel
|
||||||
|
</Text>
|
||||||
<Dropzone
|
<Dropzone
|
||||||
onDrop={(files) => {
|
onDrop={(files) => {
|
||||||
const selectedFile = files[0]; // Ambil file pertama
|
const selectedFile = files[0];
|
||||||
if (selectedFile) {
|
if (selectedFile) {
|
||||||
setFile(selectedFile);
|
setFile(selectedFile);
|
||||||
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
|
setPreviewImage(URL.createObjectURL(selectedFile));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onReject={() => toast.error('File tidak valid.')}
|
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
||||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
maxSize={5 * 1024 ** 2}
|
||||||
accept={{ 'image/*': [] }}
|
accept={{ 'image/*': [] }}
|
||||||
|
radius="md"
|
||||||
|
p="xl"
|
||||||
>
|
>
|
||||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
<Group justify="center" gap="xl" mih={180}>
|
||||||
<Dropzone.Accept>
|
<Dropzone.Accept>
|
||||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
|
||||||
</Dropzone.Accept>
|
</Dropzone.Accept>
|
||||||
<Dropzone.Reject>
|
<Dropzone.Reject>
|
||||||
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
<IconX size={48} color="red" stroke={1.5} />
|
||||||
</Dropzone.Reject>
|
</Dropzone.Reject>
|
||||||
<Dropzone.Idle>
|
<Dropzone.Idle>
|
||||||
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
<IconPhoto size={48} color="#868e96" stroke={1.5} />
|
||||||
</Dropzone.Idle>
|
</Dropzone.Idle>
|
||||||
|
<Stack gap="xs" align="center">
|
||||||
<div>
|
<Text size="md" fw={500}>
|
||||||
<Text size="xl" inline>
|
Seret gambar atau klik untuk memilih file
|
||||||
Drag gambar ke sini atau klik untuk pilih file
|
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="sm" c="dimmed" inline mt={7}>
|
<Text size="sm" c="dimmed">
|
||||||
Maksimal 5MB dan harus format gambar
|
Maksimal 5MB, format gambar wajib
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</Stack>
|
||||||
</Group>
|
</Group>
|
||||||
</Dropzone>
|
</Dropzone>
|
||||||
|
|
||||||
{previewImage ? (
|
{previewImage && (
|
||||||
<Image alt="" src={previewImage} w={200} h={200} />
|
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
|
||||||
) : (
|
<Image
|
||||||
<Center w={200} h={200} bg={"gray"}>
|
src={previewImage}
|
||||||
<IconImageInPicture />
|
alt="Preview Gambar"
|
||||||
</Center>
|
radius="md"
|
||||||
|
style={{
|
||||||
|
maxHeight: 220,
|
||||||
|
objectFit: 'contain',
|
||||||
|
border: `1px solid ${colors['blue-button']}`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Daerah</Text>}
|
label="Daerah"
|
||||||
placeholder='Masukkan daerah'
|
placeholder="Masukkan daerah"
|
||||||
value={formData.daerah}
|
value={formData.daerah}
|
||||||
onChange={(e) =>
|
onChange={(e) => setFormData({ ...formData, daerah: e.target.value })}
|
||||||
(formData.daerah = e.target.value)
|
required
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Periode</Text>}
|
label="Periode"
|
||||||
placeholder='Masukkan periode'
|
placeholder="Masukkan periode"
|
||||||
value={formData.periode}
|
value={formData.periode}
|
||||||
onChange={(e) =>
|
onChange={(e) => setFormData({ ...formData, periode: e.target.value })}
|
||||||
(formData.periode = e.target.value)
|
required
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<Group>
|
|
||||||
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
|
color: '#fff',
|
||||||
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||||
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
|
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
|
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
@@ -10,99 +10,130 @@ import { useState } from 'react';
|
|||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
function DetailPerbekelDariMasa() {
|
function DetailPerbekelDariMasa() {
|
||||||
const state = useProxy(stateProfileDesa.mantanPerbekel)
|
const state = useProxy(stateProfileDesa.mantanPerbekel);
|
||||||
const [modalHapus, setModalHapus] = useState(false);
|
const [modalHapus, setModalHapus] = useState(false);
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
const params = useParams()
|
const params = useParams();
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
state.findUnique.load(params?.id as string)
|
state.findUnique.load(params?.id as string);
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
const handleHapus = () => {
|
const handleHapus = () => {
|
||||||
if (selectedId) {
|
if (selectedId) {
|
||||||
state.delete.byId(selectedId)
|
state.delete.byId(selectedId);
|
||||||
setModalHapus(false)
|
setModalHapus(false);
|
||||||
setSelectedId(null)
|
setSelectedId(null);
|
||||||
router.push("/admin/desa/profile/profile-perbekel-dari-masa-ke-masa")
|
router.push("/admin/desa/profile/profile-perbekel-dari-masa-ke-masa");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (!state.findUnique.data) {
|
if (!state.findUnique.data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton h={500} />
|
<Skeleton height={500} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = state.findUnique.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box py={10}>
|
||||||
<Box mb={10}>
|
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
|
|
||||||
<Stack>
|
|
||||||
<Text fz={"xl"} fw={"bold"}>Detail Perbekel Dari Masa Ke Masa</Text>
|
|
||||||
{state.findUnique.data ? (
|
|
||||||
<Paper key={state.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
|
||||||
<Stack gap={"xs"}>
|
|
||||||
<Box>
|
|
||||||
<Text fw={"bold"} fz={"lg"}>Nama Perbekel</Text>
|
|
||||||
<Text fz={"lg"}>{state.findUnique.data?.nama}</Text>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text fw={"bold"} fz={"lg"}>Daerah</Text>
|
|
||||||
<Text fz={"lg"}>{state.findUnique.data?.daerah}</Text>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text fw={"bold"} fz={"lg"}>Periode</Text>
|
|
||||||
<Text fz={"lg"}>{state.findUnique.data?.periode}</Text>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
|
|
||||||
<Image w={{ base: 300, md: 350}} src={state.findUnique.data?.image?.link} alt="gambar" />
|
|
||||||
</Box>
|
|
||||||
<Flex gap={"xs"} mt={10}>
|
|
||||||
<Button
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
|
||||||
|
mb={15}
|
||||||
|
>
|
||||||
|
Kembali
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Paper
|
||||||
|
withBorder
|
||||||
|
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 Perbekel Dari Masa Ke Masa
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">Gambar</Text>
|
||||||
|
{data.image?.link ? (
|
||||||
|
<Image
|
||||||
|
src={data.image.link}
|
||||||
|
alt={data.nama || 'Gambar Perbekel'}
|
||||||
|
w={150}
|
||||||
|
h={150}
|
||||||
|
radius="md"
|
||||||
|
fit="cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">Nama Perbekel</Text>
|
||||||
|
<Text fz="md" c="dimmed">{data.nama || '-'}</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">Daerah</Text>
|
||||||
|
<Text fz="md" c="dimmed">{data.daerah || '-'}</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">Periode</Text>
|
||||||
|
<Text fz="md" c="dimmed">{data.periode || '-'}</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Group gap="sm">
|
||||||
|
<Tooltip label="Hapus Perbekel" withArrow position="top">
|
||||||
|
<Button
|
||||||
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (state.findUnique.data) {
|
setSelectedId(data.id);
|
||||||
setSelectedId(state.findUnique.data.id);
|
|
||||||
setModalHapus(true);
|
setModalHapus(true);
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
disabled={state.delete.loading || !state.findUnique.data}
|
variant="light"
|
||||||
color={"red"}
|
radius="md"
|
||||||
|
size="md"
|
||||||
>
|
>
|
||||||
<IconX size={20} />
|
<IconX size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip label="Edit Perbekel" withArrow position="top">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
color="green"
|
||||||
if (state.findUnique.data) {
|
onClick={() => router.push(`/admin/desa/profile/profile-perbekel-dari-masa-ke-masa/${data.id}/edit`)}
|
||||||
router.push(`/admin/desa/profile/profile-perbekel-dari-masa-ke-masa/${state.findUnique.data.id}/edit`);
|
variant="light"
|
||||||
}
|
radius="md"
|
||||||
}}
|
size="md"
|
||||||
disabled={!state.findUnique.data}
|
|
||||||
color={"green"}
|
|
||||||
>
|
>
|
||||||
<IconEdit size={20} />
|
<IconEdit size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
) : null}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{/* Modal Konfirmasi Hapus */}
|
|
||||||
<ModalKonfirmasiHapus
|
<ModalKonfirmasiHapus
|
||||||
opened={modalHapus}
|
opened={modalHapus}
|
||||||
onClose={() => setModalHapus(false)}
|
onClose={() => setModalHapus(false)}
|
||||||
onConfirm={handleHapus}
|
onConfirm={handleHapus}
|
||||||
text='Apakah anda yakin ingin menghapus perbekel dari masa ke masa ini?'
|
text="Apakah Anda yakin ingin menghapus perbekel dari masa ke masa ini?"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
'use client'
|
'use client';
|
||||||
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
|
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import ApiFetch from '@/lib/api-fetch';
|
import ApiFetch from '@/lib/api-fetch';
|
||||||
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
|
||||||
import { Dropzone } from '@mantine/dropzone';
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
@@ -10,29 +10,26 @@ import { useState } from 'react';
|
|||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
|
function CreatePerbekelDariMasaKeMasa() {
|
||||||
|
const state = useProxy(stateProfileDesa.mantanPerbekel);
|
||||||
function CreateVideo() {
|
|
||||||
const state = useProxy(stateProfileDesa.mantanPerbekel)
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
state.create.form = {
|
state.create.form = {
|
||||||
nama: "",
|
nama: '',
|
||||||
daerah: "",
|
daerah: '',
|
||||||
periode: "",
|
periode: '',
|
||||||
imageId: "",
|
imageId: '',
|
||||||
};
|
};
|
||||||
setPreviewImage(null)
|
setPreviewImage(null);
|
||||||
setFile(null)
|
setFile(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return toast.warn("Pilih file gambar terlebih dahulu");
|
return toast.warn('Pilih file gambar terlebih dahulu');
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await ApiFetch.api.fileStorage.create.post({
|
const res = await ApiFetch.api.fileStorage.create.post({
|
||||||
@@ -41,110 +38,118 @@ function CreateVideo() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const uploaded = res.data?.data;
|
const uploaded = res.data?.data;
|
||||||
if (!uploaded?.id) {
|
if (!uploaded?.id) return toast.error('Gagal upload gambar');
|
||||||
return toast.error("Gagal upload gambar");
|
|
||||||
}
|
|
||||||
state.create.form.imageId = uploaded.id;
|
state.create.form.imageId = uploaded.id;
|
||||||
await state.create.create();
|
await state.create.create();
|
||||||
resetForm();
|
resetForm();
|
||||||
router.push("/admin/desa/profile/profile-perbekel-dari-masa-ke-masa");
|
router.push('/admin/desa/profile/profile-perbekel-dari-masa-ke-masa');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Back button + Title */}
|
||||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Tooltip>
|
||||||
|
<Title order={4} ml="sm" c="dark">
|
||||||
|
Create Perbekel Dari Masa Ke Masa
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
<Paper
|
||||||
<Stack gap={"xs"}>
|
w={{ base: '100%', md: '50%' }}
|
||||||
<Title order={4}>Create Perbekel Dari Masa Ke Masa</Title>
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Nama Perbekel</Text>}
|
label={<Text fw="bold" fz="sm">Nama Perbekel</Text>}
|
||||||
placeholder='Masukkan nama perbekel'
|
placeholder="Masukkan nama perbekel"
|
||||||
value={state.create.form.nama}
|
value={state.create.form.nama}
|
||||||
onChange={(val) => {
|
onChange={(e) => (state.create.form.nama = e.target.value)}
|
||||||
state.create.form.nama = val.target.value;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
label="Daerah"
|
|
||||||
placeholder="Masukkan daerah"
|
|
||||||
value={state.create.form.daerah}
|
|
||||||
onChange={(e) => {
|
|
||||||
state.create.form.daerah = e.currentTarget.value;
|
|
||||||
}}
|
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Periode</Text>}
|
label={<Text fw="bold" fz="sm">Daerah</Text>}
|
||||||
placeholder='Masukkan periode'
|
placeholder="Masukkan daerah"
|
||||||
value={state.create.form.periode}
|
value={state.create.form.daerah}
|
||||||
onChange={(e) =>
|
onChange={(e) => (state.create.form.daerah = e.target.value)}
|
||||||
(state.create.form.periode = e.target.value)
|
required
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
<TextInput
|
||||||
|
label={<Text fw="bold" fz="sm">Periode</Text>}
|
||||||
|
placeholder="Masukkan periode"
|
||||||
|
value={state.create.form.periode}
|
||||||
|
onChange={(e) => (state.create.form.periode = e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Dropzone */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
<Text fw="bold" fz="sm" mb={6}>Gambar Perbekel</Text>
|
||||||
<Box>
|
|
||||||
<Dropzone
|
<Dropzone
|
||||||
onDrop={(files) => {
|
onDrop={(files) => {
|
||||||
const selectedFile = files[0]; // Ambil file pertama
|
const selectedFile = files[0];
|
||||||
if (selectedFile) {
|
if (selectedFile) {
|
||||||
setFile(selectedFile);
|
setFile(selectedFile);
|
||||||
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
|
setPreviewImage(URL.createObjectURL(selectedFile));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onReject={() => toast.error('File tidak valid.')}
|
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
||||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
maxSize={5 * 1024 ** 2}
|
||||||
accept={{ 'image/*': [] }}
|
accept={{ 'image/*': [] }}
|
||||||
|
radius="md"
|
||||||
|
p="xl"
|
||||||
>
|
>
|
||||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
<Group justify="center" gap="xl" mih={180}>
|
||||||
<Dropzone.Accept>
|
<Dropzone.Accept>
|
||||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
<IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||||
</Dropzone.Accept>
|
</Dropzone.Accept>
|
||||||
<Dropzone.Reject>
|
<Dropzone.Reject>
|
||||||
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
<IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||||
</Dropzone.Reject>
|
</Dropzone.Reject>
|
||||||
<Dropzone.Idle>
|
<Dropzone.Idle>
|
||||||
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
<IconPhoto size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||||
</Dropzone.Idle>
|
</Dropzone.Idle>
|
||||||
|
|
||||||
<div>
|
|
||||||
<Text size="xl" inline>
|
|
||||||
Drag gambar ke sini atau klik untuk pilih file
|
|
||||||
</Text>
|
|
||||||
<Text size="sm" c="dimmed" inline mt={7}>
|
|
||||||
Maksimal 5MB dan harus format gambar
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</Group>
|
</Group>
|
||||||
|
<Text ta="center" mt="sm" size="sm" color="dimmed">
|
||||||
|
Seret gambar atau klik untuk memilih file (maks 5MB)
|
||||||
|
</Text>
|
||||||
</Dropzone>
|
</Dropzone>
|
||||||
|
|
||||||
{/* Tampilkan preview kalau ada */}
|
|
||||||
{previewImage && (
|
{previewImage && (
|
||||||
<Box mt="sm">
|
<Box mt="sm" style={{ textAlign: 'center' }}>
|
||||||
<Image
|
<Image
|
||||||
src={previewImage}
|
src={previewImage}
|
||||||
alt="Preview"
|
alt="Preview Gambar"
|
||||||
style={{
|
radius="md"
|
||||||
maxWidth: '100%',
|
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
|
||||||
maxHeight: '200px',
|
|
||||||
objectFit: 'contain',
|
|
||||||
borderRadius: '8px',
|
|
||||||
border: '1px solid #ddd',
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
</Box>
|
{/* Submit */}
|
||||||
</Box>
|
<Group justify="right">
|
||||||
<Group>
|
<Button
|
||||||
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
|
onClick={handleSubmit}
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
|
color: '#fff',
|
||||||
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
@@ -152,4 +157,4 @@ function CreateVideo() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CreateVideo;
|
export default CreatePerbekelDariMasaKeMasa;
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Pagination, Paper, Skeleton, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
|
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import HeaderSearch from '../../../_com/header';
|
import HeaderSearch from '../../../_com/header';
|
||||||
import JudulList from '../../../_com/judulList';
|
|
||||||
import stateProfileDesa from '../../../_state/desa/profile';
|
import stateProfileDesa from '../../../_state/desa/profile';
|
||||||
|
|
||||||
function PerbekelDariMasaKeMasa() {
|
function PerbekelDariMasaKeMasa() {
|
||||||
@@ -16,7 +15,7 @@ function PerbekelDariMasaKeMasa() {
|
|||||||
<Box>
|
<Box>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
title='Perbekel Dari Masa Ke Masa'
|
title='Perbekel Dari Masa Ke Masa'
|
||||||
placeholder='pencarian'
|
placeholder='Cari nama perbekel...'
|
||||||
searchIcon={<IconSearch size={20} />}
|
searchIcon={<IconSearch size={20} />}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
@@ -29,74 +28,96 @@ function PerbekelDariMasaKeMasa() {
|
|||||||
function ListPerbekelDariMasaKeMasa({ search }: { search: string }) {
|
function ListPerbekelDariMasaKeMasa({ search }: { search: string }) {
|
||||||
const state = useProxy(stateProfileDesa.mantanPerbekel)
|
const state = useProxy(stateProfileDesa.mantanPerbekel)
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const {
|
const { data, page, totalPages, loading, load } = state.findMany;
|
||||||
data,
|
|
||||||
page,
|
|
||||||
totalPages,
|
|
||||||
loading,
|
|
||||||
load,
|
|
||||||
} = state.findMany;
|
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
load(page, 10, search)
|
load(page, 10, search)
|
||||||
}, [page, search])
|
}, [page, search]);
|
||||||
|
|
||||||
const filteredData = (data || [])
|
const filteredData = data || [];
|
||||||
|
|
||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton h={500} />
|
<Skeleton height={500} radius="md" />
|
||||||
</Box>
|
</Stack>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={10}>
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||||
<JudulList
|
<Group justify='space-between' mb="md">
|
||||||
title='List Perbekel Dari Masa Ke Masa'
|
<Title order={4}>List Perbekel Dari Masa Ke Masa</Title>
|
||||||
href='/admin/desa/profile/profile-perbekel-dari-masa-ke-masa/create'
|
<Tooltip label="Tambah Perbekel Baru" withArrow>
|
||||||
/>
|
<Button
|
||||||
<Table striped withTableBorder withRowBorders>
|
leftSection={<IconPlus size={18} />}
|
||||||
|
color="blue"
|
||||||
|
variant="light"
|
||||||
|
onClick={() => router.push('/admin/desa/profile/profile-perbekel-dari-masa-ke-masa/create')}
|
||||||
|
>
|
||||||
|
Tambah Baru
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Box style={{ overflowX: "auto" }}>
|
||||||
|
<Table highlightOnHover>
|
||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh>Nama Perbekel</TableTh>
|
<TableTh style={{ width: '35%' }}>Nama Perbekel</TableTh>
|
||||||
<TableTh>Periode</TableTh>
|
<TableTh style={{ width: '35%' }}>Periode</TableTh>
|
||||||
<TableTh>Detail</TableTh>
|
<TableTh style={{ width: '20%' }}>Aksi</TableTh>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
</TableThead>
|
</TableThead>
|
||||||
<TableTbody>
|
<TableTbody>
|
||||||
{filteredData.map((item) => (
|
{filteredData.length > 0 ? (
|
||||||
|
filteredData.map((item) => (
|
||||||
<TableTr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Box w={200}>
|
<Text fw={500} lineClamp={1}>{item.nama}</Text>
|
||||||
<Text lineClamp={1}>{item.nama}</Text>
|
|
||||||
</Box>
|
|
||||||
</TableTd>
|
</TableTd>
|
||||||
|
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Box w={200}>
|
|
||||||
<Text lineClamp={1}>{item.periode}</Text>
|
<Text lineClamp={1}>{item.periode}</Text>
|
||||||
</Box>
|
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Button onClick={() => router.push(`/admin/desa/profile/profile-perbekel-dari-masa-ke-masa/${item.id}`)}>
|
<Button
|
||||||
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
onClick={() => router.push(`/admin/desa/profile/profile-perbekel-dari-masa-ke-masa/${item.id}`)}
|
||||||
|
>
|
||||||
<IconDeviceImac size={20} />
|
<IconDeviceImac size={20} />
|
||||||
|
<Text ml={5}>Detail</Text>
|
||||||
</Button>
|
</Button>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))}
|
))
|
||||||
|
) : (
|
||||||
|
<TableTr>
|
||||||
|
<TableTd colSpan={3}>
|
||||||
|
<Center py={20}>
|
||||||
|
<Text color="dimmed">Tidak ada data perbekel yang cocok</Text>
|
||||||
|
</Center>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
)}
|
||||||
</TableTbody>
|
</TableTbody>
|
||||||
</Table>
|
</Table>
|
||||||
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
<Center>
|
<Center>
|
||||||
<Pagination
|
<Pagination
|
||||||
value={page}
|
value={page}
|
||||||
onChange={(newPage) => load(newPage)} // ini penting!
|
onChange={(newPage) => {
|
||||||
|
load(newPage, 10);
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
}}
|
||||||
total={totalPages}
|
total={totalPages}
|
||||||
mt="md"
|
mt="md"
|
||||||
mb="md"
|
mb="md"
|
||||||
|
color="blue"
|
||||||
|
radius="md"
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
|||||||
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
|
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import ApiFetch from '@/lib/api-fetch';
|
import ApiFetch from '@/lib/api-fetch';
|
||||||
import { Box, Button, Center, Group, Image, Paper, Stack, Text, Title } from '@mantine/core';
|
import { Box, Button, Center, Group, Image, Paper, Stack, Text, Title, Tooltip } from '@mantine/core';
|
||||||
import { Dropzone } from '@mantine/dropzone';
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
import { IconArrowBack, IconPhoto, IconUpload, IconX, IconImageInPicture } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
@@ -30,22 +30,29 @@ function ProfilePerbekel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await perbekelState.findUnique.load(id);
|
const data = await perbekelState.findUnique.load(id);
|
||||||
if (data) {
|
if (data) perbekelState.edit.initialize(data);
|
||||||
perbekelState.edit.initialize(data);
|
if (data?.image?.link) setPreviewImage(data.image.link);
|
||||||
}
|
|
||||||
if (data?.image?.link) {
|
|
||||||
setPreviewImage(data.image.link);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
loadData();
|
loadData();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
perbekelState.edit.reset();
|
perbekelState.edit.reset();
|
||||||
perbekelState.findUnique.reset(); // opsional: reset juga data lama
|
perbekelState.findUnique.reset();
|
||||||
};
|
};
|
||||||
}, [params?.id, router]);
|
}, [params?.id, router]);
|
||||||
|
|
||||||
|
const handleFileChange = (newFile: File | null) => {
|
||||||
|
if (!newFile) {
|
||||||
|
setFile(null);
|
||||||
|
setPreviewImage(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setFile(newFile);
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (event) => setPreviewImage(event.target?.result as string);
|
||||||
|
reader.readAsDataURL(newFile);
|
||||||
|
}
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (isSubmitting || !perbekelState.edit.form.biodata.trim()) {
|
if (isSubmitting || !perbekelState.edit.form.biodata.trim()) {
|
||||||
@@ -62,7 +69,6 @@ function ProfilePerbekel() {
|
|||||||
toast.error("Gagal upload gambar");
|
toast.error("Gagal upload gambar");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
perbekelState.edit.form.imageId = uploaded.id;
|
perbekelState.edit.form.imageId = uploaded.id;
|
||||||
}
|
}
|
||||||
const success = await perbekelState.edit.submit()
|
const success = await perbekelState.edit.submit()
|
||||||
@@ -78,15 +84,9 @@ function ProfilePerbekel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleBack = () => {
|
const handleBack = () => router.back();
|
||||||
router.back()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (perbekelState.findUnique.loading || perbekelState.edit.loading) {
|
||||||
perbekelState.findUnique.loading ||
|
|
||||||
!perbekelState.findUnique.data ||
|
|
||||||
perbekelState.edit.loading
|
|
||||||
) {
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Center h={400}>
|
<Center h={400}>
|
||||||
@@ -98,117 +98,112 @@ function ProfilePerbekel() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={10}>
|
||||||
<Stack gap={'xs'}>
|
<Stack gap="xs">
|
||||||
<Group>
|
{/* Header */}
|
||||||
<Button variant="subtle" onClick={handleBack}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={20} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
|
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Title order={4} ml="sm" c="dark">
|
||||||
|
Edit Profil Perbekel
|
||||||
|
</Title>
|
||||||
</Group>
|
</Group>
|
||||||
<Paper bg={colors['white-1']} p={'xs'} w={{ base: '100%', md: '50%' }}>
|
|
||||||
<Stack gap={'xs'}>
|
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }} radius="md" shadow="sm" style={{ border: '1px solid #e0e0e0' }}>
|
||||||
|
<Stack gap="xs">
|
||||||
|
{/* Biodata */}
|
||||||
<Box>
|
<Box>
|
||||||
<Box>
|
<Text fz="md" fw="bold">Biodata</Text>
|
||||||
<Stack>
|
|
||||||
<Title order={3}>Edit Profil Perbekel</Title>
|
|
||||||
<Text fz={"md"} fw={"bold"}>Biodata</Text>
|
|
||||||
<EditEditor
|
<EditEditor
|
||||||
value={perbekelState.edit.form.biodata}
|
value={perbekelState.edit.form.biodata}
|
||||||
onChange={(val) => perbekelState.edit.form.biodata = val}
|
onChange={(val) => perbekelState.edit.form.biodata = val}
|
||||||
/>
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Gambar */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
<Text fz="md" fw="bold">Gambar</Text>
|
||||||
<Box>
|
|
||||||
<Dropzone
|
<Dropzone
|
||||||
onDrop={(files) => {
|
onDrop={(files) => handleFileChange(files[0])}
|
||||||
const selectedFile = files[0]; // Ambil file pertama
|
|
||||||
if (selectedFile) {
|
|
||||||
setFile(selectedFile);
|
|
||||||
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onReject={() => toast.error('File tidak valid.')}
|
onReject={() => toast.error('File tidak valid.')}
|
||||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
maxSize={5 * 1024 ** 2} // 5MB
|
||||||
accept={{ 'image/*': [] }}
|
accept={{ 'image/*': [] }}
|
||||||
>
|
>
|
||||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
||||||
<Dropzone.Accept>
|
<Dropzone.Accept><IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /></Dropzone.Accept>
|
||||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
<Dropzone.Reject><IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} /></Dropzone.Reject>
|
||||||
</Dropzone.Accept>
|
<Dropzone.Idle><IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} /></Dropzone.Idle>
|
||||||
<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>
|
<div>
|
||||||
<Text size="xl" inline>
|
<Text size="xl" inline>Drag gambar ke sini atau klik untuk pilih file</Text>
|
||||||
Drag gambar ke sini atau klik untuk pilih file
|
<Text size="sm" c="dimmed" inline mt={7}>Maksimal 5MB dan harus format gambar</Text>
|
||||||
</Text>
|
|
||||||
<Text size="sm" c="dimmed" inline mt={7}>
|
|
||||||
Maksimal 5MB dan harus format gambar
|
|
||||||
</Text>
|
|
||||||
</div>
|
</div>
|
||||||
</Group>
|
</Group>
|
||||||
</Dropzone>
|
</Dropzone>
|
||||||
|
|
||||||
{/* Tampilkan preview kalau ada */}
|
{/* Preview */}
|
||||||
{previewImage && (
|
|
||||||
<Box mt="sm">
|
<Box mt="sm">
|
||||||
<Image
|
{previewImage ? (
|
||||||
src={previewImage}
|
<Image src={previewImage} alt="Preview" w={200} h={200} fit="cover" radius="md" />
|
||||||
alt="Preview"
|
) : (
|
||||||
style={{
|
<Center w={200} h={200} bg="gray.2">
|
||||||
maxWidth: '100%',
|
<Stack align="center" gap="xs">
|
||||||
maxHeight: '200px',
|
<IconImageInPicture size={48} color="gray" />
|
||||||
objectFit: 'contain',
|
<Text size="sm" c="gray">Tidak ada gambar</Text>
|
||||||
borderRadius: '8px',
|
</Stack>
|
||||||
border: '1px solid #ddd',
|
</Center>
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Pengalaman */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"md"} fw={"bold"}>Pengalaman</Text>
|
<Text fz="md" fw="bold">Pengalaman</Text>
|
||||||
<EditEditor
|
<EditEditor
|
||||||
value={perbekelState.edit.form.pengalaman}
|
value={perbekelState.edit.form.pengalaman}
|
||||||
onChange={(val) => perbekelState.edit.form.pengalaman = val}
|
onChange={(val) => perbekelState.edit.form.pengalaman = val}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Pengalaman Organisasi */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"md"} fw={"bold"}>Pengalaman Organisasi</Text>
|
<Text fz="md" fw="bold">Pengalaman Organisasi</Text>
|
||||||
<EditEditor
|
<EditEditor
|
||||||
value={perbekelState.edit.form.pengalamanOrganisasi}
|
value={perbekelState.edit.form.pengalamanOrganisasi}
|
||||||
onChange={(val) => perbekelState.edit.form.pengalamanOrganisasi = val}
|
onChange={(val) => perbekelState.edit.form.pengalamanOrganisasi = val}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Program Unggulan */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"md"} fw={"bold"}>Program Unggulan</Text>
|
<Text fz="md" fw="bold">Program Unggulan</Text>
|
||||||
<EditEditor
|
<EditEditor
|
||||||
value={perbekelState.edit.form.programUnggulan}
|
value={perbekelState.edit.form.programUnggulan}
|
||||||
onChange={(val) => perbekelState.edit.form.programUnggulan = val}
|
onChange={(val) => perbekelState.edit.form.programUnggulan = val}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Submit */}
|
||||||
<Group>
|
<Group>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSubmit}
|
|
||||||
bg={colors['blue-button']}
|
bg={colors['blue-button']}
|
||||||
|
onClick={handleSubmit}
|
||||||
|
loading={isSubmitting || perbekelState.edit.loading}
|
||||||
|
disabled={!perbekelState.edit.form.biodata.trim()}
|
||||||
>
|
>
|
||||||
Submit
|
{isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button variant="outline" onClick={handleBack} disabled={isSubmitting || perbekelState.edit.loading}>
|
||||||
|
Batal
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
</Paper>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ProfilePerbekel;
|
export default ProfilePerbekel;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Divider, Grid, GridCol, Image, Paper, Stack, Text, Title } from '@mantine/core';
|
import { Box, Button, Center, Divider, Grid, GridCol, Image, Paper, Skeleton, Stack, Text, Title, Tooltip } from '@mantine/core';
|
||||||
import { IconEdit } from '@tabler/icons-react';
|
import { IconEdit } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
@@ -12,98 +12,102 @@ function Page() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const snap = useSnapshot(stateProfileDesa);
|
const snap = useSnapshot(stateProfileDesa);
|
||||||
|
|
||||||
// Panggil load data sekali saat komponen mount
|
// Load data saat mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
stateProfileDesa.profilPerbekel.findUnique.load("edit");
|
stateProfileDesa.profilPerbekel.findUnique.load("edit");
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const perbekel = snap.profilPerbekel.findUnique.data;
|
const perbekel = snap.profilPerbekel.findUnique.data;
|
||||||
|
|
||||||
|
if (!perbekel) {
|
||||||
return (
|
return (
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
<Stack align="center" justify="center" py="xl">
|
||||||
<Stack gap={"xs"}>
|
<Skeleton radius="md" height={800} />
|
||||||
<Grid>
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
|
||||||
|
<Stack gap="md">
|
||||||
|
{/* Header + tombol edit */}
|
||||||
|
<Grid align="center">
|
||||||
<GridCol span={{ base: 12, md: 11 }}>
|
<GridCol span={{ base: 12, md: 11 }}>
|
||||||
<Title order={3}>Preview Profile PPID</Title>
|
<Title order={3} c={colors['blue-button']}>Preview Profil PPID</Title>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
<GridCol span={{ base: 12, md: 1 }}>
|
<GridCol span={{ base: 12, md: 1 }}>
|
||||||
<Button bg={colors['blue-button']} onClick={() => router.push(`/admin/desa/profile/profile-perbekel/${snap.profilPerbekel.findUnique.data?.id}`)}>
|
<Tooltip label="Edit Profil Perbekel" withArrow>
|
||||||
<IconEdit size={16} />
|
<Button
|
||||||
|
c="green"
|
||||||
|
variant="light"
|
||||||
|
leftSection={<IconEdit size={18} stroke={2} />}
|
||||||
|
radius="md"
|
||||||
|
onClick={() => router.push(`/admin/desa/profile/profile-perbekel/${perbekel.id}`)}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
</Grid>
|
</Grid>
|
||||||
{perbekel && (
|
|
||||||
<Box>
|
{/* Card Profil */}
|
||||||
<Paper p={"xl"} bg={colors['BG-trans']}>
|
<Paper p="xl" bg={colors['white-1']} withBorder radius="md" shadow="xs">
|
||||||
<Box px={{ base: "md", md: 100 }}>
|
<Box px={{ base: "sm", md: 100 }}>
|
||||||
<Grid>
|
<Grid>
|
||||||
<GridCol span={{ base: 12, md: 12 }}>
|
<GridCol span={12}>
|
||||||
<Center>
|
<Center>
|
||||||
<Image src={"/darmasaba-icon.png"} w={{ base: 100, md: 150 }} alt='' />
|
<Image src="/darmasaba-icon.png" w={{ base: 100, md: 150 }} alt="Logo Desa" />
|
||||||
</Center>
|
</Center>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
<GridCol span={{ base: 12, md: 12 }}>
|
<GridCol span={12}>
|
||||||
<Text ta={"center"} fz={{ base: "1.2rem", md: "1.8rem" }} fw={'bold'}>PROFIL PIMPINAN BADAN PUBLIK DESA DARMASABA </Text>
|
<Text ta="center" fz={{ base: "1.2rem", md: "1.8rem" }} fw="bold" c={colors['blue-button']}>
|
||||||
|
Profil Pimpinan Badan Publik Desa Darmasaba
|
||||||
|
</Text>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Box>
|
</Box>
|
||||||
<Divider my={"md"} color={colors['blue-button']} />
|
<Divider my="md" color={colors['blue-button']} />
|
||||||
{/* biodata perbekel */}
|
|
||||||
<Box px={{ base: 0, md: 50 }} pb={30}>
|
<Stack gap={0} px={{ base: 0, md: 50 }} pb="xl">
|
||||||
<Box pb={20} px={{ base: 0, md: 50 }}>
|
|
||||||
<Paper bg={colors['BG-trans']} w={{ base: "100%", md: "100%" }}>
|
|
||||||
<Stack gap={0}>
|
|
||||||
<Center>
|
<Center>
|
||||||
<Image
|
<Image
|
||||||
pt={{ base: 0, md: 90 }}
|
pt={{ base: 0, md: 60 }}
|
||||||
src={perbekel.image?.link || "/perbekel.png"}
|
src={perbekel.image?.link || "/perbekel.png"}
|
||||||
w={{ base: 250, md: 350 }}
|
w={{ base: 250, md: 350 }}
|
||||||
alt='Foto Profil PPID'
|
alt="Foto Profil Perbekel"
|
||||||
onError={(e) => {
|
radius="md"
|
||||||
e.currentTarget.src = "/perbekel.png";
|
onError={(e) => { e.currentTarget.src = "/perbekel.png"; }}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
<Paper
|
<Paper
|
||||||
bg={colors['blue-button']}
|
bg={colors['blue-button']}
|
||||||
py={20}
|
py="md"
|
||||||
|
px="sm"
|
||||||
|
radius="md"
|
||||||
className="glass3"
|
className="glass3"
|
||||||
px={{ base: 10, md: 10 }}
|
style={{ mt: -30, boxShadow: '0 4px 20px rgba(0,0,0,0.15)' }}
|
||||||
|
|
||||||
>
|
>
|
||||||
<Text ta={"center"} c={colors['white-1']} fw={"bolder"} fz={{ base: "1.2rem", md: "1.6rem" }}>
|
<Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "1.2rem", md: "1.6rem" }}>
|
||||||
I.B. Surya Prabhawa Manuaba, S.H.,M.H.,NL.P.
|
I.B. Surya Prabhawa Manuaba, S.H., M.H.
|
||||||
</Text>
|
</Text>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
|
||||||
</Box>
|
{/* Biodata & Info */}
|
||||||
<Box pt={10}>
|
<Box mt="lg">
|
||||||
<Box>
|
<Text fz={{ base: "1.125rem", md: "1.5rem" }} fw="bold" mb={4}>Biodata</Text>
|
||||||
<Text fz={{ base: "1.125rem", md: "1.6rem" }} fw={'bold'}>Biodata</Text>
|
<Text fz={{ base: "1rem", md: "1.4rem" }} ta="justify" dangerouslySetInnerHTML={{ __html: perbekel.biodata }} />
|
||||||
<Text fz={{ base: "1rem", md: "1.5rem" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: perbekel.biodata }} />
|
|
||||||
</Box>
|
<Text fz={{ base: "1.125rem", md: "1.5rem" }} fw="bold" mt="md" mb={4}>Pengalaman</Text>
|
||||||
<Box>
|
<Text fz={{ base: "1rem", md: "1.4rem" }} ta="justify" dangerouslySetInnerHTML={{ __html: perbekel.pengalaman }} />
|
||||||
<Text fz={{ base: "1.125rem", md: "1.6rem" }} fw={'bold'}>Pengalaman</Text>
|
|
||||||
<Text fz={{ base: "1rem", md: "1.5rem" }} dangerouslySetInnerHTML={{ __html: perbekel.pengalaman }} />
|
<Text fz={{ base: "1.125rem", md: "1.5rem" }} fw="bold" mt="md" mb={4}>Pengalaman Organisasi</Text>
|
||||||
</Box>
|
<Text fz={{ base: "1rem", md: "1.4rem" }} ta="justify" dangerouslySetInnerHTML={{ __html: perbekel.pengalamanOrganisasi }} />
|
||||||
</Box>
|
|
||||||
<Box pb={30}>
|
<Text fz={{ base: "1.125rem", md: "1.5rem" }} fw="bold" mt="md" mb={4}>Program Kerja Unggulan</Text>
|
||||||
<Text fz={{ base: "1.125rem", md: "1.6rem" }} fw={'bold'}>Pengalaman Organisasi</Text>
|
<Text fz={{ base: "1rem", md: "1.4rem" }} ta="justify" dangerouslySetInnerHTML={{ __html: perbekel.programUnggulan }} />
|
||||||
<Box px={20}>
|
|
||||||
<Text fz={{ base: "1rem", md: "1.5rem" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: perbekel.pengalamanOrganisasi }} />
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
<Box pb={20}>
|
|
||||||
<Text fz={{ base: "1.125rem", md: "1.6rem" }} fw={'bold'}>Program Kerja Unggulan</Text>
|
|
||||||
<Box px={20}>
|
|
||||||
<Text fz={{ base: "1rem", md: "1.5rem" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: perbekel.programUnggulan }} />
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -91,7 +91,6 @@ function ListResponden({ search }: ListRespondenProps) {
|
|||||||
<Table
|
<Table
|
||||||
striped
|
striped
|
||||||
highlightOnHover
|
highlightOnHover
|
||||||
withTableBorder
|
|
||||||
withRowBorders
|
withRowBorders
|
||||||
verticalSpacing="sm"
|
verticalSpacing="sm"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -73,8 +73,6 @@ function ListDaftarInformasi({ search }: { search: string }) {
|
|||||||
<Box style={{ overflowX: 'auto' }}>
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
<Table
|
<Table
|
||||||
highlightOnHover
|
highlightOnHover
|
||||||
withTableBorder
|
|
||||||
withColumnBorders
|
|
||||||
striped
|
striped
|
||||||
stickyHeader
|
stickyHeader
|
||||||
style={{ minWidth: '700px' }}
|
style={{ minWidth: '700px' }}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ function ListResponden({ search }: ListRespondenProps) {
|
|||||||
<Paper withBorder bg="white" p="lg" radius="md" shadow="sm">
|
<Paper withBorder bg="white" p="lg" radius="md" shadow="sm">
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Title order={3}>Data Responden</Title>
|
<Title order={3}>Data Responden</Title>
|
||||||
<Table striped withTableBorder withRowBorders>
|
<Table striped withRowBorders>
|
||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh style={{ textAlign: 'center' }}>No</TableTh>
|
<TableTh style={{ textAlign: 'center' }}>No</TableTh>
|
||||||
@@ -82,7 +82,7 @@ function ListResponden({ search }: ListRespondenProps) {
|
|||||||
<Paper withBorder bg="white" p="lg" radius="md" shadow="sm">
|
<Paper withBorder bg="white" p="lg" radius="md" shadow="sm">
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Title order={3}>Data Responden</Title>
|
<Title order={3}>Data Responden</Title>
|
||||||
<Table striped withTableBorder withRowBorders>
|
<Table striped withRowBorders>
|
||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh>
|
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh>
|
||||||
|
|||||||
@@ -1,11 +1,53 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
// /api/berita/findManyPaginated.ts
|
||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
|
import { Context } from "elysia";
|
||||||
|
|
||||||
export default async function kategoriBeritaFindMany() {
|
async function kategoriBeritaFindMany(context: Context) {
|
||||||
const data = await prisma.kategoriBerita.findMany();
|
// Ambil parameter dari query
|
||||||
|
const page = Number(context.query.page) || 1;
|
||||||
|
const limit = Number(context.query.limit) || 10;
|
||||||
|
const search = (context.query.search as string) || '';
|
||||||
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
|
// Buat where clause
|
||||||
|
const where: any = { isActive: true };
|
||||||
|
|
||||||
|
// Tambahkan pencarian (jika ada)
|
||||||
|
if (search) {
|
||||||
|
where.OR = [
|
||||||
|
{ name: { contains: search, mode: 'insensitive' } },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Ambil data dan total count secara paralel
|
||||||
|
const [data, total] = await Promise.all([
|
||||||
|
prisma.kategoriBerita.findMany({
|
||||||
|
where,
|
||||||
|
skip,
|
||||||
|
take: limit,
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
}),
|
||||||
|
prisma.kategoriBerita.count({ where }),
|
||||||
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Success get all kategori berita",
|
message: "Berhasil ambil kategori berita dengan pagination",
|
||||||
data,
|
data,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
total,
|
||||||
|
totalPages: Math.ceil(total / limit),
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error di findMany paginated:", e);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Gagal mengambil data kategori berita",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default kategoriBeritaFindMany;
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
import { Context } from "elysia";
|
import { Context } from "elysia";
|
||||||
|
|
||||||
@@ -5,43 +6,44 @@ export default async function pelayananSuratKeteranganFindMany(context: Context)
|
|||||||
const page = Number(context.query.page) || 1;
|
const page = Number(context.query.page) || 1;
|
||||||
const limit = Number(context.query.limit) || 10;
|
const limit = Number(context.query.limit) || 10;
|
||||||
const skip = (page - 1) * limit;
|
const skip = (page - 1) * limit;
|
||||||
|
const search = (context.query.search as string) || '';
|
||||||
|
|
||||||
|
const where: any = { isActive: true };
|
||||||
|
|
||||||
|
// Tambahkan pencarian (jika ada)
|
||||||
|
if (search) {
|
||||||
|
where.OR = [
|
||||||
|
{ name: { contains: search, mode: 'insensitive' } },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Ambil data dan total count secara paralel
|
||||||
const [data, total] = await Promise.all([
|
const [data, total] = await Promise.all([
|
||||||
prisma.pelayananSuratKeterangan.findMany({
|
prisma.pelayananSuratKeterangan.findMany({
|
||||||
where: { isActive: true },
|
where,
|
||||||
include: {
|
|
||||||
image: true,
|
|
||||||
image2: true,
|
|
||||||
},
|
|
||||||
skip,
|
skip,
|
||||||
take: limit,
|
take: limit,
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: 'desc' },
|
||||||
}),
|
}),
|
||||||
prisma.pelayananSuratKeterangan.count({
|
prisma.pelayananSuratKeterangan.count({ where }),
|
||||||
where: { isActive: true }
|
|
||||||
})
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const totalPages = Math.ceil(total / limit);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Success fetch pelayanan surat keterangan with pagination",
|
message: "Berhasil ambil pelayanan surat keterangan dengan pagination",
|
||||||
data,
|
data,
|
||||||
page,
|
page,
|
||||||
totalPages,
|
limit,
|
||||||
total,
|
total,
|
||||||
|
totalPages: Math.ceil(total / limit),
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Find many paginated error:", e);
|
console.error("Error di findMany paginated:", e);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "Failed fetch pelayanan surat keterangan with pagination",
|
message: "Gagal mengambil data pelayanan surat keterangan",
|
||||||
data: [],
|
|
||||||
page: 1,
|
|
||||||
totalPages: 1,
|
|
||||||
total: 0,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,43 +1,49 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
import { Context } from "elysia";
|
import { Context } from "elysia";
|
||||||
|
|
||||||
export default async function pelayananTelunjukSaktiDesaFindMany(context: Context) {
|
export default async function pelayananTelunjukSaktiDesaFindMany(
|
||||||
|
context: Context
|
||||||
|
) {
|
||||||
const page = Number(context.query.page) || 1;
|
const page = Number(context.query.page) || 1;
|
||||||
const limit = Number(context.query.limit) || 10;
|
const limit = Number(context.query.limit) || 10;
|
||||||
|
const search = (context.query.search as string) || "";
|
||||||
const skip = (page - 1) * limit;
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
|
// Buat where clause
|
||||||
|
const where: any = { isActive: true };
|
||||||
|
|
||||||
|
// Tambahkan pencarian (jika ada)
|
||||||
|
if (search) {
|
||||||
|
where.OR = [{ name: { contains: search, mode: "insensitive" } }];
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Ambil data dan total count secara paralel
|
||||||
const [data, total] = await Promise.all([
|
const [data, total] = await Promise.all([
|
||||||
prisma.pelayananTelunjukSaktiDesa.findMany({
|
prisma.pelayananTelunjukSaktiDesa.findMany({
|
||||||
where: { isActive: true },
|
where,
|
||||||
skip,
|
skip,
|
||||||
take: limit,
|
take: limit,
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: "desc" },
|
||||||
}),
|
}),
|
||||||
prisma.pelayananTelunjukSaktiDesa.count({
|
prisma.pelayananTelunjukSaktiDesa.count({ where }),
|
||||||
where: { isActive: true }
|
|
||||||
})
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const totalPages = Math.ceil(total / limit);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Success fetch pelayanan telunjuk sakti desa with pagination",
|
message: "Berhasil ambil pelayanan telunjuk sakti desa dengan pagination",
|
||||||
data,
|
data,
|
||||||
page,
|
page,
|
||||||
totalPages,
|
limit,
|
||||||
total,
|
total,
|
||||||
|
totalPages: Math.ceil(total / limit),
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Find many paginated error:", e);
|
console.error("Error di findMany paginated:", e);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "Failed fetch pelayanan telunjuk sakti desa with pagination",
|
message: "Gagal mengambil data pelayanan telunjuk sakti desa",
|
||||||
data: [],
|
|
||||||
page: 1,
|
|
||||||
totalPages: 1,
|
|
||||||
total: 0,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,46 +1,50 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
import { Context } from "elysia";
|
import { Context } from "elysia";
|
||||||
|
|
||||||
export default async function penghargaanFindMany(context: Context) {
|
export default async function penghargaanFindMany(context: Context) {
|
||||||
const page = Number(context.query.page) || 1;
|
const page = Number(context.query.page) || 1;
|
||||||
const limit = Number(context.query.limit) || 10;
|
const limit = Number(context.query.limit) || 10;
|
||||||
|
const search = (context.query.search as string) || "";
|
||||||
const skip = (page - 1) * limit;
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
|
// Buat where clause
|
||||||
|
const where: any = { isActive: true };
|
||||||
|
|
||||||
|
// Tambahkan pencarian (jika ada)
|
||||||
|
if (search) {
|
||||||
|
where.OR = [
|
||||||
|
{ name: { contains: search, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: search, mode: "insensitive" } },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Ambil data dan total count secara paralel
|
||||||
const [data, total] = await Promise.all([
|
const [data, total] = await Promise.all([
|
||||||
prisma.penghargaan.findMany({
|
prisma.penghargaan.findMany({
|
||||||
where: { isActive: true },
|
where,
|
||||||
include: {
|
|
||||||
image: true,
|
|
||||||
},
|
|
||||||
skip,
|
skip,
|
||||||
take: limit,
|
take: limit,
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: "desc" },
|
||||||
}),
|
}),
|
||||||
prisma.penghargaan.count({
|
prisma.penghargaan.count({ where }),
|
||||||
where: { isActive: true }
|
|
||||||
})
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const totalPages = Math.ceil(total / limit);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Success fetch penghargaan with pagination",
|
message: "Berhasil ambil penghargaan dengan pagination",
|
||||||
data,
|
data,
|
||||||
page,
|
page,
|
||||||
totalPages,
|
limit,
|
||||||
total,
|
total,
|
||||||
|
totalPages: Math.ceil(total / limit),
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Find many paginated error:", e);
|
console.error("Error di findMany paginated:", e);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: "Failed fetch penghargaan with pagination",
|
message: "Gagal mengambil data penghargaan",
|
||||||
data: [],
|
|
||||||
page: 1,
|
|
||||||
totalPages: 1,
|
|
||||||
total: 0,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,54 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
|
import { Context } from "elysia";
|
||||||
|
|
||||||
async function kategoriPengumumanFindMany() {
|
async function kategoriPengumumanFindMany(context: Context) {
|
||||||
const data = await prisma.categoryPengumuman.findMany({
|
const page = Number(context.query.page) || 1;
|
||||||
|
const limit = Number(context.query.limit) || 10;
|
||||||
|
const search = (context.query.search as string) || "";
|
||||||
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
|
// Buat where clause
|
||||||
|
const where: any = { isActive: true };
|
||||||
|
|
||||||
|
// Tambahkan pencarian (jika ada)
|
||||||
|
if (search) {
|
||||||
|
where.OR = [{ name: { contains: search, mode: "insensitive" } }];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [data, total] = await Promise.all([
|
||||||
|
prisma.categoryPengumuman.findMany({
|
||||||
|
where,
|
||||||
|
skip,
|
||||||
|
take: limit,
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
include: {
|
include: {
|
||||||
_count: {
|
_count: {
|
||||||
select: {
|
select: {
|
||||||
pengumumans: true
|
pengumumans: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.categoryPengumuman.count({ where }),
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Berhasil ambil kategori potensi dengan pagination",
|
||||||
|
data,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
total,
|
||||||
|
totalPages: Math.ceil(total / limit),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error di findMany paginated:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Gagal mengambil data kategori potensi",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
return { data };
|
|
||||||
}
|
|
||||||
|
|
||||||
export default kategoriPengumumanFindMany
|
export default kategoriPengumumanFindMany;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
import { Context } from "elysia";
|
import { Context } from "elysia";
|
||||||
|
|
||||||
@@ -5,11 +6,23 @@ export default async function potensiDesaFindMany(context: Context) {
|
|||||||
const page = Number(context.query.page) || 1;
|
const page = Number(context.query.page) || 1;
|
||||||
const limit = Number(context.query.limit) || 10;
|
const limit = Number(context.query.limit) || 10;
|
||||||
const skip = (page - 1) * limit;
|
const skip = (page - 1) * limit;
|
||||||
|
const search = (context.query.search as string) || '';
|
||||||
|
|
||||||
|
const where: any = { isActive: true };
|
||||||
|
|
||||||
|
// Tambahkan pencarian (jika ada)
|
||||||
|
if (search) {
|
||||||
|
where.OR = [
|
||||||
|
{ name: { contains: search, mode: 'insensitive' } },
|
||||||
|
{ kategori: { nama: { contains: search, mode: 'insensitive' } } },
|
||||||
|
{ deskripsi: { contains: search, mode: 'insensitive' } },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [data, total] = await Promise.all([
|
const [data, total] = await Promise.all([
|
||||||
prisma.potensiDesa.findMany({
|
prisma.potensiDesa.findMany({
|
||||||
where: { isActive: true },
|
where,
|
||||||
skip,
|
skip,
|
||||||
take: limit,
|
take: limit,
|
||||||
include: {
|
include: {
|
||||||
@@ -23,14 +36,12 @@ export default async function potensiDesaFindMany(context: Context) {
|
|||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const totalPages = Math.ceil(total / limit);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Success fetch potensi desa with pagination",
|
message: "Success fetch potensi desa with pagination",
|
||||||
data,
|
data,
|
||||||
page,
|
page,
|
||||||
totalPages,
|
totalPages: Math.ceil(total / limit),
|
||||||
total,
|
total,
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -1,11 +1,53 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
// /api/berita/findManyPaginated.ts
|
||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
|
import { Context } from "elysia";
|
||||||
|
|
||||||
export default async function kategoriPotensiFindMany() {
|
async function kategoriPotensiFindMany(context: Context) {
|
||||||
const data = await prisma.kategoriPotensi.findMany();
|
// Ambil parameter dari query
|
||||||
|
const page = Number(context.query.page) || 1;
|
||||||
|
const limit = Number(context.query.limit) || 10;
|
||||||
|
const search = (context.query.search as string) || '';
|
||||||
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
|
// Buat where clause
|
||||||
|
const where: any = { isActive: true };
|
||||||
|
|
||||||
|
// Tambahkan pencarian (jika ada)
|
||||||
|
if (search) {
|
||||||
|
where.OR = [
|
||||||
|
{ nama: { contains: search, mode: 'insensitive' } },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Ambil data dan total count secara paralel
|
||||||
|
const [data, total] = await Promise.all([
|
||||||
|
prisma.kategoriPotensi.findMany({
|
||||||
|
where,
|
||||||
|
skip,
|
||||||
|
take: limit,
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
}),
|
||||||
|
prisma.kategoriPotensi.count({ where }),
|
||||||
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Success get all kategori potensi",
|
message: "Berhasil ambil kategori potensi dengan pagination",
|
||||||
data,
|
data,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
total,
|
||||||
|
totalPages: Math.ceil(total / limit),
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error di findMany paginated:", e);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Gagal mengambil data kategori potensi",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default kategoriPotensiFindMany;
|
||||||
Reference in New Issue
Block a user